完成基础功能

This commit is contained in:
zyj
2026-03-10 15:49:36 +08:00
parent 1be2f4021f
commit 8a0b1e6e98
19 changed files with 3724 additions and 529 deletions

983
libs/device.go Normal file
View File

@@ -0,0 +1,983 @@
package libs
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"log"
"net"
"strings"
"sync"
"time"
"github.com/zhuy1228/go-mobile-uiautomator/adb"
"github.com/zhuy1228/go-mobile-uiautomator/services"
)
// ---------- Device核心设备客户端 ----------
// Device 是 UIAutomator2 的核心客户端
// 封装了设备连接、UIAutomator2 服务管理、JSON-RPC 调用等功能
// 对应 Python 版本的 Device 类
type Device struct {
// ADB 连接信息
addr string // ADB 服务器地址
serial string // 设备序列号
// UIAutomator2 服务配置
serverPort int // 设备端服务端口(默认 9008
debug bool // 调试模式
jarPath string // 本地 u2.jar 路径(空字符串使用默认路径)
// 设备连接接口(通过 adb forward 连接)
dev AdbDevice
// JSON-RPC 调用器
jsonrpc *JsonRpcWrapper
// 设置
settings *Settings
// UIAutomator2 进程管理
mu sync.Mutex
processConn net.Conn // 启动 UIAutomator 时的连接
// 窗口尺寸缓存
windowSizeCache [2]int
}
// NewDevice 创建一个新的 Device 客户端并启动 UIAutomator2 服务
// 使用 adb forward 端口转发,与 Python uiautomator2 相同的方案
//
// serial: 设备序列号(如 "emulator-5554"
// addr: 可选ADB 服务器地址,不传则使用默认值 "127.0.0.1:5037"
//
// 创建后会自动执行:
// 1. 设置 adb forward 端口转发
// 2. 推送 u2.jar 到设备(如果尚未存在)
// 3. 启动 UIAutomator2 服务
// 4. 等待服务就绪
func NewDevice(serial string, addr ...string) (*Device, error) {
a := DefaultADBAddr
if len(addr) > 0 && addr[0] != "" {
a = addr[0]
}
// 使用 ADB 隧道设备(与 Python uiautomator2 完全一致,无需 adb forward
dev := &AdbTunnelDevice{AdbAddr: a, Serial: serial}
d := &Device{
addr: a,
serial: serial,
serverPort: DeviceServerPort,
dev: dev,
settings: NewSettings(),
}
// 创建 JSON-RPC 调用器
d.jsonrpc = NewJsonRpcWrapper(func(method string, params interface{}, timeout float64) (json.RawMessage, error) {
return d.jsonrpcCall(method, params, timeout)
})
// 推送 u2.jar 到设备(仅在文件不存在时推送)
if err := services.InstallServiceJar(a, serial, d.jarPath, false); err != nil {
return nil, fmt.Errorf("安装 u2.jar 失败: %w", err)
}
// 启动 UIAutomator2 服务
if err := d.StartUiautomator(); err != nil {
return nil, err
}
return d, nil
}
// NewDeviceWithoutStart 创建 Device 但不自动启动 UIAutomator2 服务
// 适用于服务已经在设备上运行的场景
//
// serial: 设备序列号
// addr: 可选ADB 服务器地址,不传则使用默认值 "127.0.0.1:5037"
func NewDeviceWithoutStart(serial string, addr ...string) *Device {
a := DefaultADBAddr
if len(addr) > 0 && addr[0] != "" {
a = addr[0]
}
dev := &AdbTunnelDevice{AdbAddr: a, Serial: serial}
d := &Device{
addr: a,
serial: serial,
serverPort: DeviceServerPort,
dev: dev,
settings: NewSettings(),
}
d.jsonrpc = NewJsonRpcWrapper(func(method string, params interface{}, timeout float64) (json.RawMessage, error) {
return d.jsonrpcCall(method, params, timeout)
})
return d
}
// ---------- 设备属性 ----------
// Serial 返回设备序列号
func (d *Device) Serial() string {
return d.serial
}
// Settings 返回设备的配置管理器
func (d *Device) Settings() *Settings {
return d.settings
}
// SetDebug 设置调试模式
func (d *Device) SetDebug(debug bool) {
d.debug = debug
}
// Debug 返回是否为调试模式
func (d *Device) Debug() bool {
return d.debug
}
// ---------- UIAutomator2 服务管理 ----------
// StartUiautomator 启动 UIAutomator2 服务
// 如果服务已经在运行(/ping 响应 pong则不会重复启动
func (d *Device) StartUiautomator() error {
d.mu.Lock()
defer d.mu.Unlock()
// 检查服务是否已经在运行
if d.checkAlive() {
return nil
}
// 启动 UIAutomator2 进程
return d.launchAndWait()
}
// StopUiautomator 停止 UIAutomator2 服务
func (d *Device) StopUiautomator() {
d.mu.Lock()
if d.processConn != nil {
d.processConn.Close()
d.processConn = nil
}
d.mu.Unlock()
// 等待服务退出
deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
if !d.checkAlive() {
return
}
time.Sleep(500 * time.Millisecond)
}
}
// Close 关闭设备连接,停止 UIAutomator2 服务
// ADB 隧道方案无需额外的端口清理
func (d *Device) Close() {
d.StopUiautomator()
}
// ResetUiautomator 重启 UIAutomator2 服务
func (d *Device) ResetUiautomator() error {
d.StopUiautomator()
return d.StartUiautomator()
}
// checkAlive 通过 /ping 端点检查 UIAutomator2 服务是否存活
func (d *Device) checkAlive() bool {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := HttpRequest(ctx, d.dev, d.serverPort, "GET", "/ping", nil, 5.0, false)
if err != nil {
return false
}
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
conn, err := adb.ConnectToDevice(d.addr, d.serial, 15*time.Second)
if err != nil {
return &LaunchUiAutomationError{Message: "连接 ADB 失败", Output: err.Error()}
}
// 启动 UIAutomator2 服务进程
cmd := "shell:CLASSPATH=/data/local/tmp/u2.jar app_process / com.wetest.uia2.Main"
if err := adb.WriteAdbCmd(conn, cmd); err != nil {
conn.Close()
return &LaunchUiAutomationError{Message: "发送启动命令失败", Output: err.Error()}
}
status, err := adb.ReadStatus(conn)
if err != nil {
conn.Close()
return &LaunchUiAutomationError{Message: "读取启动状态失败", Output: err.Error()}
}
if status != "OKAY" {
conn.Close()
return &LaunchUiAutomationError{Message: fmt.Sprintf("启动状态异常: %s", status)}
}
d.processConn = conn
// 启动后台 goroutine 读取输出
go func() {
buf := make([]byte, 1024)
for {
n, err := conn.Read(buf)
if n > 0 {
output := string(buf[:n])
if d.debug {
log.Printf("[UIAutomator2] %s", output)
}
// 检查是否有 "already registered" 错误
if strings.Contains(output, "already registered") {
log.Printf("[UIAutomator2] 辅助功能服务已注册,需要重启")
}
}
if err != nil {
break
}
}
}()
// 等待服务就绪
return d.waitReady(30 * time.Second)
}
// waitReady 等待 UIAutomator2 服务就绪
func (d *Device) waitReady(timeout time.Duration) error {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if d.checkAlive() {
return nil
}
time.Sleep(500 * time.Millisecond)
}
return &LaunchUiAutomationError{Message: "服务启动超时"}
}
// ---------- JSON-RPC 调用 ----------
// jsonrpcCall 发送 JSON-RPC 调用,失败时自动重启 UIAutomator2 并重试
func (d *Device) jsonrpcCall(method string, params interface{}, timeout float64) (json.RawMessage, error) {
ctx := context.Background()
result, err := JsonRpcCall(ctx, d.dev, d.serverPort, method, params, timeout, d.debug)
if err != nil {
// 如果是连接错误或 UIAutomation 断开,尝试重启
var uaErr *UiAutomationNotConnectedError
var httpErr *HTTPError
if errors.As(err, &uaErr) || errors.As(err, &httpErr) {
log.Printf("UIAutomator2 服务异常,正在重启: %v", err)
d.StopUiautomator()
if startErr := d.StartUiautomator(); startErr != nil {
return nil, startErr
}
// 重试一次
return JsonRpcCall(ctx, d.dev, d.serverPort, method, params, timeout, d.debug)
}
return nil, err
}
return result, nil
}
// JsonRpc 返回 JSON-RPC 调用包装器
func (d *Device) JsonRpc() *JsonRpcWrapper {
return d.jsonrpc
}
// ---------- Shell 命令 ----------
// ShellResponse Shell 命令的返回结果
type ShellResponse struct {
Output string // 命令输出
ExitCode int // 退出码(暂不支持精确返回)
}
// Shell 在设备上执行 Shell 命令
func (d *Device) Shell(cmdArgs ...string) (*ShellResponse, error) {
cmd := strings.Join(cmdArgs, " ")
conn, err := adb.ConnectToDevice(d.addr, d.serial, 10*time.Second)
if err != nil {
return nil, &AdbShellError{Message: fmt.Sprintf("连接失败: %v", err)}
}
defer conn.Close()
out, err := adb.ExecShell(conn, cmd)
if err != nil {
return nil, &AdbShellError{Message: fmt.Sprintf("执行失败: %v", err)}
}
return &ShellResponse{
Output: string(out),
ExitCode: 0,
}, nil
}
// ---------- 设备信息 ----------
// Info 获取设备的 UI 信息(通过 JSON-RPC deviceInfo
func (d *Device) Info() (map[string]interface{}, error) {
raw, err := d.jsonrpc.Call("deviceInfo", nil, 10)
if err != nil {
return nil, err
}
var info map[string]interface{}
if err := json.Unmarshal(raw, &info); err != nil {
return nil, err
}
return info, nil
}
// DeviceInfo 获取设备的硬件信息(通过 getprop
func (d *Device) DeviceInfo() (map[string]interface{}, error) {
info := make(map[string]interface{})
props := []struct {
key string
prop string
}{
{"serial", "ro.serialno"},
{"sdk", "ro.build.version.sdk"},
{"brand", "ro.product.brand"},
{"model", "ro.product.model"},
{"arch", "ro.product.cpu.abi"},
{"version", "ro.build.version.release"},
}
for _, p := range props {
resp, err := d.Shell("getprop", p.prop)
if err == nil {
info[p.key] = strings.TrimSpace(resp.Output)
}
}
return info, nil
}
// WindowSize 获取设备屏幕尺寸(宽, 高)
func (d *Device) WindowSize() (int, int, error) {
if d.windowSizeCache[0] > 0 {
return d.windowSizeCache[0], d.windowSizeCache[1], nil
}
info, err := d.Info()
if err != nil {
return 0, 0, err
}
w := int(info["displayWidth"].(float64))
h := int(info["displayHeight"].(float64))
d.windowSizeCache = [2]int{w, h}
return w, h, nil
}
// ---------- 基础操作 ----------
// Click 点击屏幕坐标
func (d *Device) Click(x, y int) error {
d.operationDelay("click")
_, err := d.jsonrpc.Call("click", []interface{}{x, y})
d.operationDelayAfter("click")
return err
}
// DoubleClick 双击屏幕坐标
func (d *Device) DoubleClick(x, y int, duration float64) error {
if duration <= 0 {
duration = 0.1
}
// 第一次按下抬起
_, err := d.jsonrpc.Call("injectInputEvent", []interface{}{ActionDown, x, y, 0})
if err != nil {
return err
}
_, err = d.jsonrpc.Call("injectInputEvent", []interface{}{ActionUp, x, y, 0})
if err != nil {
return err
}
time.Sleep(time.Duration(duration * float64(time.Second)))
// 第二次点击
return d.Click(x, y)
}
// LongClick 长按屏幕坐标
// duration 为按住时间(秒),默认 0.5 秒
func (d *Device) LongClick(x, y int, duration float64) error {
if duration <= 0 {
duration = 0.5
}
d.operationDelay("click")
_, err := d.jsonrpc.Call("click", []interface{}{x, y, int(duration * 1000)})
d.operationDelayAfter("click")
return err
}
// Swipe 从 (fx,fy) 滑动到 (tx,ty)
// steps: 滑动步数,每步约 5ms
func (d *Device) Swipe(fx, fy, tx, ty, steps int) error {
if steps < 2 {
steps = 2
}
d.operationDelay("swipe")
_, err := d.jsonrpc.Call("swipe", []interface{}{fx, fy, tx, ty, steps})
d.operationDelayAfter("swipe")
return err
}
// SwipeWithDuration 按持续时间滑动
func (d *Device) SwipeWithDuration(fx, fy, tx, ty int, duration float64) error {
steps := int(duration * 200)
if steps < 2 {
steps = ScrollSteps
}
return d.Swipe(fx, fy, tx, ty, steps)
}
// SwipePoints 多点连续滑动
// points 为坐标点列表 [[x1,y1], [x2,y2], ...]
// duration 为总持续时间(秒)
func (d *Device) SwipePoints(points [][2]int, duration float64) error {
ppoints := make([]interface{}, 0, len(points)*2)
for _, p := range points {
ppoints = append(ppoints, p[0], p[1])
}
steps := int(duration / 0.005)
_, err := d.jsonrpc.Call("swipePoints", []interface{}{ppoints, steps})
return err
}
// Drag 将坐标从 (sx,sy) 拖拽到 (ex,ey)
func (d *Device) Drag(sx, sy, ex, ey int, duration float64) error {
if duration <= 0 {
duration = 0.5
}
d.operationDelay("drag")
_, err := d.jsonrpc.Call("drag", []interface{}{sx, sy, ex, ey, int(duration * 200)})
d.operationDelayAfter("drag")
return err
}
// Press 按键操作
// key 可以是按键名称(如 "home", "back")或按键代码
func (d *Device) Press(key string) error {
d.operationDelay("press")
_, err := d.jsonrpc.Call("pressKey", []interface{}{key})
d.operationDelayAfter("press")
return err
}
// PressKeyCode 按键代码操作
func (d *Device) PressKeyCode(keyCode int, meta ...int) error {
d.operationDelay("press")
params := []interface{}{keyCode}
if len(meta) > 0 {
params = append(params, meta[0])
}
_, err := d.jsonrpc.Call("pressKeyCode", params)
d.operationDelayAfter("press")
return err
}
// LongPress 长按按键
func (d *Device) LongPress(key string) error {
d.operationDelay("press")
_, err := d.Shell("input", "keyevent", "--longpress", strings.ToUpper(key))
d.operationDelayAfter("press")
return err
}
// ---------- 屏幕操作 ----------
// ScreenOn 唤醒屏幕
func (d *Device) ScreenOn() error {
_, err := d.jsonrpc.Call("wakeUp", nil)
return err
}
// ScreenOff 熄灭屏幕
func (d *Device) ScreenOff() error {
_, err := d.jsonrpc.Call("sleep", nil)
return err
}
// Screenshot 截取屏幕截图,返回 JPEG 图片的原始字节
func (d *Device) Screenshot() ([]byte, error) {
raw, err := d.jsonrpc.Call("takeScreenshot", []interface{}{1, 80})
if err != nil {
return nil, err
}
// 结果是 base64 编码的字符串
var base64Data string
if err := json.Unmarshal(raw, &base64Data); err != nil {
return nil, fmt.Errorf("解析截图数据失败: %w", err)
}
if base64Data == "" {
return nil, fmt.Errorf("截图返回空数据")
}
// Base64 解码(使用标准库)
decoded, err := base64.StdEncoding.DecodeString(base64Data)
if err != nil {
return nil, fmt.Errorf("Base64 解码失败: %w", err)
}
return decoded, nil
}
// ---------- 层级转储 ----------
// DumpHierarchy 转储当前窗口的 UI 层级 XML
// maxDepth 为最大递归深度0 使用默认值
func (d *Device) DumpHierarchy(compressed bool, maxDepth int) (string, error) {
if maxDepth <= 0 {
maxDepth = d.settings.GetInt("max_depth")
if maxDepth <= 0 {
maxDepth = 50
}
}
raw, err := d.jsonrpc.Call("dumpWindowHierarchy", []interface{}{compressed, maxDepth})
if err != nil {
return "", err
}
var content string
if err := json.Unmarshal(raw, &content); err != nil {
return "", err
}
if content == "" {
return "", &HierarchyEmptyError{Message: "层级转储为空"}
}
if strings.Contains(content, `<hierarchy rotation="0" />`) {
return "", &HierarchyEmptyError{Message: "层级转储为空(无子节点)"}
}
return content, nil
}
// ---------- 方向和旋转 ----------
// Orientation 获取当前屏幕方向
func (d *Device) Orientation() (string, error) {
info, err := d.Info()
if err != nil {
return "", err
}
rotation := int(info["displayRotation"].(float64))
for _, o := range Orientations {
if o.Value == rotation {
return o.Name, nil
}
}
return "natural", nil
}
// SetOrientation 设置屏幕方向
// value 可以是 "natural"/"n"、"left"/"l"、"right"/"r"、"upsidedown"/"u"
func (d *Device) SetOrientation(value string) error {
for _, o := range Orientations {
if value == o.Name || value == o.Short || value == fmt.Sprintf("%d", o.Value) {
_, err := d.jsonrpc.Call("setOrientation", []interface{}{o.Name})
return err
}
}
return fmt.Errorf("无效的方向值: %s", value)
}
// FreezeRotation 冻结/解冻屏幕旋转
func (d *Device) FreezeRotation(freeze bool) error {
_, err := d.jsonrpc.Call("freezeRotation", []interface{}{freeze})
return err
}
// ---------- 通知和快捷设置 ----------
// OpenNotification 打开通知栏
func (d *Device) OpenNotification() error {
_, err := d.jsonrpc.Call("openNotification", nil)
return err
}
// OpenQuickSettings 打开快捷设置
func (d *Device) OpenQuickSettings() error {
_, err := d.jsonrpc.Call("openQuickSettings", nil)
return err
}
// OpenURL 通过浏览器打开 URL
func (d *Device) OpenURL(url string) error {
_, err := d.Shell("am", "start", "-a", "android.intent.action.VIEW", "-d", url)
return err
}
// ---------- 剪贴板 ----------
// GetClipboard 获取剪贴板内容
func (d *Device) GetClipboard() (string, error) {
raw, err := d.jsonrpc.Call("getClipboard", nil)
if err != nil {
return "", err
}
var text string
json.Unmarshal(raw, &text)
return text, nil
}
// SetClipboard 设置剪贴板内容
func (d *Device) SetClipboard(text string, label ...string) error {
l := ""
if len(label) > 0 {
l = label[0]
}
_, err := d.jsonrpc.Call("setClipboard", []interface{}{l, text})
return err
}
// ---------- Toast ----------
// GetLastToast 获取最后一个 Toast 消息
func (d *Device) GetLastToast() (string, error) {
raw, err := d.jsonrpc.Call("getLastToast", nil)
if err != nil {
return "", err
}
var text string
json.Unmarshal(raw, &text)
return text, nil
}
// ClearToast 清除 Toast 消息
func (d *Device) ClearToast() error {
_, err := d.jsonrpc.Call("clearLastToast", nil)
return err
}
// MakeToast 在设备上显示 Toast 消息
func (d *Device) MakeToast(text string, durationMs float64) error {
_, err := d.jsonrpc.Call("makeToast", []interface{}{text, durationMs * 1000})
return err
}
// ---------- 等待超时 ----------
// ImplicitlyWait 设置默认等待超时
func (d *Device) ImplicitlyWait(seconds float64) error {
return d.settings.Set("wait_timeout", seconds)
}
// WaitTimeout 获取当前的等待超时时间
func (d *Device) WaitTimeout() float64 {
return d.settings.GetFloat64("wait_timeout")
}
// ---------- UiObject 选择器入口 ----------
// FindElement 根据选择器参数查找 UI 元素,返回 UiObject
func (d *Device) FindElement(params map[string]interface{}) (*UiObject, error) {
sel, err := New(params)
if err != nil {
return nil, err
}
return NewUiObject(d, sel), nil
}
// By 通过任意选择器参数查找 UI 元素(便捷方法)
//
// 用法类似 Python 的 d(text="xxx", className="yyy")
//
// d.By(libs.P{"text": "登录"}).Click()
// d.By(libs.P{"resourceId": "com.example:id/btn", "clickable": true}).Click()
func (d *Device) By(params map[string]interface{}) *UiObject {
sel := MustNew(params)
return NewUiObject(d, sel)
}
// ByText 通过文本查找 UI 元素
//
// d.ByText("向设备添加账号").Click()
func (d *Device) ByText(text string) *UiObject {
return d.By(map[string]interface{}{"text": text})
}
// ByTextContains 通过包含的文本查找 UI 元素
//
// d.ByTextContains("添加").Click()
func (d *Device) ByTextContains(text string) *UiObject {
return d.By(map[string]interface{}{"textContains": text})
}
// ByResourceId 通过资源 ID 查找 UI 元素
//
// d.ByResourceId("com.example:id/login_btn").Click()
func (d *Device) ByResourceId(id string) *UiObject {
return d.By(map[string]interface{}{"resourceId": id})
}
// ByDescription 通过 contentDescription 查找 UI 元素
//
// d.ByDescription("返回").Click()
func (d *Device) ByDescription(desc string) *UiObject {
return d.By(map[string]interface{}{"description": desc})
}
// ByClassName 通过类名查找 UI 元素
//
// d.ByClassName("android.widget.EditText").SetText("hello")
func (d *Device) ByClassName(className string) *UiObject {
return d.By(map[string]interface{}{"className": className})
}
// P 是 map[string]interface{} 的别名,用于简化选择器参数书写
//
// d.By(libs.P{"text": "确定", "clickable": true})
type P = map[string]interface{}
// ---------- 应用管理 ----------
// AppStart 启动应用
// packageName: 包名
// activity: Activity 名称(可选)
// stop: 是否先停止应用
func (d *Device) AppStart(packageName string, activity string, stop bool) error {
if stop {
d.AppStop(packageName)
}
if activity == "" {
// 使用 monkey 命令启动
_, err := d.Shell("monkey", "-p", packageName, "-c",
"android.intent.category.LAUNCHER", "1")
return err
}
args := []string{
"am", "start",
"-a", "android.intent.action.MAIN",
"-c", "android.intent.category.LAUNCHER",
"-n", fmt.Sprintf("%s/%s", packageName, activity),
}
_, err := d.Shell(args...)
return err
}
// AppStop 停止应用
func (d *Device) AppStop(packageName string) error {
_, err := d.Shell("am", "force-stop", packageName)
return err
}
// AppClear 清除应用数据
func (d *Device) AppClear(packageName string) error {
_, err := d.Shell("pm", "clear", packageName)
return err
}
// AppUninstall 卸载应用
func (d *Device) AppUninstall(packageName string) (bool, error) {
resp, err := d.Shell("pm", "uninstall", packageName)
if err != nil {
return false, err
}
return strings.Contains(resp.Output, "Success"), nil
}
// AppCurrent 获取当前前台应用信息
func (d *Device) AppCurrent() (map[string]string, error) {
resp, err := d.Shell("dumpsys", "activity", "activities")
if err != nil {
return nil, err
}
result := make(map[string]string)
lines := strings.Split(resp.Output, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
// 解析 mResumedActivity 或 mFocusedActivity
if strings.Contains(line, "mResumedActivity") || strings.Contains(line, "mFocusedActivity") {
// 格式: mResumedActivity: ActivityRecord{... pkg/activity ...}
parts := strings.Fields(line)
for _, p := range parts {
if strings.Contains(p, "/") && !strings.HasPrefix(p, "{") {
comp := strings.TrimSuffix(p, "}")
slash := strings.Index(comp, "/")
if slash > 0 {
result["package"] = comp[:slash]
result["activity"] = comp[slash+1:]
return result, nil
}
}
}
}
}
return result, &DeviceError{Message: "无法获取前台应用信息"}
}
// AppWait 等待应用启动
// timeout: 超时时间(秒)
// front: 是否等待到前台
// 返回应用的 PID0 表示未启动
func (d *Device) AppWait(packageName string, timeout float64, front bool) (int, error) {
if timeout <= 0 {
timeout = 20.0
}
deadline := time.Now().Add(time.Duration(timeout * float64(time.Second)))
for time.Now().Before(deadline) {
if front {
current, err := d.AppCurrent()
if err == nil && current["package"] == packageName {
pid := d.pidOfApp(packageName)
if pid > 0 {
return pid, nil
}
}
} else {
pid := d.pidOfApp(packageName)
if pid > 0 {
return pid, nil
}
}
time.Sleep(1 * time.Second)
}
return 0, nil
}
// pidOfApp 获取应用的进程 ID
func (d *Device) pidOfApp(packageName string) int {
resp, err := d.Shell("ps", "-A")
if err != nil {
return 0
}
output := resp.Output
if len(strings.TrimSpace(output)) <= 1 {
resp, err = d.Shell("ps")
if err != nil {
return 0
}
output = resp.Output
}
lines := strings.Split(output, "\n")
for _, line := range lines {
fields := strings.Fields(strings.TrimSpace(line))
if len(fields) >= 9 && fields[len(fields)-1] == packageName {
pid := 0
fmt.Sscanf(fields[1], "%d", &pid)
return pid
}
}
return 0
}
// ---------- 文件操作 ----------
// Push 推送文件到设备
func (d *Device) Push(localPath, remotePath string) error {
_, err := adb.PushFile(d.addr, d.serial, localPath, remotePath, 0644, d.debug)
return err
}
// ---------- 辅助方法 ----------
// operationDelayHelper 通用操作延迟辅助函数
// isBefore 为 true 时取操作前延迟,否则取操作后延迟
func (d *Device) operationDelayHelper(operation string, isBefore bool) {
methods := d.settings.GetStringSlice("operation_delay_methods")
for _, m := range methods {
if m == operation {
before, after := d.settings.GetOperationDelay()
delay := after
if isBefore {
delay = before
}
if delay > 0 {
time.Sleep(time.Duration(delay * float64(time.Second)))
}
return
}
}
}
// operationDelay 操作前延迟
func (d *Device) operationDelay(operation string) {
d.operationDelayHelper(operation, true)
}
// operationDelayAfter 操作后延迟
func (d *Device) operationDelayAfter(operation string) {
d.operationDelayHelper(operation, false)
}
// ---------- 存在性检查 ----------
// Exists 检查匹配选择器参数的 UI 元素是否存在
func (d *Device) Exists(params map[string]interface{}) (bool, error) {
obj, err := d.FindElement(params)
if err != nil {
return false, err
}
return obj.Exists()
}
// ClearText 清除输入框文本
func (d *Device) ClearText() error {
_, err := d.jsonrpc.Call("clearInputText", nil)
return err
}
// Keyevent 发送按键事件
func (d *Device) Keyevent(key string) error {
_, err := d.Shell("input", "keyevent", strings.ToUpper(key))
return err
}
// WlanIP 获取设备 WLAN IP 地址
func (d *Device) WlanIP() (string, error) {
resp, err := d.Shell("ip", "addr", "show", "wlan0")
if err != nil {
return "", err
}
lines := strings.Split(resp.Output, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "inet ") {
parts := strings.Fields(line)
if len(parts) >= 2 {
ip := strings.Split(parts[1], "/")[0]
return ip, nil
}
}
}
return "", nil
}
// Unlock 解锁屏幕(从左下滑到右上)
func (d *Device) Unlock() error {
info, err := d.Info()
if err != nil {
return err
}
screenOn, _ := info["screenOn"].(bool)
if !screenOn {
d.Keyevent("POWER")
w, h, err := d.WindowSize()
if err != nil {
return err
}
return d.Swipe(int(float64(w)*0.1), int(float64(h)*0.9),
int(float64(w)*0.9), int(float64(h)*0.1), ScrollSteps)
}
return nil
}

View File

@@ -1,16 +1,45 @@
// Package libs 提供了 UIAutomator2 服务交互的工具库
// Package libs 提供了完整的 Android UIAutomator2 自动化框架
//
// 本包包含以下核心组件:
// - AdbHTTPConnection通过 ADB 隧道发送 HTTP 请求到设备端 UIAutomator2 服务
// - SelectorUI 元素选择器构造器,支持文本、类名、资源 ID 等多种查询条件
// - HTTPResponseHTTP 响应封装
// 本包是 Python uiautomator2 的 Go 语言实现,通过 ADB 协议与运行在 Android 设备上的
// UIAutomator2 HTTP 服务通信提供设备控制、UI 操作、文本输入等功能。
//
// 核心组件:
// - Device设备客户端管理 UIAutomator2 服务生命周期,提供所有设备操作
// - UiObjectUI 控件对象,支持点击、输入、滑动、等待等操作
// - SelectorUI 元素选择器,支持文本、类名、资源 ID 等多种查询条件
// - JsonRpcWrapperJSON-RPC 2.0 调用封装
// - InputMethod通过 AdbKeyboard 输入法实现快速文本输入
// - SwipeExt扩展滑动操作按方向、比例滑动
// - WatchContext/Watcher弹窗/对话框自动监控和处理
// - Session应用会话管理自动检测应用状态
// - Settings设备配置管理等待超时、操作延迟等
//
// 通信层:
// - AdbHTTPConnection通过 ADB 隧道发送 HTTP 请求
// - HttpRequest高层 HTTP 请求封装
// - JsonRpcCallJSON-RPC 2.0 请求/响应处理
//
// 使用示例:
//
// // 创建 UI 选择器
// selector := libs.MustNew(map[string]interface{}{
// "text": "登录",
// "className": "android.widget.Button",
// })
// jsonData, _ := selector.ToJSON()
// // 创建设备连接
// device, err := libs.NewDevice("emulator-5554")
// // 自定义 ADB 地址: libs.NewDevice("emulator-5554", "192.168.1.100:5037")
// if err != nil {
// log.Fatal(err)
// }
// defer device.StopUiautomator()
//
// // 查找并点击按钮
// btn, _ := device.FindElement(map[string]interface{}{"text": "登录"})
// btn.Click()
//
// // 输入文本
// input, _ := device.FindElement(map[string]interface{}{"resourceId": "com.example:id/username"})
// input.SetText("admin")
//
// // 使用 Watcher 自动处理弹窗
// watcher := libs.NewWatcher(device)
// watcher.WhenText("同意").Click()
// watcher.Start(2.0)
// defer watcher.Stop()
package libs

160
libs/errors.go Normal file
View File

@@ -0,0 +1,160 @@
package libs
import "fmt"
// ---------- 基础错误类型 ----------
// DeviceError 设备层面的错误基类
type DeviceError struct {
Message string
}
func (e *DeviceError) Error() string {
return fmt.Sprintf("设备错误: %s", e.Message)
}
// ConnectError 设备连接失败
type ConnectError struct {
Message string
}
func (e *ConnectError) Error() string {
return fmt.Sprintf("连接失败: %s", e.Message)
}
// HTTPError HTTP 请求失败
type HTTPError struct {
Message string
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("HTTP 错误: %s", e.Message)
}
// HTTPTimeoutError HTTP 请求超时
type HTTPTimeoutError struct {
Message string
}
func (e *HTTPTimeoutError) Error() string {
return fmt.Sprintf("HTTP 超时: %s", e.Message)
}
// AdbShellError ADB Shell 执行失败
type AdbShellError struct {
Message string
}
func (e *AdbShellError) Error() string {
return fmt.Sprintf("ADB Shell 错误: %s", e.Message)
}
// ---------- RPC 错误类型 ----------
// RPCError JSON-RPC 调用的错误基类
type RPCError struct {
Code int
Message string
Data string // 堆栈信息
Params interface{}
}
func (e *RPCError) Error() string {
return fmt.Sprintf("RPC 错误 [%d]: %s", e.Code, e.Message)
}
// RPCUnknownError 未知的 RPC 错误
type RPCUnknownError struct {
RPCError
}
// RPCInvalidError 无效的 RPC 响应
type RPCInvalidError struct {
Message string
}
func (e *RPCInvalidError) Error() string {
return fmt.Sprintf("RPC 无效响应: %s", e.Message)
}
// RPCStackOverflowError Java 端栈溢出错误
type RPCStackOverflowError struct {
RPCError
}
// UiObjectNotFoundError UI 元素未找到
type UiObjectNotFoundError struct {
Code int
Message string
Params interface{}
}
func (e *UiObjectNotFoundError) Error() string {
return fmt.Sprintf("UiObject 未找到: %s (参数: %v)", e.Message, e.Params)
}
// UiAutomationNotConnectedError UIAutomation 服务未连接
type UiAutomationNotConnectedError struct {
Message string
}
func (e *UiAutomationNotConnectedError) Error() string {
return fmt.Sprintf("UIAutomation 未连接: %s", e.Message)
}
// HierarchyEmptyError dump_hierarchy 返回空结果
type HierarchyEmptyError struct {
Message string
}
func (e *HierarchyEmptyError) Error() string {
return fmt.Sprintf("层级为空: %s", e.Message)
}
// ---------- 应用相关错误 ----------
// LaunchUiAutomationError UIAutomator2 服务启动失败
type LaunchUiAutomationError struct {
Message string
Output string
}
func (e *LaunchUiAutomationError) Error() string {
return fmt.Sprintf("UIAutomator 启动失败: %s\n输出: %s", e.Message, e.Output)
}
// AccessibilityServiceAlreadyRegisteredError 辅助功能服务已注册
type AccessibilityServiceAlreadyRegisteredError struct {
Output string
}
func (e *AccessibilityServiceAlreadyRegisteredError) Error() string {
return fmt.Sprintf("辅助功能服务已注册: %s", e.Output)
}
// SessionBrokenError 应用会话中断(应用已退出或崩溃)
type SessionBrokenError struct {
Message string
}
func (e *SessionBrokenError) Error() string {
return fmt.Sprintf("会话中断: %s", e.Message)
}
// AppNotFoundError 应用未安装
type AppNotFoundError struct {
PackageName string
}
func (e *AppNotFoundError) Error() string {
return fmt.Sprintf("应用未找到: %s", e.PackageName)
}
// InputIMEError 输入法错误
type InputIMEError struct {
Message string
}
func (e *InputIMEError) Error() string {
return fmt.Sprintf("输入法错误: %s", e.Message)
}

220
libs/input.go Normal file
View File

@@ -0,0 +1,220 @@
package libs
import (
"encoding/base64"
"fmt"
"regexp"
"strings"
"time"
)
// ---------- 输入法功能 ----------
// 输入法相关常量
const (
// imeID AdbKeyboard 输入法的标识符
imeID = "com.github.uiautomator/.AdbKeyboard"
// broadcastResultOK 广播成功返回码
broadcastResultOK = -1
)
// BroadcastResult 广播命令的返回结果
type BroadcastResult struct {
Code int // 结果码,-1 表示成功
Data string // 返回数据
}
// InputMethod 提供输入法相关操作
// 通过 AdbKeyboard 输入法实现快速文本输入
type InputMethod struct {
device *Device
}
// NewInputMethod 创建输入法操作实例
func NewInputMethod(device *Device) *InputMethod {
return &InputMethod{device: device}
}
// CurrentIME 获取当前活动的输入法
func (im *InputMethod) CurrentIME() (string, error) {
resp, err := im.device.Shell("settings", "get", "secure", "default_input_method")
if err != nil {
return "", err
}
return strings.TrimSpace(resp.Output), nil
}
// SetInputIME 启用或禁用 AdbKeyboard 输入法
func (im *InputMethod) SetInputIME(enable bool) error {
if !enable {
_, err := im.device.Shell("ime", "disable", imeID)
return err
}
// 检查是否已经设置为当前输入法
current, err := im.CurrentIME()
if err == nil && current == imeID {
return nil
}
// 检查是否已安装
if !im.IsInstalled() {
return &InputIMEError{Message: "AdbKeyboard 输入法未安装,请先安装 app-uiautomator.apk"}
}
// 启用并设置为默认输入法
im.device.Shell("ime", "enable", imeID)
im.device.Shell("ime", "set", imeID)
im.device.Shell("settings", "put", "secure", "default_input_method", imeID)
// 等待输入法就绪
return im.waitReady()
}
// IsInstalled 检查 AdbKeyboard 输入法是否已安装
func (im *InputMethod) IsInstalled() bool {
list, _ := im.getIMEList()
for _, id := range list {
if id == imeID {
return true
}
}
return false
}
// getIMEList 获取设备上所有输入法列表
func (im *InputMethod) getIMEList() ([]string, error) {
resp, err := im.device.Shell("ime", "list", "-s")
if err != nil {
return nil, err
}
lines := strings.Split(strings.TrimSpace(resp.Output), "\n")
var result []string
for _, line := range lines {
line = strings.TrimSpace(line)
if line != "" {
result = append(result, line)
}
}
return result, nil
}
// waitReady 等待输入法就绪
func (im *InputMethod) waitReady() error {
for i := 0; i < 10; i++ {
current, err := im.CurrentIME()
if err == nil && current == imeID {
return nil
}
time.Sleep(300 * time.Millisecond)
}
return &InputIMEError{Message: "等待输入法就绪超时"}
}
// broadcast 发送广播命令
func (im *InputMethod) broadcast(action string, extras map[string]string) (*BroadcastResult, error) {
args := []string{"am", "broadcast", "-a", action}
for k, v := range extras {
args = append(args, "--es", k, v)
}
resp, err := im.device.Shell(args...)
if err != nil {
return nil, err
}
// 解析返回结果
// 格式: result=-1 data="success"
result := &BroadcastResult{Code: 0}
reResult := regexp.MustCompile(`result=(-?\d+)`)
reData := regexp.MustCompile(`data="([^"]+)"`)
if m := reResult.FindStringSubmatch(resp.Output); len(m) > 1 {
fmt.Sscanf(m[1], "%d", &result.Code)
}
if m := reData.FindStringSubmatch(resp.Output); len(m) > 1 {
result.Data = m[1]
}
return result, nil
}
// mustBroadcast 发送广播并确保成功
func (im *InputMethod) mustBroadcast(action string, extras map[string]string) error {
var lastErr error
for i := 0; i < 3; i++ {
result, err := im.broadcast(action, extras)
if err != nil {
lastErr = err
time.Sleep(time.Duration(1000+i*500) * time.Millisecond)
continue
}
if result.Code == broadcastResultOK {
return nil
}
lastErr = fmt.Errorf("广播 %s 失败: code=%d data=%s", action, result.Code, result.Data)
time.Sleep(time.Duration(1000+i*500) * time.Millisecond)
}
return lastErr
}
// SendKeys 通过 AdbKeyboard 输入法输入文本
// 自动启用输入法并在输入完成后隐藏键盘
func (im *InputMethod) SendKeys(text string) error {
if err := im.SetInputIME(true); err != nil {
return err
}
// Base64 编码文本
encoded := base64.StdEncoding.EncodeToString([]byte(text))
// 发送文本输入广播
if err := im.mustBroadcast("ADB_KEYBOARD_INPUT_TEXT", map[string]string{
"text": encoded,
}); err != nil {
return err
}
// 隐藏键盘
im.mustBroadcast("ADB_KEYBOARD_HIDE", nil)
return nil
}
// SendAction 模拟输入法编辑器动作
// code 为动作代码:
//
// "go"/"search"/"send"/"next"/"done"/"previous" 或数字
func (im *InputMethod) SendAction(code string) error {
if err := im.SetInputIME(true); err != nil {
return err
}
// 将名称转换为代码
actionCodes := map[string]string{
"go": "2",
"search": "3",
"send": "4",
"next": "5",
"done": "6",
"previous": "7",
}
codeStr := code
if mapped, ok := actionCodes[strings.ToLower(code)]; ok {
codeStr = mapped
}
return im.mustBroadcast("ADB_KEYBOARD_EDITOR_CODE", map[string]string{
"code": codeStr,
})
}
// ClearText 通过输入法清除文本
func (im *InputMethod) ClearText() error {
if err := im.SetInputIME(true); err != nil {
return err
}
return im.mustBroadcast("ADB_KEYBOARD_CLEAR_TEXT", nil)
}

175
libs/jsonrpc.go Normal file
View File

@@ -0,0 +1,175 @@
package libs
import (
"context"
"encoding/json"
"fmt"
"log"
"strings"
)
// ---------- JSON-RPC 请求/响应结构 ----------
// JsonRpcRequest JSON-RPC 2.0 请求
type JsonRpcRequest struct {
JsonRpc string `json:"jsonrpc"`
ID int `json:"id"`
Method string `json:"method"`
Params interface{} `json:"params"`
}
// JsonRpcResponse JSON-RPC 2.0 响应
type JsonRpcResponse struct {
JsonRpc string `json:"jsonrpc"`
ID int `json:"id"`
Result *json.RawMessage `json:"result,omitempty"`
Error *JsonRpcError `json:"error,omitempty"`
}
// JsonRpcError JSON-RPC 错误对象
type JsonRpcError struct {
Code int `json:"code"`
Message string `json:"message"`
Data string `json:"data,omitempty"` // Java 堆栈信息
}
// ---------- JSON-RPC 调用函数 ----------
// JsonRpcCall 向 UIAutomator2 服务发送 JSON-RPC 调用
// dev 实现 AdbDevice 接口devicePort 为设备端服务端口
// method 为 RPC 方法名params 为参数
// timeout 为请求超时debug 为 true 时输出调试信息
//
// 返回值为 JSON 原始字节(由调用者根据需要解析)
//
// 可能返回的错误类型:
// - *UiObjectNotFoundError: UI 元素未找到
// - *UiAutomationNotConnectedError: UIAutomation 服务断开
// - *RPCStackOverflowError: Java 端栈溢出
// - *RPCUnknownError: 未知 RPC 错误
// - *RPCInvalidError: 无效的 RPC 响应
func JsonRpcCall(ctx context.Context, dev AdbDevice, devicePort int, method string, params interface{}, timeout float64, debug bool) (json.RawMessage, error) {
// 构造 JSON-RPC 请求体
payload := map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"method": method,
"params": params,
}
// 通过 HTTP 发送到 /jsonrpc/0 端点
resp, err := HttpRequest(ctx, dev, devicePort, "POST", "/jsonrpc/0", payload, timeout, debug)
if err != nil {
return nil, err
}
// 解析响应
var rpcResp JsonRpcResponse
if err := json.Unmarshal(resp.Content, &rpcResp); err != nil {
return nil, &RPCInvalidError{Message: fmt.Sprintf("JSON 解析失败: %v", err)}
}
// 处理 RPC 错误
if rpcResp.Error != nil {
return nil, handleRpcError(rpcResp.Error, resp.Text(), params)
}
// 确保有结果字段
if rpcResp.Result == nil {
return nil, &RPCInvalidError{Message: "响应中缺少 result 字段"}
}
return *rpcResp.Result, nil
}
// handleRpcError 根据 JSON-RPC 错误内容映射到具体的 Go 错误类型
func handleRpcError(rpcErr *JsonRpcError, rawText string, params interface{}) error {
code := rpcErr.Code
message := rpcErr.Message
data := rpcErr.Data
if debug := false; debug {
log.Printf("JSON-RPC 错误: code=%d message=%s", code, message)
}
// UIAutomation 未连接
if strings.Contains(rawText, "UiAutomation not connected") {
return &UiAutomationNotConnectedError{Message: "UiAutomation not connected"}
}
if strings.Contains(message, "android.os.DeadObjectException") {
return &UiAutomationNotConnectedError{Message: "android.os.DeadObjectException"}
}
if strings.Contains(message, "android.os.DeadSystemRuntimeException") {
return &UiAutomationNotConnectedError{Message: "android.os.DeadSystemRuntimeException"}
}
// UI 元素未找到
if strings.Contains(message, "uiautomator.UiObjectNotFoundException") {
return &UiObjectNotFoundError{
Code: code,
Message: message,
Params: params,
}
}
// 栈溢出
if strings.Contains(message, "java.lang.StackOverflowError") {
truncated := data
if len(data) > 2000 {
truncated = data[:1000] + "..." + data[len(data)-1000:]
}
return &RPCStackOverflowError{
RPCError: RPCError{
Code: code,
Message: fmt.Sprintf("StackOverflowError: %s", message),
Data: truncated,
Params: params,
},
}
}
// 未知 RPC 错误
return &RPCUnknownError{
RPCError: RPCError{
Code: code,
Message: fmt.Sprintf("未知 RPC 错误: %d %s", code, message),
Data: data,
Params: params,
},
}
}
// ---------- JSON-RPC 动态调用包装器 ----------
// JsonRpcWrapper 提供动态方法名的 JSON-RPC 调用
// 通过记录方法名并在 Call 时发送请求,实现类似 Python 的动态属性访问
type JsonRpcWrapper struct {
// caller 为实际发送 JSON-RPC 请求的函数
caller func(method string, params interface{}, timeout float64) (json.RawMessage, error)
}
// NewJsonRpcWrapper 创建 JSON-RPC 动态调用包装器
func NewJsonRpcWrapper(caller func(method string, params interface{}, timeout float64) (json.RawMessage, error)) *JsonRpcWrapper {
return &JsonRpcWrapper{caller: caller}
}
// Call 发送 JSON-RPC 调用
// method 为 RPC 方法名(如 "click"、"setText"
// params 为方法参数,通常为 []interface{} 或 map[string]interface{}
// timeout 为超时时间0 使用默认值
func (w *JsonRpcWrapper) Call(method string, params interface{}, timeout ...float64) (json.RawMessage, error) {
t := HTTPTimeout
if len(timeout) > 0 && timeout[0] > 0 {
t = timeout[0]
}
return w.caller(method, params, t)
}
// CallResult 发送 JSON-RPC 调用并将结果解析到指定结构体
func (w *JsonRpcWrapper) CallResult(result interface{}, method string, params interface{}, timeout ...float64) error {
raw, err := w.Call(method, params, timeout...)
if err != nil {
return err
}
return json.Unmarshal(raw, result)
}

65
libs/proto.go Normal file
View File

@@ -0,0 +1,65 @@
package libs
// ---------- 协议常量 ----------
// ScrollSteps 默认滚动步数
// 每步约 5ms55 步约 275ms
const ScrollSteps = 55
// HTTPTimeout 默认 HTTP 请求超时时间(秒)
const HTTPTimeout = 300.0
// DeviceServerPort UIAutomator2 服务默认端口
const DeviceServerPort = 9008
// DefaultADBAddr 默认 ADB 服务器地址
const DefaultADBAddr = "127.0.0.1:5037"
// ---------- 方向枚举 ----------
// Direction 表示滑动/滚动方向
type Direction string
const (
// DirectionLeft 向左
DirectionLeft Direction = "left"
// DirectionRight 向右
DirectionRight Direction = "right"
// DirectionUp 向上
DirectionUp Direction = "up"
// DirectionDown 向下
DirectionDown Direction = "down"
// DirectionForward 向前(等同于向下)
DirectionForward Direction = "forward"
// DirectionBackward 向后(等同于向上)
DirectionBackward Direction = "backward"
)
// ---------- 触摸事件常量 ----------
const (
// ActionDown 手指按下事件
ActionDown = 0
// ActionUp 手指抬起事件
ActionUp = 1
// ActionMove 手指移动事件
ActionMove = 2
)
// ---------- 设备方向映射 ----------
// OrientationInfo 设备方向信息
type OrientationInfo struct {
Value int // displayRotation 值
Name string // 方向名称
Short string // 简写
Rotation int // 旋转角度
}
// Orientations 所有设备方向定义
var Orientations = []OrientationInfo{
{Value: 0, Name: "natural", Short: "n", Rotation: 0},
{Value: 1, Name: "left", Short: "l", Rotation: 90},
{Value: 2, Name: "upsidedown", Short: "u", Rotation: 180},
{Value: 3, Name: "right", Short: "r", Rotation: 270},
}

View File

@@ -1,7 +1,6 @@
package libs
import (
"bufio"
"bytes"
"context"
"encoding/json"
@@ -12,17 +11,32 @@ import (
"net/http"
"strings"
"time"
"github.com/zhuy1228/go-mobile-uiautomator/adb"
)
// ---------- 外部接口和类型定义 ----------
// AdbDevice 定义了通过 ADB 隧道创建设备连接接口
// AdbDevice 定义了设备连接接口
type AdbDevice interface {
// CreateConnection 建立到设备的 TCP 连接
// network 通常为 "tcp"port 为设备上服务监听端口
CreateConnection(network string, port int) (net.Conn, error)
}
// AdbTunnelDevice 通过 ADB 隧道直连设备(与 Python uiautomator2 完全一致)
// 每次 CreateConnection 会建立一条新的 ADB 隧道,无需 adb forward无需端口管理
// 等同于 Python 中 AdbHTTPConnection 继承 HTTPConnection 并重写 connect() 的方案
type AdbTunnelDevice struct {
AdbAddr string // ADB 服务器地址
Serial string // 设备序列号
}
// CreateConnection 建立到设备指定端口的 ADB 隧道连接
func (d *AdbTunnelDevice) CreateConnection(network string, port int) (net.Conn, error) {
return adb.CreateTunnel(d.AdbAddr, d.Serial, port)
}
// HTTPResponse 封装 HTTP 响应数据
type HTTPResponse struct {
Content []byte // 响应体内容
@@ -49,126 +63,25 @@ var (
ErrHTTPFailed = errors.New("HTTP 请求失败")
)
// ---------- AdbHTTPConnection通过 ADB 隧道发送 HTTP 请求 ----------
// AdbHTTPConnection 基于 net.Conn 实现的 HTTP 连接
// 通过 ADB 端口转发直接与设备端 UIAutomator2 服务通信
type AdbHTTPConnection struct {
Conn net.Conn
}
// NewAdbHTTPConnection 创建一个新的 ADB HTTP 连接
// dev 为设备接口port 为设备端服务端口timeout 为连接超时
func NewAdbHTTPConnection(dev AdbDevice, port int, timeout time.Duration) (*AdbHTTPConnection, error) {
conn, err := dev.CreateConnection("tcp", port)
if err != nil {
return nil, fmt.Errorf("无法连接到 UIAutomator2 服务: %w", err)
}
_ = conn.SetDeadline(time.Now().Add(timeout))
return &AdbHTTPConnection{Conn: conn}, nil
}
// Close 关闭底层连接
func (c *AdbHTTPConnection) Close() error {
if c.Conn != nil {
return c.Conn.Close()
}
return nil
}
// sendRequest 将 HTTP 请求写入连接并读取响应
// 通过原始 TCP 连接发送 HTTP 报文,避免依赖标准 http.Client
func (c *AdbHTTPConnection) sendRequest(req *http.Request, timeout time.Duration) (*http.Response, error) {
// 设置读写截止时间
if timeout > 0 {
_ = c.Conn.SetDeadline(time.Now().Add(timeout))
} else {
_ = c.Conn.SetDeadline(time.Time{})
}
// 序列化 HTTP 请求为原始报文
var buf bytes.Buffer
// 请求行METHOD PATH HTTP/1.1
path := req.URL.RequestURI()
if path == "" {
path = "/"
}
fmt.Fprintf(&buf, "%s %s HTTP/1.1\r\n", req.Method, path)
fmt.Fprintf(&buf, "Host: localhost\r\n")
// 设置默认请求头
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "uiautomator2")
}
if req.Header.Get("Accept-Encoding") == "" {
req.Header.Set("Accept-Encoding", "")
}
if req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "application/json")
}
// 写入请求头
for k, vals := range req.Header {
for _, v := range vals {
fmt.Fprintf(&buf, "%s: %s\r\n", k, v)
}
}
// 处理请求体
var bodyBytes []byte
if req.Body != nil {
var err error
bodyBytes, err = io.ReadAll(req.Body)
if err != nil {
return nil, fmt.Errorf("读取请求体失败: %w", err)
}
fmt.Fprintf(&buf, "Content-Length: %d\r\n", len(bodyBytes))
} else {
fmt.Fprintf(&buf, "Content-Length: 0\r\n")
}
// 请求头与请求体之间的空行
buf.WriteString("\r\n")
// 发送请求头
if _, err := c.Conn.Write(buf.Bytes()); err != nil {
return nil, fmt.Errorf("发送请求头失败: %w", err)
}
// 发送请求体
if len(bodyBytes) > 0 {
if _, err := c.Conn.Write(bodyBytes); err != nil {
return nil, fmt.Errorf("发送请求体失败: %w", err)
}
}
// 使用标准库解析 HTTP 响应
reader := bufio.NewReader(c.Conn)
resp, err := http.ReadResponse(reader, req)
if err != nil {
return nil, fmt.Errorf("读取 HTTP 响应失败: %w", err)
}
return resp, nil
}
// ---------- HttpRequest高层 HTTP 请求封装 ----------
// ---------- HttpRequest通过标准 HTTP 客户端发送请求 ----------
// HttpRequest 向设备端 UIAutomator2 服务发送 HTTP 请求
// ctx 为上下文控制dev 为设备接口devicePort 为设备端服务端口
// method 为 HTTP 方法path 为请求路径
// data 为请求体数据(会被 JSON 编码timeoutSecs 为超时秒数
// printRequest 为 true 时输出 curl 风格的调试信息
// 使用 Go 标准 http.Client + 自定义 ADB 隧道 Dialer与 Python uiautomator2 完全一致
//
// 工作原理(与 Python 的 AdbHTTPConnection 等价):
// 1. http.Transport 的 DialContext 会为每个请求建立一条新的 ADB 隧道
// 2. 标准 http.Client 在这条隧道上发送完整的 HTTP/1.1 请求
// 3. 请求完成后隧道自动关闭,无需额外清理
func HttpRequest(ctx context.Context, dev AdbDevice, devicePort int, method, path string, data map[string]interface{}, timeoutSecs float64, printRequest bool) (*HTTPResponse, error) {
// 默认超时 10 秒
if timeoutSecs <= 0 {
timeoutSecs = 10.0
}
timeout := time.Duration(timeoutSecs * float64(time.Second))
// 调试模式:打印 curl 风格的请求信息
if printRequest {
now := time.Now().Format("15:04:05.000")
url := fmt.Sprintf("http://127.0.0.1:%d%s", devicePort, path)
url := fmt.Sprintf("http://<adb-tunnel>:%d%s", devicePort, path)
if data != nil {
b, _ := json.Marshal(data)
fmt.Printf("# HTTP 超时=%.3f\n%s $ curl -X %s %s -d '%s'\n", timeoutSecs, now, method, url, string(b))
@@ -177,7 +90,9 @@ func HttpRequest(ctx context.Context, dev AdbDevice, devicePort int, method, pat
}
}
// 构造 HTTP 请求
// 构造 HTTP 请求
// URL 中的 host:port 会被自定义 DialContext 忽略,实际连接通过 ADB 隧道
url := fmt.Sprintf("http://127.0.0.1:%d%s", devicePort, path)
var body io.Reader
if data != nil {
b, err := json.Marshal(data)
@@ -186,23 +101,27 @@ func HttpRequest(ctx context.Context, dev AdbDevice, devicePort int, method, pat
}
body = bytes.NewReader(b)
}
req, err := http.NewRequestWithContext(ctx, method, "http://localhost"+path, body)
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return nil, fmt.Errorf("创建 HTTP 请求失败: %w", err)
}
req.Header.Set("User-Agent", "uiautomator2")
req.Header.Set("Accept-Encoding", "")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Content-Type", "application/json; charset=utf-8")
// 建立到设备的连接
connWrapper, err := NewAdbHTTPConnection(dev, devicePort, timeout)
if err != nil {
return nil, err
// 核心:自定义 Transport用 ADB 隧道替代普通 TCP 连接
// 这与 Python 中 AdbHTTPConnection.connect() 重写 self.sock 的做法完全等价
transport := &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dev.CreateConnection("tcp", devicePort)
},
DisableKeepAlives: true, // 每次请求独立隧道,与 Python 行为一致
}
client := &http.Client{
Transport: transport,
Timeout: time.Duration(timeoutSecs * float64(time.Second)),
}
defer connWrapper.Close()
// 发送请求并读取响应
resp, err := connWrapper.sendRequest(req, timeout)
resp, err := client.Do(req)
if err != nil {
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {

View File

@@ -4,7 +4,6 @@ import (
"encoding/json"
"errors"
"fmt"
"strconv"
)
// FieldMeta 定义选择器字段的掩码位和默认值
@@ -198,20 +197,20 @@ func (s *Selector) Clone() *Selector {
}
// ToMap 将选择器序列化为 map便于 JSON 编码或 RPC 调用
// 始终包含 childOrSibling 和 childOrSiblingSelector 字段(即使为空),
// 与 Python 版本保持一致,确保 UIAutomator2 服务端能正确解析
func (s *Selector) ToMap() map[string]interface{} {
out := make(map[string]interface{}, len(s.fields)+3)
for k, v := range s.fields {
out[k] = v
}
out["mask"] = s.mask
if len(s.childOrSibling) > 0 {
out["childOrSibling"] = append([]string{}, s.childOrSibling...)
cs := make([]map[string]interface{}, 0, len(s.childOrSiblingSelector))
for _, c := range s.childOrSiblingSelector {
cs = append(cs, c.ToMap())
}
out["childOrSiblingSelector"] = cs
out["childOrSibling"] = append([]string{}, s.childOrSibling...)
cs := make([]map[string]interface{}, 0, len(s.childOrSiblingSelector))
for _, c := range s.childOrSiblingSelector {
cs = append(cs, c.ToMap())
}
out["childOrSiblingSelector"] = cs
return out
}
@@ -305,47 +304,3 @@ func (s *Selector) String() string {
b, _ := json.Marshal(m)
return "Selector " + string(b)
}
// Example 使用示例(仅供参考,非单元测试)
func Example() {
// 初始化根选择器
root := MustNew(map[string]interface{}{
"className": "android.widget.LinearLayout",
})
// 添加子元素选择器
root.Child(map[string]interface{}{
"text": "下一步",
"instance": 0,
})
// 更新最后一个子选择器的 instance
_ = root.UpdateInstance(2)
// 深拷贝
cpy := root.Clone()
// 序列化为 JSON
j, _ := cpy.ToJSON()
fmt.Println(string(j))
}
// SimpleTests 简单测试函数(建议迁移到 _test.go 文件中使用 testing 包)
func SimpleTests() {
// 设置与删除字段
s := MustNew(map[string]interface{}{"text": "hello"})
fmt.Println("设置后掩码:", strconv.FormatUint(uint64(s.Mask()), 10))
_ = s.Delete("text")
fmt.Println("删除后掩码:", strconv.FormatUint(uint64(s.Mask()), 10))
// 类型校验bool 字段传入非 bool 值应报错
_, err := New(map[string]interface{}{"checkable": "yes"})
fmt.Println("非法 bool 值报错:", err != nil)
// 深拷贝独立性验证
s2 := MustNew(map[string]interface{}{"text": "a"})
s2.Child(map[string]interface{}{"text": "b", "instance": 1})
c := s2.Clone()
c.childOrSibling[0] = "sibling"
fmt.Println("原始 childOrSibling:", s2.childOrSibling[0], "克隆 childOrSibling:", c.childOrSibling[0])
}

86
libs/session.go Normal file
View File

@@ -0,0 +1,86 @@
package libs
import (
"encoding/json"
"fmt"
)
// Session 在 Device 基础上增加应用会话监控
// 每次 JSON-RPC 调用前检查目标应用是否仍在运行
// 如果应用退出或崩溃,会返回 SessionBrokenError
//
// 对应 Python 版本的 Session 类
type Session struct {
*Device
packageName string // 监控的应用包名
pid int // 应用启动时的 PID
}
// NewSession 创建一个新的应用会话
// 启动应用并记录其 PID
func NewSession(device *Device, packageName string, attach bool) (*Session, error) {
// 如果不是 attach 模式,先停止再启动应用
if !attach {
device.AppStop(packageName)
}
device.AppStart(packageName, "", false)
// 等待应用启动并获取 PID
pid, err := device.AppWait(packageName, 20.0, false)
if err != nil {
return nil, err
}
if pid == 0 {
return nil, &DeviceError{Message: fmt.Sprintf("应用 %s 启动失败", packageName)}
}
return &Session{
Device: device,
packageName: packageName,
pid: pid,
}, nil
}
// PackageName 返回会话监控的应用包名
func (s *Session) PackageName() string {
return s.packageName
}
// PID 返回应用的进程 ID
func (s *Session) PID() int {
return s.pid
}
// Running 检查应用是否仍在运行
func (s *Session) Running() bool {
currentPid := s.pidOfApp(s.packageName)
return currentPid == s.pid && s.pid > 0
}
// jsonrpcCall 重写 Device 的 jsonrpcCall增加会话状态检查
func (s *Session) jsonrpcCall(method string, params interface{}, timeout float64) (json.RawMessage, error) {
if !s.Running() {
return nil, &SessionBrokenError{
Message: fmt.Sprintf("应用 %s (PID: %d) 已退出", s.packageName, s.pid),
}
}
return s.Device.jsonrpcCall(method, params, timeout)
}
// Restart 重启应用
func (s *Session) Restart() error {
s.Device.AppStop(s.packageName)
s.Device.AppStart(s.packageName, "", false)
pid, err := s.Device.AppWait(s.packageName, 20.0, false)
if err != nil {
return err
}
s.pid = pid
return nil
}
// Close 关闭会话(停止应用)
func (s *Session) Close() {
s.Device.AppStop(s.packageName)
s.pid = 0
}

139
libs/settings.go Normal file
View File

@@ -0,0 +1,139 @@
package libs
import (
"fmt"
"sync"
)
// Settings 管理设备的各项配置参数
// 支持类型安全的读写操作
type Settings struct {
mu sync.RWMutex
data map[string]interface{}
}
// 默认配置值
var defaultSettings = map[string]interface{}{
// 等待元素出现的超时时间(秒)
"wait_timeout": 20.0,
// 操作前后的延迟 [前延迟, 后延迟](秒)
"operation_delay": [2]float64{0, 0},
// 需要应用操作延迟的方法列表
"operation_delay_methods": []string{"click", "swipe", "drag", "press"},
// dump_hierarchy 的最大深度
"max_depth": 50,
}
// NewSettings 创建一个新的 Settings 实例,使用默认配置
func NewSettings() *Settings {
s := &Settings{
data: make(map[string]interface{}),
}
// 复制默认配置
for k, v := range defaultSettings {
s.data[k] = v
}
return s
}
// Get 获取配置项的值
// 如果配置项不存在,返回 nil
func (s *Settings) Get(key string) interface{} {
s.mu.RLock()
defer s.mu.RUnlock()
return s.data[key]
}
// GetFloat64 获取 float64 类型的配置值
func (s *Settings) GetFloat64(key string) float64 {
v := s.Get(key)
if v == nil {
return 0
}
switch val := v.(type) {
case float64:
return val
case float32:
return float64(val)
case int:
return float64(val)
default:
return 0
}
}
// GetInt 获取 int 类型的配置值
func (s *Settings) GetInt(key string) int {
v := s.Get(key)
if v == nil {
return 0
}
switch val := v.(type) {
case int:
return val
case float64:
return int(val)
default:
return 0
}
}
// GetStringSlice 获取 []string 类型的配置值
func (s *Settings) GetStringSlice(key string) []string {
v := s.Get(key)
if v == nil {
return nil
}
if val, ok := v.([]string); ok {
return val
}
return nil
}
// GetOperationDelay 获取操作延迟配置 [前延迟, 后延迟]
func (s *Settings) GetOperationDelay() (float64, float64) {
v := s.Get("operation_delay")
if v == nil {
return 0, 0
}
if val, ok := v.([2]float64); ok {
return val[0], val[1]
}
return 0, 0
}
// Set 设置配置项的值,包含类型校验
func (s *Settings) Set(key string, value interface{}) error {
s.mu.Lock()
defer s.mu.Unlock()
// 类型校验
if existing, ok := defaultSettings[key]; ok {
if err := validateSettingType(key, existing, value); err != nil {
return err
}
}
s.data[key] = value
return nil
}
// validateSettingType 校验设置值的类型是否与默认值匹配
func validateSettingType(key string, defaultVal, newVal interface{}) error {
switch defaultVal.(type) {
case float64:
switch newVal.(type) {
case float64, float32, int, int64:
return nil
default:
return fmt.Errorf("配置 %s 必须是数值类型", key)
}
case int:
switch newVal.(type) {
case int, int64, float64:
return nil
default:
return fmt.Errorf("配置 %s 必须是整数类型", key)
}
}
return nil
}

86
libs/swipe_ext.go Normal file
View File

@@ -0,0 +1,86 @@
package libs
import "fmt"
// SwipeExt 提供扩展的滑动操作
// 支持按方向、比例、区域进行滑动
// 对应 Python 版本的 SwipeExt 类
type SwipeExt struct {
device *Device
}
// NewSwipeExt 创建扩展滑动操作实例
func NewSwipeExt(device *Device) *SwipeExt {
return &SwipeExt{device: device}
}
// SwipeDirection 按方向滑动
// direction: 滑动方向DirectionLeft/Right/Up/Down
// scale: 滑动比例0-1.0),默认 0.9
// box: 滑动区域 [left, top, right, bottom]nil 表示全屏
// steps: 滑动步数
func (s *SwipeExt) SwipeDirection(direction Direction, scale float64, box *[4]int, steps int) error {
if scale <= 0 || scale > 1.0 {
scale = 0.9
}
if steps <= 0 {
steps = ScrollSteps
}
var lx, ly, rx, ry int
if box != nil {
lx, ly, rx, ry = box[0], box[1], box[2], box[3]
} else {
w, h, err := s.device.WindowSize()
if err != nil {
return err
}
lx, ly = 0, 0
rx, ry = w, h
}
width := rx - lx
height := ry - ly
hOffset := int(float64(width) * (1 - scale) / 2)
vOffset := int(float64(height) * (1 - scale) / 2)
center := [2]int{lx + width/2, ly + height/2}
left := [2]int{lx + hOffset, ly + height/2}
up := [2]int{lx + width/2, ly + vOffset}
right := [2]int{rx - hOffset, ly + height/2}
bottom := [2]int{lx + width/2, ry - vOffset}
switch direction {
case DirectionLeft:
return s.device.Swipe(right[0], right[1], left[0], left[1], steps)
case DirectionRight:
return s.device.Swipe(left[0], left[1], right[0], right[1], steps)
case DirectionUp:
return s.device.Swipe(center[0], center[1], up[0], up[1], steps)
case DirectionDown:
return s.device.Swipe(center[0], center[1], bottom[0], bottom[1], steps)
default:
return fmt.Errorf("不支持的方向: %s", string(direction))
}
}
// Left 向左滑动
func (s *SwipeExt) Left(scale float64, steps int) error {
return s.SwipeDirection(DirectionLeft, scale, nil, steps)
}
// Right 向右滑动
func (s *SwipeExt) Right(scale float64, steps int) error {
return s.SwipeDirection(DirectionRight, scale, nil, steps)
}
// Up 向上滑动
func (s *SwipeExt) Up(scale float64, steps int) error {
return s.SwipeDirection(DirectionUp, scale, nil, steps)
}
// Down 向下滑动
func (s *SwipeExt) Down(scale float64, steps int) error {
return s.SwipeDirection(DirectionDown, scale, nil, steps)
}

664
libs/uiobject.go Normal file
View File

@@ -0,0 +1,664 @@
package libs
import (
"encoding/json"
"fmt"
"time"
)
// UiObject 表示一个 Android UI 控件对象
// 通过 Selector 定位,支持点击、输入、滑动等操作
// 对应 Python 版本的 UiObject 类
type UiObject struct {
device *Device
selector *Selector
jsonrpc *JsonRpcWrapper
}
// NewUiObject 创建一个新的 UiObject
func NewUiObject(device *Device, selector *Selector) *UiObject {
return &UiObject{
device: device,
selector: selector,
jsonrpc: device.JsonRpc(),
}
}
// Selector 返回当前 UiObject 的选择器
func (u *UiObject) Selector() *Selector {
return u.selector
}
// ---------- 等待和存在性 ----------
// Exists 检查 UI 元素是否存在于当前窗口
func (u *UiObject) Exists() (bool, error) {
raw, err := u.jsonrpc.Call("objInfo", []interface{}{u.selector.ToMap()}, 10)
if err != nil {
// UiObjectNotFoundError 意味着不存在
if _, ok := err.(*UiObjectNotFoundError); ok {
return false, nil
}
return false, err
}
return raw != nil, nil
}
// Wait 等待 UI 元素出现或消失
// exists: true 等待出现false 等待消失
// timeout: 超时时间0 使用默认值
//
// 通过 JSON-RPC 调用服务端的 waitForExists/waitUntilGone 实现
// 使用 adb forward 端口转发,连接稳定,与 Python 版本行为一致
func (u *UiObject) Wait(exists bool, timeout float64) (bool, error) {
if timeout <= 0 {
timeout = u.device.WaitTimeout()
}
if timeout <= 0 {
timeout = 10.0
}
httpWait := timeout + 10
if exists {
raw, err := u.jsonrpc.Call("waitForExists", []interface{}{u.selector.ToMap(), int(timeout * 1000)}, httpWait)
if err != nil {
// HTTP 超时时回退到 Exists 检查
if _, ok := err.(*HTTPError); ok {
ex, _ := u.Exists()
return ex, nil
}
return false, err
}
var result bool
json.Unmarshal(raw, &result)
return result, nil
}
// 等待消失
raw, err := u.jsonrpc.Call("waitUntilGone", []interface{}{u.selector.ToMap(), int(timeout * 1000)}, httpWait)
if err != nil {
if _, ok := err.(*HTTPError); ok {
ex, _ := u.Exists()
return !ex, nil
}
return false, err
}
var result bool
json.Unmarshal(raw, &result)
return result, nil
}
// WaitGone 等待 UI 元素消失
func (u *UiObject) WaitGone(timeout float64) (bool, error) {
return u.Wait(false, timeout)
}
// MustWait 等待元素出现,不存在则返回 UiObjectNotFoundError
func (u *UiObject) MustWait(timeout float64) error {
found, err := u.Wait(true, timeout)
if err != nil {
return err
}
if !found {
return &UiObjectNotFoundError{
Code: -32002,
Message: fmt.Sprintf("等待超时: %s", u.selector.String()),
Params: u.selector.ToMap(),
}
}
return nil
}
// ---------- 元素信息 ----------
// ObjInfo 包含 UI 元素的详细信息
type ObjInfo struct {
Text string `json:"text"`
ClassName string `json:"className"`
ContentDescription string `json:"contentDescription"`
PackageName string `json:"packageName"`
ResourceName string `json:"resourceName"`
Checkable bool `json:"checkable"`
Checked bool `json:"checked"`
Clickable bool `json:"clickable"`
Enabled bool `json:"enabled"`
Focusable bool `json:"focusable"`
Focused bool `json:"focused"`
LongClickable bool `json:"longClickable"`
Scrollable bool `json:"scrollable"`
Selected bool `json:"selected"`
Bounds map[string]int `json:"bounds"`
VisibleBounds map[string]int `json:"visibleBounds"`
ChildCount int `json:"childCount"`
Extra map[string]interface{} `json:"-"` // 额外字段
}
// Info 获取 UI 元素信息
func (u *UiObject) Info() (*ObjInfo, error) {
raw, err := u.jsonrpc.Call("objInfo", []interface{}{u.selector.ToMap()})
if err != nil {
return nil, err
}
var info ObjInfo
if err := json.Unmarshal(raw, &info); err != nil {
return nil, err
}
return &info, nil
}
// InfoRaw 获取 UI 元素的原始 map 信息
func (u *UiObject) InfoRaw() (map[string]interface{}, error) {
raw, err := u.jsonrpc.Call("objInfo", []interface{}{u.selector.ToMap()})
if err != nil {
return nil, err
}
var info map[string]interface{}
if err := json.Unmarshal(raw, &info); err != nil {
return nil, err
}
return info, nil
}
// InfoList 获取所有匹配元素的信息列表
func (u *UiObject) InfoList() ([]map[string]interface{}, error) {
raw, err := u.jsonrpc.Call("objInfoOfAllInstances", []interface{}{u.selector.ToMap()})
if err != nil {
return nil, err
}
var list []map[string]interface{}
if err := json.Unmarshal(raw, &list); err != nil {
return nil, err
}
return list, nil
}
// ---------- 边界和坐标 ----------
// Bounds 获取元素的边界坐标 (left, top, right, bottom)
func (u *UiObject) Bounds() (int, int, int, int, error) {
info, err := u.InfoRaw()
if err != nil {
return 0, 0, 0, 0, err
}
// 优先使用 visibleBounds
bounds, ok := info["visibleBounds"].(map[string]interface{})
if !ok {
bounds, ok = info["bounds"].(map[string]interface{})
if !ok {
return 0, 0, 0, 0, fmt.Errorf("无法获取元素边界")
}
}
lx := int(bounds["left"].(float64))
ly := int(bounds["top"].(float64))
rx := int(bounds["right"].(float64))
ry := int(bounds["bottom"].(float64))
return lx, ly, rx, ry, nil
}
// Center 获取元素中心坐标
// offset: [xoff, yoff](0,0) 表示左上角,(0.5,0.5) 表示中心
func (u *UiObject) Center(offset ...float64) (int, int, error) {
xoff, yoff := 0.5, 0.5
if len(offset) >= 2 {
xoff, yoff = offset[0], offset[1]
}
lx, ly, rx, ry, err := u.Bounds()
if err != nil {
return 0, 0, err
}
width := rx - lx
height := ry - ly
x := lx + int(float64(width)*xoff)
y := ly + int(float64(height)*yoff)
return x, y, nil
}
// ---------- 点击操作 ----------
// Click 点击 UI 元素
// timeout: 等待元素出现的超时时间0 使用默认值
func (u *UiObject) Click(timeout ...float64) error {
t := 0.0
if len(timeout) > 0 {
t = timeout[0]
}
if err := u.MustWait(t); err != nil {
return err
}
x, y, err := u.Center()
if err != nil {
return err
}
return u.device.Click(x, y)
}
// ClickWithOffset 带偏移量点击 UI 元素
func (u *UiObject) ClickWithOffset(xoff, yoff float64, timeout ...float64) error {
t := 0.0
if len(timeout) > 0 {
t = timeout[0]
}
if err := u.MustWait(t); err != nil {
return err
}
x, y, err := u.Center(xoff, yoff)
if err != nil {
return err
}
return u.device.Click(x, y)
}
// ClickExists 如果元素存在则点击,返回是否成功
func (u *UiObject) ClickExists(timeout ...float64) bool {
t := 0.0
if len(timeout) > 0 {
t = timeout[0]
}
err := u.Click(t)
return err == nil
}
// ClickGone 持续点击直到元素消失
// maxRetry: 最大重试次数
// interval: 重试间隔(秒)
func (u *UiObject) ClickGone(maxRetry int, interval float64) bool {
if maxRetry <= 0 {
maxRetry = 10
}
if interval <= 0 {
interval = 1.0
}
u.ClickExists(0)
for i := 0; i < maxRetry; i++ {
time.Sleep(time.Duration(interval * float64(time.Second)))
exists, _ := u.Exists()
if !exists {
return true
}
u.ClickExists(0)
}
return false
}
// LongClick 长按 UI 元素
// duration: 按住时间(秒),默认 0.5
func (u *UiObject) LongClick(duration float64, timeout ...float64) error {
if duration <= 0 {
duration = 0.5
}
t := 0.0
if len(timeout) > 0 {
t = timeout[0]
}
if err := u.MustWait(t); err != nil {
return err
}
x, y, err := u.Center()
if err != nil {
return err
}
return u.device.LongClick(x, y, duration)
}
// ---------- 文本操作 ----------
// GetText 获取元素文本内容
func (u *UiObject) GetText(timeout ...float64) (string, error) {
t := 0.0
if len(timeout) > 0 {
t = timeout[0]
}
if err := u.MustWait(t); err != nil {
return "", err
}
raw, err := u.jsonrpc.Call("getText", []interface{}{u.selector.ToMap()})
if err != nil {
return "", err
}
var text string
json.Unmarshal(raw, &text)
return text, nil
}
// SetText 设置元素文本内容
// 如果 text 为空,则清除文本
func (u *UiObject) SetText(text string, timeout ...float64) error {
t := 0.0
if len(timeout) > 0 {
t = timeout[0]
}
if err := u.MustWait(t); err != nil {
return err
}
if text == "" {
_, err := u.jsonrpc.Call("clearTextField", []interface{}{u.selector.ToMap()})
return err
}
_, err := u.jsonrpc.Call("setText", []interface{}{u.selector.ToMap(), text})
return err
}
// ClearText 清除元素文本
func (u *UiObject) ClearText(timeout ...float64) error {
return u.SetText("", timeout...)
}
// SendKeys SetText 的别名
func (u *UiObject) SendKeys(text string, timeout ...float64) error {
return u.SetText(text, timeout...)
}
// ---------- 滑动操作 ----------
// UiSwipe 在元素范围内滑动
// direction: 方向 "left"/"right"/"up"/"down"
// steps: 滑动步数
func (u *UiObject) UiSwipe(direction string, steps int) error {
if steps <= 0 {
steps = 10
}
if err := u.MustWait(0); err != nil {
return err
}
lx, ly, rx, ry, err := u.Bounds()
if err != nil {
return err
}
cx := (lx + rx) / 2
cy := (ly + ry) / 2
switch direction {
case "up":
return u.device.Swipe(cx, cy, cx, ly, steps)
case "down":
return u.device.Swipe(cx, cy, cx, ry-1, steps)
case "left":
return u.device.Swipe(cx, cy, lx, cy, steps)
case "right":
return u.device.Swipe(cx, cy, rx-1, cy, steps)
default:
return fmt.Errorf("不支持的方向: %s", direction)
}
}
// DragTo 将元素拖拽到指定坐标
func (u *UiObject) DragTo(x, y int, duration float64, timeout ...float64) error {
if duration <= 0 {
duration = 0.5
}
t := 0.0
if len(timeout) > 0 {
t = timeout[0]
}
if err := u.MustWait(t); err != nil {
return err
}
steps := int(duration * 200)
_, err := u.jsonrpc.Call("dragTo", []interface{}{u.selector.ToMap(), x, y, steps})
return err
}
// ---------- 手势操作 ----------
// PinchIn 向内捏合(缩小)
func (u *UiObject) PinchIn(percent, steps int) error {
if percent <= 0 {
percent = 100
}
if steps <= 0 {
steps = 50
}
_, err := u.jsonrpc.Call("pinchIn", []interface{}{u.selector.ToMap(), percent, steps})
return err
}
// PinchOut 向外捏合(放大)
func (u *UiObject) PinchOut(percent, steps int) error {
if percent <= 0 {
percent = 100
}
if steps <= 0 {
steps = 50
}
_, err := u.jsonrpc.Call("pinchOut", []interface{}{u.selector.ToMap(), percent, steps})
return err
}
// ---------- 子/兄弟元素 ----------
// Child 查找子元素
func (u *UiObject) Child(params map[string]interface{}) (*UiObject, error) {
sel := u.selector.Clone()
if _, err := sel.Child(params); err != nil {
return nil, err
}
return NewUiObject(u.device, sel), nil
}
// Sibling 查找兄弟元素
func (u *UiObject) Sibling(params map[string]interface{}) (*UiObject, error) {
sel := u.selector.Clone()
if _, err := sel.Sibling(params); err != nil {
return nil, err
}
return NewUiObject(u.device, sel), nil
}
// ChildByText 通过文本查找子元素
func (u *UiObject) ChildByText(text string, params map[string]interface{}) (*UiObject, error) {
childSel, err := New(params)
if err != nil {
return nil, err
}
raw, err := u.jsonrpc.Call("childByText", []interface{}{u.selector.ToMap(), childSel.ToMap(), text})
if err != nil {
return nil, err
}
var resultMap map[string]interface{}
if err := json.Unmarshal(raw, &resultMap); err != nil {
return nil, err
}
resultSel, err := FromMap(resultMap)
if err != nil {
return nil, err
}
return NewUiObject(u.device, resultSel), nil
}
// ChildByDescription 通过描述查找子元素
func (u *UiObject) ChildByDescription(desc string, params map[string]interface{}) (*UiObject, error) {
childSel, err := New(params)
if err != nil {
return nil, err
}
raw, err := u.jsonrpc.Call("childByDescription", []interface{}{u.selector.ToMap(), childSel.ToMap(), desc})
if err != nil {
return nil, err
}
var resultMap map[string]interface{}
if err := json.Unmarshal(raw, &resultMap); err != nil {
return nil, err
}
resultSel, err := FromMap(resultMap)
if err != nil {
return nil, err
}
return NewUiObject(u.device, resultSel), nil
}
// ---------- 数量和索引 ----------
// Count 获取匹配元素的数量
func (u *UiObject) Count() (int, error) {
raw, err := u.jsonrpc.Call("count", []interface{}{u.selector.ToMap()})
if err != nil {
return 0, err
}
var count int
json.Unmarshal(raw, &count)
return count, nil
}
// Instance 获取指定索引的元素
func (u *UiObject) Instance(index int) *UiObject {
sel := u.selector.Clone()
if index < 0 {
// 负数索引需要先获取总数
count, err := u.Count()
if err == nil && index+count >= 0 {
index = index + count
} else {
index = 0
}
}
sel.UpdateInstance(index)
return NewUiObject(u.device, sel)
}
// ---------- 滚动操作 ----------
// ScrollForward 向前滚动
func (u *UiObject) ScrollForward(vertical bool, steps int) (bool, error) {
if steps <= 0 {
steps = ScrollSteps
}
raw, err := u.jsonrpc.Call("scrollForward", []interface{}{u.selector.ToMap(), vertical, steps})
if err != nil {
return false, err
}
var result bool
json.Unmarshal(raw, &result)
return result, nil
}
// ScrollBackward 向后滚动
func (u *UiObject) ScrollBackward(vertical bool, steps int) (bool, error) {
if steps <= 0 {
steps = ScrollSteps
}
raw, err := u.jsonrpc.Call("scrollBackward", []interface{}{u.selector.ToMap(), vertical, steps})
if err != nil {
return false, err
}
var result bool
json.Unmarshal(raw, &result)
return result, nil
}
// ScrollToBeginning 滚动到开头
func (u *UiObject) ScrollToBeginning(vertical bool, maxSwipes, steps int) (bool, error) {
if maxSwipes <= 0 {
maxSwipes = 500
}
if steps <= 0 {
steps = ScrollSteps
}
raw, err := u.jsonrpc.Call("scrollToBeginning", []interface{}{u.selector.ToMap(), vertical, maxSwipes, steps})
if err != nil {
return false, err
}
var result bool
json.Unmarshal(raw, &result)
return result, nil
}
// ScrollToEnd 滚动到末尾
func (u *UiObject) ScrollToEnd(vertical bool, maxSwipes, steps int) (bool, error) {
if maxSwipes <= 0 {
maxSwipes = 500
}
if steps <= 0 {
steps = ScrollSteps
}
raw, err := u.jsonrpc.Call("scrollToEnd", []interface{}{u.selector.ToMap(), vertical, maxSwipes, steps})
if err != nil {
return false, err
}
var result bool
json.Unmarshal(raw, &result)
return result, nil
}
// ScrollTo 滚动到指定元素可见
func (u *UiObject) ScrollTo(targetParams map[string]interface{}, vertical bool) (bool, error) {
targetSel, err := New(targetParams)
if err != nil {
return false, err
}
raw, err := u.jsonrpc.Call("scrollTo", []interface{}{u.selector.ToMap(), targetSel.ToMap(), vertical})
if err != nil {
return false, err
}
var result bool
json.Unmarshal(raw, &result)
return result, nil
}
// ---------- Fling 操作 ----------
// FlingForward 向前快速滑动
func (u *UiObject) FlingForward(vertical bool) (bool, error) {
raw, err := u.jsonrpc.Call("flingForward", []interface{}{u.selector.ToMap(), vertical})
if err != nil {
return false, err
}
var result bool
json.Unmarshal(raw, &result)
return result, nil
}
// FlingBackward 向后快速滑动
func (u *UiObject) FlingBackward(vertical bool) (bool, error) {
raw, err := u.jsonrpc.Call("flingBackward", []interface{}{u.selector.ToMap(), vertical})
if err != nil {
return false, err
}
var result bool
json.Unmarshal(raw, &result)
return result, nil
}
// FlingToBeginning 快速滑动到开头
func (u *UiObject) FlingToBeginning(vertical bool, maxSwipes int) (bool, error) {
if maxSwipes <= 0 {
maxSwipes = 500
}
raw, err := u.jsonrpc.Call("flingToBeginning", []interface{}{u.selector.ToMap(), vertical, maxSwipes})
if err != nil {
return false, err
}
var result bool
json.Unmarshal(raw, &result)
return result, nil
}
// FlingToEnd 快速滑动到末尾
func (u *UiObject) FlingToEnd(vertical bool, maxSwipes int) (bool, error) {
if maxSwipes <= 0 {
maxSwipes = 500
}
raw, err := u.jsonrpc.Call("flingToEnd", []interface{}{u.selector.ToMap(), vertical, maxSwipes})
if err != nil {
return false, err
}
var result bool
json.Unmarshal(raw, &result)
return result, nil
}

278
libs/watcher.go Normal file
View File

@@ -0,0 +1,278 @@
package libs
import (
"fmt"
"log"
"sync"
"time"
)
// ---------- WatchContext简化版弹窗监控 ----------
// WatchCondition 定义一个监控条件和对应的操作
type WatchCondition struct {
// Selectors 匹配条件列表xpath 或文本),全部匹配时触发
Selectors []map[string]interface{}
// Callback 匹配后的回调操作
Callback func(d *Device) error
}
// WatchContext 提供 UI 弹窗/对话框的自动监控和处理
// 对应 Python 版本的 WatchContext 和 Watcher
type WatchContext struct {
device *Device
// 监控条件列表
conditions []WatchCondition
// 当前正在构建的条件
pendingSelectors []map[string]interface{}
// 状态管理
mu sync.Mutex
stopCh chan struct{}
stopped chan struct{}
started bool
triggerTime time.Time
// 检查间隔(秒)
interval float64
}
// NewWatchContext 创建一个新的监控上下文
// builtin: 是否添加内置的中文弹窗处理规则
func NewWatchContext(device *Device, builtin bool) *WatchContext {
wc := &WatchContext{
device: device,
conditions: []WatchCondition{},
interval: 2.0,
triggerTime: time.Now(),
}
if builtin {
// 添加常见的中文弹窗自动处理规则
wc.WhenText("继续使用").Click()
wc.WhenText("同意").Click()
wc.WhenText("确定").Click()
wc.WhenText("好的").Click()
wc.WhenText("继续安装").Click()
wc.WhenText("安装").Click()
wc.WhenText("Agree").Click()
wc.WhenText("ALLOW").Click()
}
return wc
}
// WhenText 添加按文本匹配的监控条件(支持链式调用)
func (wc *WatchContext) WhenText(text string) *WatchContext {
wc.pendingSelectors = append(wc.pendingSelectors, map[string]interface{}{
"text": text,
})
return wc
}
// WhenDescription 添加按描述匹配的监控条件
func (wc *WatchContext) WhenDescription(desc string) *WatchContext {
wc.pendingSelectors = append(wc.pendingSelectors, map[string]interface{}{
"description": desc,
})
return wc
}
// WhenResourceID 添加按资源 ID 匹配的监控条件
func (wc *WatchContext) WhenResourceID(id string) *WatchContext {
wc.pendingSelectors = append(wc.pendingSelectors, map[string]interface{}{
"resourceId": id,
})
return wc
}
// Click 为当前待处理的条件设置点击操作
func (wc *WatchContext) Click() {
if len(wc.pendingSelectors) == 0 {
return
}
selectors := make([]map[string]interface{}, len(wc.pendingSelectors))
copy(selectors, wc.pendingSelectors)
wc.pendingSelectors = nil
wc.conditions = append(wc.conditions, WatchCondition{
Selectors: selectors,
Callback: func(d *Device) error {
// 点击最后一个匹配的选择器
lastSel := selectors[len(selectors)-1]
obj, err := d.FindElement(lastSel)
if err != nil {
return err
}
return obj.Click(0)
},
})
}
// Press 为当前待处理的条件设置按键操作
func (wc *WatchContext) Press(key string) {
if len(wc.pendingSelectors) == 0 {
return
}
selectors := make([]map[string]interface{}, len(wc.pendingSelectors))
copy(selectors, wc.pendingSelectors)
wc.pendingSelectors = nil
wc.conditions = append(wc.conditions, WatchCondition{
Selectors: selectors,
Callback: func(d *Device) error {
return d.Press(key)
},
})
}
// Call 为当前待处理的条件设置自定义回调
func (wc *WatchContext) Call(fn func(d *Device) error) {
if len(wc.pendingSelectors) == 0 {
return
}
selectors := make([]map[string]interface{}, len(wc.pendingSelectors))
copy(selectors, wc.pendingSelectors)
wc.pendingSelectors = nil
wc.conditions = append(wc.conditions, WatchCondition{
Selectors: selectors,
Callback: fn,
})
}
// ---------- 运行控制 ----------
// Start 开始后台监控
func (wc *WatchContext) Start() {
wc.mu.Lock()
defer wc.mu.Unlock()
if wc.started {
return
}
wc.started = true
wc.stopCh = make(chan struct{})
wc.stopped = make(chan struct{})
go wc.runForever()
}
// Stop 停止监控
func (wc *WatchContext) Stop() {
wc.mu.Lock()
if !wc.started {
wc.mu.Unlock()
return
}
close(wc.stopCh)
wc.mu.Unlock()
// 等待停止
select {
case <-wc.stopped:
case <-time.After(10 * time.Second):
}
wc.mu.Lock()
wc.started = false
wc.mu.Unlock()
}
// Running 检查是否正在运行
func (wc *WatchContext) Running() bool {
wc.mu.Lock()
defer wc.mu.Unlock()
return wc.started
}
// runForever 持续监控循环
func (wc *WatchContext) runForever() {
defer close(wc.stopped)
ticker := time.NewTicker(time.Duration(wc.interval * float64(time.Second)))
defer ticker.Stop()
for {
select {
case <-wc.stopCh:
return
case <-ticker.C:
wc.runOnce()
}
}
}
// runOnce 执行一次监控检查
func (wc *WatchContext) runOnce() bool {
wc.mu.Lock()
defer wc.mu.Unlock()
for _, cond := range wc.conditions {
allMatched := true
for _, sel := range cond.Selectors {
exists, err := wc.device.Exists(sel)
if err != nil || !exists {
allMatched = false
break
}
}
if allMatched {
log.Printf("[Watcher] 条件匹配,执行回调")
if err := cond.Callback(wc.device); err != nil {
log.Printf("[Watcher] 回调执行失败: %v", err)
}
wc.triggerTime = time.Now()
return true
}
}
return false
}
// WaitStable 等待直到监控不再触发(稳定状态)
// stableSeconds: 稳定时间(秒)
// timeout: 超时时间(秒)
func (wc *WatchContext) WaitStable(stableSeconds, timeout float64) error {
if stableSeconds <= 0 {
stableSeconds = 5.0
}
if timeout <= 0 {
timeout = 60.0
}
if !wc.started {
wc.Start()
}
deadline := time.Now().Add(time.Duration(timeout * float64(time.Second)))
for time.Now().Before(deadline) {
wc.mu.Lock()
stable := time.Since(wc.triggerTime).Seconds() > stableSeconds
wc.mu.Unlock()
if stable {
return nil
}
time.Sleep(200 * time.Millisecond)
}
return fmt.Errorf("等待稳定超时")
}
// Reset 停止并移除所有监控条件
func (wc *WatchContext) Reset() {
if wc.started {
wc.Stop()
}
wc.conditions = nil
}
// Remove 移除所有监控条件
func (wc *WatchContext) Remove() {
wc.conditions = nil
}