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, ``) { 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: 是否等待到前台 // 返回应用的 PID,0 表示未启动 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 }