完成基础功能

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
}