Files
go-mobile-uiautomator/libs/device.go

976 lines
25 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 // 调试模式
// 设备连接接口(通过 ADB 隧道直连)
dev AdbDevice
// JSON-RPC 调用器
jsonrpc *JsonRpcWrapper
// 设置
settings *Settings
// UIAutomator2 进程管理
mu sync.Mutex
processConn net.Conn // 启动 UIAutomator 时的连接
// 窗口尺寸缓存
windowSizeCache [2]int
}
// NewDevice 创建一个新的 Device 客户端并启动 UIAutomator2 服务
// 通过 ADB 隧道直连设备,与 Python uiautomator2 完全一致
//
// serial: 设备序列号(如 "emulator-5554"
// addr: 可选ADB 服务器地址,不传则使用默认值 "127.0.0.1:5037"
//
// 创建后会自动执行:
// 1. 推送内嵌的 u2.jar 到设备(如果尚未存在)
// 2. 启动 UIAutomator2 服务
// 3. 等待服务就绪
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, 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"
}
// 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
}