docs: 更新包文档,匹配 ADB 隧道架构和当前 API
This commit is contained in:
@@ -233,6 +233,20 @@ func PushFile(addr, serial, localPath, remotePath string, mode int, debug bool)
|
|||||||
return s.SyncPushFile(localPath, remotePath, mode, debug)
|
return s.SyncPushFile(localPath, remotePath, mode, debug)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PushData 通过 ADB 推送内存数据到设备
|
||||||
|
// 与 PushFile 功能相同,但数据来源是 []byte 而非本地文件
|
||||||
|
// 适用于通过 go:embed 嵌入的资源文件
|
||||||
|
func PushData(addr, serial string, data []byte, remotePath string, mode int, debug bool) (int64, error) {
|
||||||
|
conn, err := ConnectToDevice(addr, serial, 15*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
s := InitSync(conn)
|
||||||
|
return s.SyncPushData(data, remotePath, mode, debug)
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- ADB 隧道(与 Python uiautomator2 完全一致) ----------
|
// ---------- ADB 隧道(与 Python uiautomator2 完全一致) ----------
|
||||||
|
|
||||||
// CreateTunnel 建立到设备指定端口的 ADB 隧道
|
// CreateTunnel 建立到设备指定端口的 ADB 隧道
|
||||||
|
|||||||
26
adb/doc.go
26
adb/doc.go
@@ -1,21 +1,27 @@
|
|||||||
// Package adb 实现了 Android Debug Bridge (ADB) 协议的核心功能。
|
// Package adb 实现了 Android Debug Bridge (ADB) 协议的核心功能。
|
||||||
//
|
//
|
||||||
// 本包提供了纯 Go 实现的 ADB 客户端,支持以下功能:
|
// 本包提供了纯 Go 实现的 ADB 客户端,零外部依赖,支持以下功能:
|
||||||
// - 设备发现与连接管理
|
// - 设备发现与连接管理
|
||||||
// - Shell 命令执行
|
// - Shell 命令执行
|
||||||
// - 文件同步传输(推送文件到设备)
|
// - ADB 隧道(transport + tcp:<port>,与 Python adbutils 一致)
|
||||||
|
// - 文件同步传输(Sync Push 协议)
|
||||||
// - APK 安装
|
// - APK 安装
|
||||||
// - UIAutomator2 服务启动
|
//
|
||||||
|
// ADB 隧道是本包的核心功能,用于替代 adb forward 端口转发。
|
||||||
|
// 每次调用 CreateTunnel 会建立一条直通设备端口的原始 TCP 管道,
|
||||||
|
// 无需占用本地端口,无需额外清理。
|
||||||
//
|
//
|
||||||
// 使用示例:
|
// 使用示例:
|
||||||
//
|
//
|
||||||
// // 连接 ADB 服务器
|
// // 列出所有设备
|
||||||
// conn, err := adb.DialADB("127.0.0.1:5037", 15*time.Second)
|
// payload, _ := adb.ListDevicesRaw("127.0.0.1:5037", 15*time.Second)
|
||||||
|
// devices := adb.ParseDevicesPayload(payload)
|
||||||
|
//
|
||||||
|
// // 建立 ADB 隧道到设备端口 9008
|
||||||
|
// conn, err := adb.CreateTunnel("127.0.0.1:5037", "emulator-5554", 9008)
|
||||||
// defer conn.Close()
|
// defer conn.Close()
|
||||||
|
// // conn 现在是一条直通设备 9008 端口的 TCP 管道
|
||||||
//
|
//
|
||||||
// // 路由到指定设备
|
// // 推送文件到设备
|
||||||
// adb.TransportTo(conn, "emulator-5556")
|
// adb.PushFile("127.0.0.1:5037", "emulator-5554", "local.txt", "/sdcard/remote.txt", 0644, false)
|
||||||
//
|
|
||||||
// // 执行 Shell 命令
|
|
||||||
// output, err := adb.ExecShell(conn, "getprop ro.product.model")
|
|
||||||
package adb
|
package adb
|
||||||
|
|||||||
79
adb/sync.go
79
adb/sync.go
@@ -1,6 +1,7 @@
|
|||||||
package adb
|
package adb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -119,6 +120,84 @@ func (s *Sync) SyncPushFile(localPath, remotePath string, mode int, debug bool)
|
|||||||
return total, nil
|
return total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SyncPushData 将内存中的数据推送到设备
|
||||||
|
// 与 SyncPushFile 功能相同,但数据来源是 []byte 而非本地文件
|
||||||
|
// 适用于通过 go:embed 嵌入的资源文件
|
||||||
|
func (s *Sync) SyncPushData(data []byte, remotePath string, mode int, debug bool) (int64, error) {
|
||||||
|
// 初始化同步模式
|
||||||
|
if err := s.StartSync(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造 SEND 请求
|
||||||
|
modeStr := strconv.Itoa(syscall.S_IFREG | mode)
|
||||||
|
sendPayload := []byte(remotePath + "," + modeStr)
|
||||||
|
|
||||||
|
hdr := make([]byte, 8)
|
||||||
|
copy(hdr[:4], []byte("SEND"))
|
||||||
|
binary.LittleEndian.PutUint32(hdr[4:], uint32(len(sendPayload)))
|
||||||
|
if _, err := s.Conn.Write(hdr); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if _, err := s.Conn.Write(sendPayload); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if debug {
|
||||||
|
fmt.Printf("[调试] 发送 SEND 请求: 长度=%d 路径=%s 权限=%s 数据大小=%d\n", len(sendPayload), remotePath, modeStr, len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分块写入数据
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
var total int64
|
||||||
|
buf := make([]byte, maxChunk)
|
||||||
|
for {
|
||||||
|
n, rerr := reader.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
dataHdr := make([]byte, 8)
|
||||||
|
copy(dataHdr[:4], []byte("DATA"))
|
||||||
|
binary.LittleEndian.PutUint32(dataHdr[4:], uint32(n))
|
||||||
|
if _, err := s.Conn.Write(dataHdr); err != nil {
|
||||||
|
return total, err
|
||||||
|
}
|
||||||
|
if _, err := s.Conn.Write(buf[:n]); err != nil {
|
||||||
|
return total, err
|
||||||
|
}
|
||||||
|
total += int64(n)
|
||||||
|
}
|
||||||
|
if rerr != nil {
|
||||||
|
if rerr == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return total, rerr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送 DONE 命令(使用当前时间戳)
|
||||||
|
done := make([]byte, 8)
|
||||||
|
copy(done[:4], []byte("DONE"))
|
||||||
|
mtime := uint32(time.Now().Unix())
|
||||||
|
binary.LittleEndian.PutUint32(done[4:], mtime)
|
||||||
|
if _, err := s.Conn.Write(done); err != nil {
|
||||||
|
return total, err
|
||||||
|
}
|
||||||
|
if debug {
|
||||||
|
fmt.Println("[调试] 发送 DONE,等待响应")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取最终响应
|
||||||
|
resp, msg, err := ReadSyncStatus(s.Conn)
|
||||||
|
if err != nil {
|
||||||
|
return total, err
|
||||||
|
}
|
||||||
|
if resp != "OKAY" {
|
||||||
|
if len(msg) > 0 {
|
||||||
|
return total, fmt.Errorf("同步失败: %s", string(msg))
|
||||||
|
}
|
||||||
|
return total, fmt.Errorf("同步失败: %s", resp)
|
||||||
|
}
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
// StartSync 启动 ADB 同步模式
|
// StartSync 启动 ADB 同步模式
|
||||||
// 发送 "sync:" 命令并等待 "OKAY" 响应
|
// 发送 "sync:" 命令并等待 "OKAY" 响应
|
||||||
func (s *Sync) StartSync() error {
|
func (s *Sync) StartSync() error {
|
||||||
|
|||||||
24
assets/embed.go
Normal file
24
assets/embed.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Package assets 嵌入 UIAutomator2 运行所需的资源文件。
|
||||||
|
//
|
||||||
|
// 通过 go:embed 将 u2.jar 和 APK 编译到 Go 二进制中,
|
||||||
|
// 使用者无需手动管理资源文件路径。
|
||||||
|
package assets
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
// 嵌入 UIAutomator2 运行所需的资源文件
|
||||||
|
var (
|
||||||
|
//go:embed u2.jar
|
||||||
|
JarData []byte
|
||||||
|
|
||||||
|
//go:embed app-uiautomator.apk
|
||||||
|
ApkData []byte
|
||||||
|
|
||||||
|
//go:embed app-uiautomator-test.apk
|
||||||
|
ApkTestData []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
// FS 提供对嵌入文件的文件系统访问
|
||||||
|
//
|
||||||
|
//go:embed *.jar *.apk
|
||||||
|
var FS embed.FS
|
||||||
@@ -27,11 +27,10 @@ type Device struct {
|
|||||||
serial string // 设备序列号
|
serial string // 设备序列号
|
||||||
|
|
||||||
// UIAutomator2 服务配置
|
// UIAutomator2 服务配置
|
||||||
serverPort int // 设备端服务端口(默认 9008)
|
serverPort int // 设备端服务端口(默认 9008)
|
||||||
debug bool // 调试模式
|
debug bool // 调试模式
|
||||||
jarPath string // 本地 u2.jar 路径(空字符串使用默认路径)
|
|
||||||
|
|
||||||
// 设备连接接口(通过 adb forward 连接)
|
// 设备连接接口(通过 ADB 隧道直连)
|
||||||
dev AdbDevice
|
dev AdbDevice
|
||||||
|
|
||||||
// JSON-RPC 调用器
|
// JSON-RPC 调用器
|
||||||
@@ -49,16 +48,15 @@ type Device struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewDevice 创建一个新的 Device 客户端并启动 UIAutomator2 服务
|
// NewDevice 创建一个新的 Device 客户端并启动 UIAutomator2 服务
|
||||||
// 使用 adb forward 端口转发,与 Python uiautomator2 相同的方案
|
// 通过 ADB 隧道直连设备,与 Python uiautomator2 完全一致
|
||||||
//
|
//
|
||||||
// serial: 设备序列号(如 "emulator-5554")
|
// serial: 设备序列号(如 "emulator-5554")
|
||||||
// addr: 可选,ADB 服务器地址,不传则使用默认值 "127.0.0.1:5037"
|
// addr: 可选,ADB 服务器地址,不传则使用默认值 "127.0.0.1:5037"
|
||||||
//
|
//
|
||||||
// 创建后会自动执行:
|
// 创建后会自动执行:
|
||||||
// 1. 设置 adb forward 端口转发
|
// 1. 推送内嵌的 u2.jar 到设备(如果尚未存在)
|
||||||
// 2. 推送 u2.jar 到设备(如果尚未存在)
|
// 2. 启动 UIAutomator2 服务
|
||||||
// 3. 启动 UIAutomator2 服务
|
// 3. 等待服务就绪
|
||||||
// 4. 等待服务就绪
|
|
||||||
func NewDevice(serial string, addr ...string) (*Device, error) {
|
func NewDevice(serial string, addr ...string) (*Device, error) {
|
||||||
a := DefaultADBAddr
|
a := DefaultADBAddr
|
||||||
if len(addr) > 0 && addr[0] != "" {
|
if len(addr) > 0 && addr[0] != "" {
|
||||||
@@ -80,8 +78,8 @@ func NewDevice(serial string, addr ...string) (*Device, error) {
|
|||||||
return d.jsonrpcCall(method, params, timeout)
|
return d.jsonrpcCall(method, params, timeout)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 推送 u2.jar 到设备(仅在文件不存在时推送)
|
// 推送内嵌的 u2.jar 到设备(仅在文件不存在时推送)
|
||||||
if err := services.InstallServiceJar(a, serial, d.jarPath, false); err != nil {
|
if err := services.InstallServiceJar(a, serial, false); err != nil {
|
||||||
return nil, fmt.Errorf("安装 u2.jar 失败: %w", err)
|
return nil, fmt.Errorf("安装 u2.jar 失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,12 +198,6 @@ func (d *Device) checkAlive() bool {
|
|||||||
return string(resp.Content) == "pong"
|
return string(resp.Content) == "pong"
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetJarPath 设置本地 u2.jar 路径
|
|
||||||
// 传空字符串则使用默认路径 (assets/u2.jar)
|
|
||||||
func (d *Device) SetJarPath(path string) {
|
|
||||||
d.jarPath = path
|
|
||||||
}
|
|
||||||
|
|
||||||
// launchAndWait 启动 UIAutomator2 进程并等待就绪
|
// launchAndWait 启动 UIAutomator2 进程并等待就绪
|
||||||
func (d *Device) launchAndWait() error {
|
func (d *Device) launchAndWait() error {
|
||||||
// 通过 ADB shell 启动 UIAutomator2
|
// 通过 ADB shell 启动 UIAutomator2
|
||||||
|
|||||||
51
libs/doc.go
51
libs/doc.go
@@ -1,45 +1,50 @@
|
|||||||
// Package libs 提供了完整的 Android UIAutomator2 自动化框架。
|
// Package libs 提供了完整的 Android UIAutomator2 自动化框架。
|
||||||
//
|
//
|
||||||
// 本包是 Python uiautomator2 的 Go 语言实现,通过 ADB 协议与运行在 Android 设备上的
|
// 本包是 Python uiautomator2 的 Go 语言实现,通过 ADB 隧道协议与运行在 Android 设备上的
|
||||||
// UIAutomator2 HTTP 服务通信,提供设备控制、UI 操作、文本输入等功能。
|
// UIAutomator2 HTTP 服务直连,提供设备控制、UI 操作、文本输入等功能。
|
||||||
|
// 零外部依赖,仅使用 Go 标准库。
|
||||||
//
|
//
|
||||||
// 核心组件:
|
// 核心组件:
|
||||||
// - Device:设备客户端,管理 UIAutomator2 服务生命周期,提供所有设备操作
|
// - Device:设备客户端,管理 UIAutomator2 服务生命周期,提供所有设备操作
|
||||||
// - UiObject:UI 控件对象,支持点击、输入、滑动、等待等操作
|
// - UiObject:UI 控件对象,支持点击、输入、滑动、等待、滚动等操作
|
||||||
// - Selector:UI 元素选择器,支持文本、类名、资源 ID 等多种查询条件
|
// - Selector:UI 元素选择器,支持文本、类名、资源 ID、描述等多种查询条件
|
||||||
// - JsonRpcWrapper:JSON-RPC 2.0 调用封装
|
// - JsonRpcWrapper:JSON-RPC 2.0 调用封装,自动错误映射和重试
|
||||||
// - InputMethod:通过 AdbKeyboard 输入法实现快速文本输入
|
// - InputMethod:通过 AdbKeyboard 输入法实现快速文本输入(支持中文)
|
||||||
// - SwipeExt:扩展滑动操作(按方向、比例滑动)
|
// - SwipeExt:扩展滑动操作(按方向、比例滑动)
|
||||||
// - WatchContext/Watcher:弹窗/对话框自动监控和处理
|
// - WatchContext:弹窗/对话框自动监控和处理
|
||||||
// - Session:应用会话管理,自动检测应用状态
|
// - Session:应用会话管理,自动检测应用存活状态
|
||||||
// - Settings:设备配置管理(等待超时、操作延迟等)
|
// - Settings:线程安全的设备配置管理(等待超时、操作延迟等)
|
||||||
//
|
//
|
||||||
// 通信层:
|
// 通信层:
|
||||||
// - AdbHTTPConnection:通过 ADB 隧道发送 HTTP 请求
|
// - AdbTunnelDevice:通过 ADB 隧道直连设备,与 Python uiautomator2 方案完全一致
|
||||||
|
// - http.Transport:自定义 DialContext 将 HTTP 连接替换为 ADB 隧道
|
||||||
// - HttpRequest:高层 HTTP 请求封装
|
// - HttpRequest:高层 HTTP 请求封装
|
||||||
// - JsonRpcCall:JSON-RPC 2.0 请求/响应处理
|
// - JsonRpcCall:JSON-RPC 2.0 请求/响应处理
|
||||||
//
|
//
|
||||||
// 使用示例:
|
// 使用示例:
|
||||||
//
|
//
|
||||||
// // 创建设备连接
|
// // 创建设备连接(自动推送 u2.jar、启动服务)
|
||||||
// device, err := libs.NewDevice("emulator-5554")
|
// d, err := libs.NewDevice("emulator-5554")
|
||||||
// // 自定义 ADB 地址: libs.NewDevice("emulator-5554", "192.168.1.100:5037")
|
// // 自定义 ADB 地址: libs.NewDevice("emulator-5554", "192.168.1.100:5037")
|
||||||
// if err != nil {
|
// if err != nil {
|
||||||
// log.Fatal(err)
|
// log.Fatal(err)
|
||||||
// }
|
// }
|
||||||
// defer device.StopUiautomator()
|
// defer d.Close()
|
||||||
//
|
//
|
||||||
// // 查找并点击按钮
|
// // 便捷选择器,链式调用
|
||||||
// btn, _ := device.FindElement(map[string]interface{}{"text": "登录"})
|
// d.ByText("登录").Click()
|
||||||
// btn.Click()
|
// d.ByResourceId("com.example:id/input").SetText("admin")
|
||||||
//
|
//
|
||||||
// // 输入文本
|
// // 多条件组合查找
|
||||||
// input, _ := device.FindElement(map[string]interface{}{"resourceId": "com.example:id/username"})
|
// d.By(libs.P{"className": "android.widget.Button", "text": "确定"}).Click()
|
||||||
// input.SetText("admin")
|
//
|
||||||
|
// // 使用 AdbKeyboard 输入中文
|
||||||
|
// im := libs.NewInputMethod(d)
|
||||||
|
// im.SendKeys("你好世界")
|
||||||
//
|
//
|
||||||
// // 使用 Watcher 自动处理弹窗
|
// // 使用 Watcher 自动处理弹窗
|
||||||
// watcher := libs.NewWatcher(device)
|
// w := libs.NewWatchContext(d, true)
|
||||||
// watcher.WhenText("同意").Click()
|
// w.WhenText("允许").Click()
|
||||||
// watcher.Start(2.0)
|
// w.Start()
|
||||||
// defer watcher.Stop()
|
// defer w.Stop()
|
||||||
package libs
|
package libs
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
// Package services 提供了 UIAutomator2 服务的安装和部署功能。
|
// Package services 提供了 UIAutomator2 服务的安装和部署功能。
|
||||||
//
|
//
|
||||||
// 本包负责将 UIAutomator2 相关的资源文件(u2.jar、APK)
|
// 本包负责将 UIAutomator2 相关的资源文件推送到 Android 设备并完成安装:
|
||||||
// 推送到 Android 设备并完成安装。
|
// - InstallServiceJar:推送 u2.jar 到设备(自动检查设备端是否已存在,避免重复推送)
|
||||||
|
// - InstallServiceApk:推送并安装 UIAutomator2 APK
|
||||||
|
//
|
||||||
|
// 通常无需直接调用本包,libs.NewDevice() 会自动完成服务安装和启动。
|
||||||
package services
|
package services
|
||||||
|
|||||||
@@ -2,22 +2,17 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/zhuy1228/go-mobile-uiautomator/adb"
|
"github.com/zhuy1228/go-mobile-uiautomator/adb"
|
||||||
|
"github.com/zhuy1228/go-mobile-uiautomator/assets"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 默认路径常量
|
// 设备端路径常量
|
||||||
const (
|
const (
|
||||||
// DefaultJarLocal 默认本地 JAR 路径
|
|
||||||
DefaultJarLocal = "assets/u2.jar"
|
|
||||||
// DefaultJarRemote 默认设备端 JAR 路径
|
// DefaultJarRemote 默认设备端 JAR 路径
|
||||||
DefaultJarRemote = "/data/local/tmp/u2.jar"
|
DefaultJarRemote = "/data/local/tmp/u2.jar"
|
||||||
// DefaultApkLocal 默认本地 APK 路径
|
|
||||||
DefaultApkLocal = "assets/app-uiautomator.apk"
|
|
||||||
// DefaultApkRemote 默认设备端 APK 临时路径
|
// DefaultApkRemote 默认设备端 APK 临时路径
|
||||||
DefaultApkRemote = "/data/local/tmp/app-uiautomator.apk"
|
DefaultApkRemote = "/data/local/tmp/app-uiautomator.apk"
|
||||||
)
|
)
|
||||||
@@ -39,63 +34,42 @@ func fileExistsOnDevice(addr, serial, remotePath string) bool {
|
|||||||
return strings.TrimSpace(string(out)) == remotePath
|
return strings.TrimSpace(string(out)) == remotePath
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstallServiceJar 将 u2.jar 推送到设备的 /data/local/tmp/ 目录
|
// InstallServiceJar 将内嵌的 u2.jar 推送到设备的 /data/local/tmp/ 目录
|
||||||
|
// JAR 文件通过 go:embed 编译到二进制中,使用者无需关心文件路径
|
||||||
|
//
|
||||||
// addr 为 ADB 服务器地址,serial 为设备序列号
|
// addr 为 ADB 服务器地址,serial 为设备序列号
|
||||||
// localPath 为本地 JAR 路径,传空字符串则使用默认路径
|
|
||||||
// force 为 true 时跳过存在性检查,强制推送
|
// force 为 true 时跳过存在性检查,强制推送
|
||||||
func InstallServiceJar(addr, serial, localPath string, force bool) error {
|
func InstallServiceJar(addr, serial string, force bool) error {
|
||||||
if localPath == "" {
|
|
||||||
localPath = DefaultJarLocal
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查本地文件是否存在
|
|
||||||
if _, err := os.Stat(localPath); os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("本地 JAR 文件不存在: %s", localPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 非强制模式下,检查设备端文件是否已存在
|
// 非强制模式下,检查设备端文件是否已存在
|
||||||
if !force && fileExistsOnDevice(addr, serial, DefaultJarRemote) {
|
if !force && fileExistsOnDevice(addr, serial, DefaultJarRemote) {
|
||||||
return nil // 文件已存在,跳过推送
|
return nil // 文件已存在,跳过推送
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := adb.PushFile(addr, serial, localPath, DefaultJarRemote, 0644, false)
|
_, err := adb.PushData(addr, serial, assets.JarData, DefaultJarRemote, 0644, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("推送 u2.jar 失败: %w", err)
|
return fmt.Errorf("推送 u2.jar 失败: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstallServiceApk 将 UIAutomator2 APK 推送到设备并安装
|
// InstallServiceApk 将内嵌的 UIAutomator2 APK 推送到设备并安装
|
||||||
|
// APK 文件通过 go:embed 编译到二进制中,使用者无需关心文件路径
|
||||||
|
//
|
||||||
// addr 为 ADB 服务器地址,serial 为设备序列号
|
// addr 为 ADB 服务器地址,serial 为设备序列号
|
||||||
// localPath 为本地 APK 路径,传空字符串则使用默认路径
|
|
||||||
// force 为 true 时跳过存在性检查,强制推送
|
// force 为 true 时跳过存在性检查,强制推送
|
||||||
func InstallServiceApk(addr, serial, localPath string, force bool) error {
|
func InstallServiceApk(addr, serial string, force bool) error {
|
||||||
if localPath == "" {
|
|
||||||
localPath = DefaultApkLocal
|
|
||||||
}
|
|
||||||
|
|
||||||
abs, err := filepath.Abs(localPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("解析 APK 路径失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查本地文件是否存在
|
|
||||||
if _, err := os.Stat(abs); os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("本地 APK 文件不存在: %s", abs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 非强制模式下,检查设备端文件是否已存在
|
// 非强制模式下,检查设备端文件是否已存在
|
||||||
if !force && fileExistsOnDevice(addr, serial, DefaultApkRemote) {
|
if !force && fileExistsOnDevice(addr, serial, DefaultApkRemote) {
|
||||||
// 文件已存在,跳过推送,但仍需确保已安装
|
// 文件已存在,跳过推送,但仍需确保已安装
|
||||||
} else {
|
} else {
|
||||||
_, err = adb.PushFile(addr, serial, abs, DefaultApkRemote, 0644, false)
|
_, err := adb.PushData(addr, serial, assets.ApkData, DefaultApkRemote, 0644, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("推送 APK 失败: %w", err)
|
return fmt.Errorf("推送 APK 失败: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在设备上安装 APK(覆盖安装)
|
// 在设备上安装 APK(覆盖安装)
|
||||||
_, err = adb.InstallApkOnDevice(addr, serial, DefaultApkRemote, "-r", false)
|
_, err := adb.InstallApkOnDevice(addr, serial, DefaultApkRemote, "-r", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("安装 APK 失败: %w", err)
|
return fmt.Errorf("安装 APK 失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user