move ghdc to pkg

This commit is contained in:
lilong.129
2025-03-05 21:40:47 +08:00
parent b5fffdf548
commit 1e1f8d215d
18 changed files with 3268 additions and 3 deletions

1
.gitignore vendored
View File

@@ -2,7 +2,6 @@
*.exe
*.exe~
*.dll
*.so
*.dylib
*.apk

View File

@@ -1 +1 @@
v5.0.0-beta-2503052133
v5.0.0-beta-2503052140

Submodule pkg/ghdc deleted from ecb76cf5bd

86
pkg/ghdc/README.md Normal file
View File

@@ -0,0 +1,86 @@
# ghdc
ghdc 是一个用于与鸿蒙设备进行交互的工具,封装了各种 HDC鸿蒙的 ADB命令和 UI 自动化能力。
## 目录结构
ghdc \
├── client.go       封装 hdc list targets 等非设备命令 \
├── device.go     封装 hdc -t connectkey shell 等指定设备的命令 \
└── uidevice.go 封装设备所有自动化能力
## hdc 命令调用
目前支持的能力:
- 获取设备
- 文件传输
- shell 命令
- 端口挂载
- 属性获取brand, model, 版本等)
- 截图
`hdc`与鸿蒙的关系和`adb`与安卓关系一致,其架构与 adb server 相同,分为 client 和 server。`hdc start` 会启动一个 hdc server监听本地的 8710 端口。我们也是和该 8710 端口通信执行 HDC 命令。目前支持常用的 hdc 能力,并可扩展至所有 hdc 能力。
```shell
hdc -m -s ::ffff:127.0.0.1:8710
```
hdc 命令分为两类:
- 不指定设备的命令:例如 `hdc list targets`。逻辑封装在 client.go 中。与 gadb 类似,所有的命令通过 RPC 方式与 hdc server 直接通信。
- 指定设备的命令:例如 `hdc -t connectkey shell`。逻辑封装在 device.go 中。这些命令会在与 server 建连时指定执行的设备。
## UI Test 自动化能力
目前支持的能力:
- 点击\滑动\输入
- 按键操作
- 手势操作
- TouchDown/TouchMove/TouchUp 屏幕操作
- 屏幕旋转
- 音量设置
- 图片流获取
- 控件信息监听
- 简单控件操作
Harmony Next 内置了 UI Test 服务,提供了所有常用的自动化能力。并且这个服务也部分开源,支持二次开发。由于协议未开源,我们通过逆向工程绕过 JS API直接通过 socket 与 UI Test 服务进行通信,操作手机。代码能力封装在 uidevice.go 中。
UI Test 协议分为两类:
- 无 session 单次返回长连接:例如点击、滑动等,发出命令即可,返回一个执行成功或失败。
- 有 session 多次返回长连接:例如获取屏幕图片流、监听控件信息变化等。
## 使用方法
```go
package main
import (
"github.com/httprunner/httprunner/v5/pkg/ghdc"
"log"
)
func main() {
client, err := ghdc.NewClient()
checkErr(err, "fail to connect hdc server")
devices, err := client.DeviceList()
checkErr(err)
if len(devices) == 0 {
log.Fatalln("list of devices is empty")
}
dev := devices[0]
driver, err := ghdc.NewUIDriver(dev)
checkErr(err, "fail to init device uiDriver")
err = driver.Touch(225, 1715)
checkErr(err)
}
func checkErr(err error, msg ...string) {
if err == nil {
return
}
var output string
if len(msg) != 0 {
output = msg[0] + " "
}
output += err.Error()
log.Fatalln(output)
}
```

BIN
pkg/ghdc/agent.so Normal file

Binary file not shown.

166
pkg/ghdc/client.go Normal file
View File

@@ -0,0 +1,166 @@
package ghdc
import (
"fmt"
"log"
"strings"
)
const HdcServerPort = 8710
type Client struct {
host string
port int
}
func NewClient() (Client, error) {
return NewClientWith("localhost")
}
func NewClientWith(host string, port ...int) (hdClient Client, err error) {
if len(port) == 0 {
port = []int{HdcServerPort}
}
hdClient.host = host
hdClient.port = port[0]
var tp transport
if tp, err = hdClient.createTransport(); err != nil {
return Client{}, err
}
defer func() { _ = tp.Close() }()
return
}
func (c Client) ServerVersion() (version string, err error) {
return c.executeCommand("version")
}
func (c Client) DeviceSerialList() (serials []string, err error) {
var resp string
if resp, err = c.executeCommand("list targets"); err != nil {
return
}
lines := strings.Split(resp, "\n")
serials = make([]string, 0, len(lines))
for i := range lines {
if lines[i] == "" {
continue
}
fields := strings.Fields(lines[i])
serials = append(serials, fields[0])
}
return
}
func (c Client) DeviceList() (devices []Device, err error) {
var resp string
if resp, err = c.executeCommand("list targets -v"); err != nil {
return
}
lines := strings.Split(resp, "\n")
devices = make([]Device, 0, len(lines))
for i := range lines {
line := strings.TrimSpace(lines[i])
if line == "" {
continue
}
fields := strings.Fields(line)
if len(fields) < 4 || len(fields[0]) == 0 {
debugLog(fmt.Sprintf("can't parse: %s", line))
continue
}
if fields[2] == "Offline" {
log.Printf("device [%s] Offline ", fields[0])
continue
}
if fields[1] == "UART" {
continue
}
slickAttrs := make(map[string]string)
slickAttrs["usb"] = fields[1]
slickAttrs["device_status"] = fields[2]
device, err := NewDevice(c, fields[0], slickAttrs)
if err != nil {
return nil, err
}
devices = append(devices, device)
}
return
}
func (c Client) ForwardList() (deviceForward []DeviceForward, err error) {
var resp string
if resp, err = c.executeCommand("fport ls"); err != nil {
return nil, err
}
lines := strings.Split(resp, "\n")
deviceForward = make([]DeviceForward, 0, len(lines))
for i := range lines {
line := strings.TrimSpace(lines[i])
line = strings.ReplaceAll(line, "'", "")
if line == "" {
continue
}
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
deviceForward = append(deviceForward, DeviceForward{Local: fields[0], Remote: fields[1]})
}
return
}
func (c Client) Connect(ip string, port ...int) (err error) {
if len(port) == 0 {
port = []int{HdcServerPort}
}
var resp string
if resp, err = c.executeCommand(fmt.Sprintf("tconn %s:%d", ip, port[0])); err != nil {
return err
}
if !strings.HasPrefix(resp, "connected to") && !strings.HasPrefix(resp, "already connected to") {
return fmt.Errorf("hd connect: %s", resp)
}
return
}
func (c Client) KillServer() (err error) {
var tp transport
if tp, err = c.createTransport(); err != nil {
return err
}
defer func() { _ = tp.Close() }()
err = tp.SendCommand("kill")
return
}
func (c Client) createTransport() (tp transport, err error) {
return newTransport(fmt.Sprintf("%s:%d", c.host, c.port), false, "")
}
func (c Client) executeCommand(command string) (resp string, err error) {
var tp transport
if tp, err = c.createTransport(); err != nil {
return "", err
}
defer func() { _ = tp.Close() }()
if err = tp.SendCommand(command); err != nil {
return "", err
}
return tp.ReadStringAll()
}

103
pkg/ghdc/client_test.go Normal file
View File

@@ -0,0 +1,103 @@
package ghdc
import (
"testing"
)
func TestClient_ServerVersion(t *testing.T) {
SetDebug(true)
hdcClient, err := NewClient()
if err != nil {
t.Fatal(err)
}
hdServerVersion, err := hdcClient.ServerVersion()
if err != nil {
t.Fatal(err)
}
t.Log(hdServerVersion)
}
func TestClient_DeviceSerialList(t *testing.T) {
SetDebug(true)
hdcClient, err := NewClient()
if err != nil {
t.Fatal(err)
}
serials, err := hdcClient.DeviceSerialList()
if err != nil {
t.Fatal(err)
}
for i := range serials {
t.Log(serials[i])
}
}
func TestClient_DeviceList(t *testing.T) {
SetDebug(true)
hdcClient, err := NewClient()
if err != nil {
t.Fatal(err)
}
devices, err := hdcClient.DeviceList()
if err != nil {
t.Fatal(err)
}
for i := range devices {
t.Log(devices[i].serial, devices[i].DeviceInfo())
}
}
func TestClient_ForwardList(t *testing.T) {
SetDebug(true)
hdcClient, err := NewClient()
if err != nil {
t.Fatal(err)
}
deviceForwardList, err := hdcClient.ForwardList()
if err != nil {
t.Fatal(err)
}
for i := range deviceForwardList {
t.Log(deviceForwardList[i])
}
}
func TestClient_Connect(t *testing.T) {
hdcClient, err := NewClient()
if err != nil {
t.Fatal(err)
}
SetDebug(true)
err = hdcClient.Connect("192.168.1.28")
if err != nil {
t.Fatal(err)
}
}
func TestClient_KillServer(t *testing.T) {
SetDebug(true)
hdcClient, err := NewClient()
if err != nil {
t.Fatal(err)
}
err = hdcClient.KillServer()
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,76 @@
package ghdc
import (
"fmt"
"net"
"sync"
"time"
)
type ConnectionPool struct {
mu sync.Mutex
conns chan net.Conn
host string
port string
maxConns int
}
func newConnectionPool(host string, port string, maxConns int) (*ConnectionPool, error) {
pool := &ConnectionPool{
conns: make(chan net.Conn, maxConns),
host: host,
port: port,
maxConns: maxConns,
}
for i := 0; i < maxConns; i++ {
conn, err := pool.newConnection()
if err != nil {
return nil, err
}
pool.conns <- conn
}
return pool, nil
}
func (p *ConnectionPool) newConnection() (net.Conn, error) {
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", p.host, p.port), 5*time.Second)
if err != nil {
return nil, err
}
return conn, nil
}
func (p *ConnectionPool) getConnection() (net.Conn, error) {
select {
case conn := <-p.conns:
return conn, nil
default:
// 如果池中没有可用连接,创建一个新的连接
return p.newConnection()
}
}
func (p *ConnectionPool) releaseConnection(conn net.Conn) {
p.mu.Lock()
defer p.mu.Unlock()
select {
case p.conns <- conn:
// 放回连接池成功
default:
// 连接池已满,关闭连接
conn.Close()
}
}
func (p *ConnectionPool) close() {
p.mu.Lock()
defer p.mu.Unlock()
close(p.conns)
for conn := range p.conns {
conn.Close()
}
}

618
pkg/ghdc/device.go Normal file
View File

@@ -0,0 +1,618 @@
package ghdc
import (
"errors"
"fmt"
"net"
"os"
"strings"
"time"
)
type DeviceFileInfo struct {
Name string
Mode os.FileMode
Size uint32
LastModified time.Time
}
func (info DeviceFileInfo) IsDir() bool {
return (info.Mode & (1 << 14)) == (1 << 14)
}
type DeviceState string
const (
StateOnline DeviceState = "Online"
StateOffline DeviceState = "Offline"
)
var deviceStateStrings = map[string]DeviceState{
"Offline": StateOffline,
"Connected": StateOnline,
}
type KeyCode int
const (
KEYCODE_FN KeyCode = 0
KEYCODE_UNKNOWN KeyCode = -1
KEYCODE_HOME KeyCode = 1
KEYCODE_BACK KeyCode = 2
KEYCODE_MEDIA_PLAY_PAUSE KeyCode = 10
KEYCODE_MEDIA_STOP KeyCode = 11
KEYCODE_MEDIA_NEXT KeyCode = 12
KEYCODE_MEDIA_PREVIOUS KeyCode = 13
KEYCODE_MEDIA_REWIND KeyCode = 14
KEYCODE_MEDIA_FAST_FORWARD KeyCode = 15
KEYCODE_VOLUME_UP KeyCode = 16
KEYCODE_VOLUME_DOWN KeyCode = 17
KEYCODE_POWER KeyCode = 18
KEYCODE_CAMERA KeyCode = 19
KEYCODE_VOLUME_MUTE KeyCode = 22
KEYCODE_MUTE KeyCode = 23
KEYCODE_BRIGHTNESS_UP KeyCode = 40
KEYCODE_BRIGHTNESS_DOWN KeyCode = 41
KEYCODE_NUM_0 KeyCode = 2000
KEYCODE_NUM_1 KeyCode = 2001
KEYCODE_NUM_2 KeyCode = 2002
KEYCODE_NUM_3 KeyCode = 2003
KEYCODE_NUM_4 KeyCode = 2004
KEYCODE_NUM_5 KeyCode = 2005
KEYCODE_NUM_6 KeyCode = 2006
KEYCODE_NUM_7 KeyCode = 2007
KEYCODE_NUM_8 KeyCode = 2008
KEYCODE_NUM_9 KeyCode = 2009
KEYCODE_STAR KeyCode = 2010
KEYCODE_POUND KeyCode = 2011
KEYCODE_DPAD_UP KeyCode = 2012
KEYCODE_DPAD_DOWN KeyCode = 2013
KEYCODE_DPAD_LEFT KeyCode = 2014
KEYCODE_DPAD_RIGHT KeyCode = 2015
KEYCODE_DPAD_CENTER KeyCode = 2016
KEYCODE_A KeyCode = 2017
KEYCODE_B KeyCode = 2018
KEYCODE_C KeyCode = 2019
KEYCODE_D KeyCode = 2020
KEYCODE_E KeyCode = 2021
KEYCODE_F KeyCode = 2022
KEYCODE_G KeyCode = 2023
KEYCODE_H KeyCode = 2024
KEYCODE_I KeyCode = 2025
KEYCODE_J KeyCode = 2026
KEYCODE_K KeyCode = 2027
KEYCODE_L KeyCode = 2028
KEYCODE_M KeyCode = 2029
KEYCODE_N KeyCode = 2030
KEYCODE_O KeyCode = 2031
KEYCODE_P KeyCode = 2032
KEYCODE_Q KeyCode = 2033
KEYCODE_R KeyCode = 2034
KEYCODE_S KeyCode = 2035
KEYCODE_T KeyCode = 2036
KEYCODE_U KeyCode = 2037
KEYCODE_V KeyCode = 2038
KEYCODE_W KeyCode = 2039
KEYCODE_X KeyCode = 2040
KEYCODE_Y KeyCode = 2041
KEYCODE_Z KeyCode = 2042
KEYCODE_COMMA KeyCode = 2043
KEYCODE_PERIOD KeyCode = 2044
KEYCODE_ALT_LEFT KeyCode = 2045
KEYCODE_ALT_RIGHT KeyCode = 2046
KEYCODE_SHIFT_LEFT KeyCode = 2047
KEYCODE_SHIFT_RIGHT KeyCode = 2048
KEYCODE_TAB KeyCode = 2049
KEYCODE_SPACE KeyCode = 2050
KEYCODE_SYM KeyCode = 2051
KEYCODE_EXPLORER KeyCode = 2052
KEYCODE_ENVELOPE KeyCode = 2053
KEYCODE_ENTER KeyCode = 2054
KEYCODE_DEL KeyCode = 2055
KEYCODE_GRAVE KeyCode = 2056
KEYCODE_MINUS KeyCode = 2057
KEYCODE_EQUALS KeyCode = 2058
KEYCODE_LEFT_BRACKET KeyCode = 2059
KEYCODE_RIGHT_BRACKET KeyCode = 2060
KEYCODE_BACKSLASH KeyCode = 2061
KEYCODE_SEMICOLON KeyCode = 2062
KEYCODE_APOSTROPHE KeyCode = 2063
KEYCODE_SLASH KeyCode = 2064
KEYCODE_AT KeyCode = 2065
KEYCODE_PLUS KeyCode = 2066
KEYCODE_MENU KeyCode = 2067
KEYCODE_PAGE_UP KeyCode = 2068
KEYCODE_PAGE_DOWN KeyCode = 2069
KEYCODE_ESCAPE KeyCode = 2070
KEYCODE_FORWARD_DEL KeyCode = 2071
KEYCODE_CTRL_LEFT KeyCode = 2072
KEYCODE_CTRL_RIGHT KeyCode = 2073
KEYCODE_CAPS_LOCK KeyCode = 2074
KEYCODE_SCROLL_LOCK KeyCode = 2075
KEYCODE_META_LEFT KeyCode = 2076
KEYCODE_META_RIGHT KeyCode = 2077
KEYCODE_FUNCTION KeyCode = 2078
KEYCODE_SYSRQ KeyCode = 2079
KEYCODE_BREAK KeyCode = 2080
KEYCODE_MOVE_HOME KeyCode = 2081
KEYCODE_MOVE_END KeyCode = 2082
KEYCODE_INSERT KeyCode = 2083
KEYCODE_FORWARD KeyCode = 2084
KEYCODE_MEDIA_PLAY KeyCode = 2085
KEYCODE_MEDIA_PAUSE KeyCode = 2086
KEYCODE_MEDIA_CLOSE KeyCode = 2087
KEYCODE_MEDIA_EJECT KeyCode = 2088
KEYCODE_MEDIA_RECORD KeyCode = 2089
KEYCODE_F1 KeyCode = 2090
KEYCODE_F2 KeyCode = 2091
KEYCODE_F3 KeyCode = 2092
KEYCODE_F4 KeyCode = 2093
KEYCODE_F5 KeyCode = 2094
KEYCODE_F6 KeyCode = 2095
KEYCODE_F7 KeyCode = 2096
KEYCODE_F8 KeyCode = 2097
KEYCODE_F9 KeyCode = 2098
KEYCODE_F10 KeyCode = 2099
KEYCODE_F11 KeyCode = 2100
KEYCODE_F12 KeyCode = 2101
KEYCODE_NUM_LOCK KeyCode = 2102
KEYCODE_NUMPAD_0 KeyCode = 2103
KEYCODE_NUMPAD_1 KeyCode = 2104
KEYCODE_NUMPAD_2 KeyCode = 2105
KEYCODE_NUMPAD_3 KeyCode = 2106
KEYCODE_NUMPAD_4 KeyCode = 2107
KEYCODE_NUMPAD_5 KeyCode = 2108
KEYCODE_NUMPAD_6 KeyCode = 2109
KEYCODE_NUMPAD_7 KeyCode = 2110
KEYCODE_NUMPAD_8 KeyCode = 2111
KEYCODE_NUMPAD_9 KeyCode = 2112
KEYCODE_NUMPAD_DIVIDE KeyCode = 2113
KEYCODE_NUMPAD_MULTIPLY KeyCode = 2114
KEYCODE_NUMPAD_SUBTRACT KeyCode = 2115
KEYCODE_NUMPAD_ADD KeyCode = 2116
KEYCODE_NUMPAD_DOT KeyCode = 2117
KEYCODE_NUMPAD_COMMA KeyCode = 2118
KEYCODE_NUMPAD_ENTER KeyCode = 2119
KEYCODE_NUMPAD_EQUALS KeyCode = 2120
KEYCODE_NUMPAD_LEFT_PAREN KeyCode = 2121
KEYCODE_NUMPAD_RIGHT_PAREN KeyCode = 2122
KEYCODE_VIRTUAL_MULTITASK KeyCode = 2210
KEYCODE_SLEEP KeyCode = 2600
KEYCODE_ZENKAKU_HANKAKU KeyCode = 2601
KEYCODE_ND KeyCode = 2602
KEYCODE_RO KeyCode = 2603
KEYCODE_KATAKANA KeyCode = 2604
KEYCODE_HIRAGANA KeyCode = 2605
KEYCODE_HENKAN KeyCode = 2606
KEYCODE_KATAKANA_HIRAGANA KeyCode = 2607
KEYCODE_MUHENKAN KeyCode = 2608
KEYCODE_LINEFEED KeyCode = 2609
KEYCODE_MACRO KeyCode = 2610
KEYCODE_NUMPAD_PLUSMINUS KeyCode = 2611
KEYCODE_SCALE KeyCode = 2612
KEYCODE_HANGUEL KeyCode = 2613
KEYCODE_HANJA KeyCode = 2614
KEYCODE_YEN KeyCode = 2615
KEYCODE_STOP KeyCode = 2616
KEYCODE_AGAIN KeyCode = 2617
KEYCODE_PROPS KeyCode = 2618
KEYCODE_UNDO KeyCode = 2619
KEYCODE_COPY KeyCode = 2620
KEYCODE_OPEN KeyCode = 2621
KEYCODE_PASTE KeyCode = 2622
KEYCODE_FIND KeyCode = 2623
KEYCODE_CUT KeyCode = 2624
KEYCODE_HELP KeyCode = 2625
KEYCODE_CALC KeyCode = 2626
KEYCODE_FILE KeyCode = 2627
KEYCODE_BOOKMARKS KeyCode = 2628
KEYCODE_NEXT KeyCode = 2629
KEYCODE_PLAYPAUSE KeyCode = 2630
KEYCODE_PREVIOUS KeyCode = 2631
KEYCODE_STOPCD KeyCode = 2632
KEYCODE_CONFIG KeyCode = 2634
KEYCODE_REFRESH KeyCode = 2635
KEYCODE_EXIT KeyCode = 2636
KEYCODE_EDIT KeyCode = 2637
KEYCODE_SCROLLUP KeyCode = 2638
KEYCODE_SCROLLDOWN KeyCode = 2639
KEYCODE_NEW KeyCode = 2640
KEYCODE_REDO KeyCode = 2641
KEYCODE_CLOSE KeyCode = 2642
KEYCODE_PLAY KeyCode = 2643
KEYCODE_BASSBOOST KeyCode = 2644
KEYCODE_PRINT KeyCode = 2645
KEYCODE_CHAT KeyCode = 2646
KEYCODE_FINANCE KeyCode = 2647
KEYCODE_CANCEL KeyCode = 2648
KEYCODE_KBDILLUM_TOGGLE KeyCode = 2649
KEYCODE_KBDILLUM_DOWN KeyCode = 2650
KEYCODE_KBDILLUM_UP KeyCode = 2651
KEYCODE_SEND KeyCode = 2652
KEYCODE_REPLY KeyCode = 2653
KEYCODE_FORWARDMAIL KeyCode = 2654
KEYCODE_SAVE KeyCode = 2655
KEYCODE_DOCUMENTS KeyCode = 2656
KEYCODE_VIDEO_NEXT KeyCode = 2657
KEYCODE_VIDEO_PREV KeyCode = 2658
KEYCODE_BRIGHTNESS_CYCLE KeyCode = 2659
KEYCODE_BRIGHTNESS_ZERO KeyCode = 2660
KEYCODE_DISPLAY_OFF KeyCode = 2661
KEYCODE_BTN_MISC KeyCode = 2662
KEYCODE_GOTO KeyCode = 2663
KEYCODE_INFO KeyCode = 2664
KEYCODE_PROGRAM KeyCode = 2665
KEYCODE_PVR KeyCode = 2666
KEYCODE_SUBTITLE KeyCode = 2667
KEYCODE_FULL_SCREEN KeyCode = 2668
KEYCODE_KEYBOARD KeyCode = 2669
KEYCODE_ASPECT_RATIO KeyCode = 2670
KEYCODE_PC KeyCode = 2671
KEYCODE_TV KeyCode = 2672
KEYCODE_TV2 KeyCode = 2673
KEYCODE_VCR KeyCode = 2674
KEYCODE_VCR2 KeyCode = 2675
KEYCODE_SAT KeyCode = 2676
KEYCODE_CD KeyCode = 2677
KEYCODE_TAPE KeyCode = 2678
KEYCODE_TUNER KeyCode = 2679
KEYCODE_PLAYER KeyCode = 2680
KEYCODE_DVD KeyCode = 2681
KEYCODE_AUDIO KeyCode = 2682
KEYCODE_VIDEO KeyCode = 2683
KEYCODE_MEMO KeyCode = 2684
KEYCODE_CALENDAR KeyCode = 2685
KEYCODE_RED KeyCode = 2686
KEYCODE_GREEN KeyCode = 2687
KEYCODE_YELLOW KeyCode = 2688
KEYCODE_BLUE KeyCode = 2689
KEYCODE_CHANNELUP KeyCode = 2690
KEYCODE_CHANNELDOWN KeyCode = 2691
KEYCODE_LAST KeyCode = 2692
KEYCODE_RESTART KeyCode = 2693
KEYCODE_SLOW KeyCode = 2694
KEYCODE_SHUFFLE KeyCode = 2695
KEYCODE_VIDEOPHONE KeyCode = 2696
KEYCODE_GAMES KeyCode = 2697
KEYCODE_ZOOMIN KeyCode = 2698
KEYCODE_ZOOMOUT KeyCode = 2699
KEYCODE_ZOOMRESET KeyCode = 2700
KEYCODE_WORDPROCESSOR KeyCode = 2701
KEYCODE_EDITOR KeyCode = 2702
KEYCODE_SPREADSHEET KeyCode = 2703
KEYCODE_GRAPHICSEDITOR KeyCode = 2704
KEYCODE_PRESENTATION KeyCode = 2705
KEYCODE_DATABASE KeyCode = 2706
KEYCODE_NEWS KeyCode = 2707
KEYCODE_VOICEMAIL KeyCode = 2708
KEYCODE_ADDRESSBOOK KeyCode = 2709
KEYCODE_MESSENGER KeyCode = 2710
KEYCODE_BRIGHTNESS_TOGGLE KeyCode = 2711
KEYCODE_SPELLCHECK KeyCode = 2712
KEYCODE_COFFEE KeyCode = 2713
KEYCODE_MEDIA_REPEAT KeyCode = 2714
KEYCODE_IMAGES KeyCode = 2715
KEYCODE_BUTTONCONFIG KeyCode = 2716
KEYCODE_TASKMANAGER KeyCode = 2717
KEYCODE_JOURNAL KeyCode = 2718
KEYCODE_CONTROLPANEL KeyCode = 2719
KEYCODE_APPSELECT KeyCode = 2720
KEYCODE_SCREENSAVER KeyCode = 2721
KEYCODE_ASSISTANT KeyCode = 2722
KEYCODE_KBD_LAYOUT_NEXT KeyCode = 2723
KEYCODE_BRIGHTNESS_MIN KeyCode = 2724
KEYCODE_BRIGHTNESS_MAX KeyCode = 2725
KEYCODE_KBDINPUTASSIST_PREV KeyCode = 2726
KEYCODE_KBDINPUTASSIST_NEXT KeyCode = 2727
KEYCODE_KBDINPUTASSIST_PREVGROUP KeyCode = 2728
KEYCODE_KBDINPUTASSIST_NEXTGROUP KeyCode = 2729
KEYCODE_KBDINPUTASSIST_ACCEPT KeyCode = 2730
KEYCODE_KBDINPUTASSIST_CANCEL KeyCode = 2731
KEYCODE_FRONT KeyCode = 2800
KEYCODE_SETUP KeyCode = 2801
KEYCODE_WAKE_UP KeyCode = 2802
KEYCODE_SENDFILE KeyCode = 2803
KEYCODE_DELETEFILE KeyCode = 2804
KEYCODE_XFER KeyCode = 2805
KEYCODE_PROG1 KeyCode = 2806
KEYCODE_PROG2 KeyCode = 2807
KEYCODE_MSDOS KeyCode = 2808
KEYCODE_SCREENLOCK KeyCode = 2809
KEYCODE_DIRECTION_ROTATE_DISPLAY KeyCode = 2810
KEYCODE_CYCLEWINDOWS KeyCode = 2811
KEYCODE_COMPUTER KeyCode = 2812
KEYCODE_EJECTCLOSECD KeyCode = 2813
KEYCODE_ISO KeyCode = 2814
KEYCODE_MOVE KeyCode = 2815
KEYCODE_F13 KeyCode = 2816
KEYCODE_F14 KeyCode = 2817
KEYCODE_F15 KeyCode = 2818
KEYCODE_F16 KeyCode = 2819
KEYCODE_F17 KeyCode = 2820
KEYCODE_F18 KeyCode = 2821
KEYCODE_F19 KeyCode = 2822
KEYCODE_F20 KeyCode = 2823
KEYCODE_F21 KeyCode = 2824
KEYCODE_F22 KeyCode = 2825
KEYCODE_F23 KeyCode = 2826
KEYCODE_F24 KeyCode = 2827
KEYCODE_PROG3 KeyCode = 2828
KEYCODE_PROG4 KeyCode = 2829
KEYCODE_DASHBOARD KeyCode = 2830
KEYCODE_SUSPEND KeyCode = 2831
KEYCODE_HP KeyCode = 2832
KEYCODE_SOUND KeyCode = 2833
KEYCODE_QUESTION KeyCode = 2834
KEYCODE_CONNECT KeyCode = 2836
KEYCODE_SPORT KeyCode = 2837
KEYCODE_SHOP KeyCode = 2838
KEYCODE_ALTERASE KeyCode = 2839
KEYCODE_SWITCHVIDEOMODE KeyCode = 2841
KEYCODE_BATTERY KeyCode = 2842
KEYCODE_BLUETOOTH KeyCode = 2843
KEYCODE_WLAN KeyCode = 2844
KEYCODE_UWB KeyCode = 2845
KEYCODE_WWAN_WIMAX KeyCode = 2846
KEYCODE_RFKILL KeyCode = 2847
KEYCODE_CHANNEL KeyCode = 3001
KEYCODE_BTN_0 KeyCode = 3100
KEYCODE_BTN_1 KeyCode = 3101
KEYCODE_BTN_2 KeyCode = 3102
KEYCODE_BTN_3 KeyCode = 3103
KEYCODE_BTN_4 KeyCode = 3104
KEYCODE_BTN_5 KeyCode = 3105
KEYCODE_BTN_6 KeyCode = 3106
KEYCODE_BTN_7 KeyCode = 3107
KEYCODE_BTN_8 KeyCode = 3108
KEYCODE_BTN_9 KeyCode = 3109
)
type DeviceForward struct {
Local string
Remote string
}
type Device struct {
hdClient Client
serial string
attrs map[string]string
}
func NewDevice(hdClient Client, serial string, attrs map[string]string) (Device, error) {
device := Device{hdClient: hdClient, serial: serial, attrs: attrs}
model, err := device.RunShellCommand("param get const.product.model")
if err != nil {
return device, err
}
attrs["model"] = model
brand, err := device.RunShellCommand("param get const.product.brand")
if err != nil {
return device, err
}
attrs["brand"] = brand
sdkVersion, err := device.RunShellCommand("param get const.product.software.version")
if err != nil {
return device, err
}
attrs["sdkVersion"] = sdkVersion
osVersion, err := device.RunShellCommand("param get const.ohos.apiversion")
if err != nil {
return device, err
}
attrs["osVersion"] = osVersion
cpu, err := device.RunShellCommand("param get const.product.cpu.abilist")
if err != nil {
return device, err
}
attrs["cpu"] = cpu
product, err := device.RunShellCommand("param get const.product.name")
if err != nil {
return device, err
}
attrs["product"] = product
_, err = device.RunShellCommand("setenforce 1")
if err != nil {
return device, err
}
return device, nil
}
func (d Device) HasAttribute(key string) bool {
_, ok := d.attrs[key]
return ok
}
func (d Device) Product() (string, error) {
if d.HasAttribute("product") {
return d.attrs["product"], nil
}
return "", errors.New("does not have attribute: product")
}
func (d Device) Model() (string, error) {
if d.HasAttribute("model") {
return d.attrs["model"], nil
}
return "", errors.New("does not have attribute: model")
}
func (d Device) Usb() (string, error) {
if d.HasAttribute("usb") {
return d.attrs["usb"], nil
}
return "", errors.New("does not have attribute: usb")
}
func (d Device) DeviceInfo() map[string]string {
return d.attrs
}
func (d Device) Serial() string {
return d.serial
}
func (d Device) IsUsb() (bool, error) {
usb, err := d.Usb()
if err != nil {
return false, err
}
return usb != "", nil
}
func (d Device) Screenshot(localPath string) error {
tmpPath := fmt.Sprintf("/data/local/tmp/hypium_tmp_shot_%d.jpeg", time.Now().Unix())
_, err := d.RunShellCommand("snapshot_display", "-f", tmpPath)
if err != nil {
err = fmt.Errorf("failed to take screencap \n%v", err)
return err
}
err = d.PullFile(tmpPath, localPath)
if err != nil {
return err
}
_, _ = d.RunShellCommand("rm", "-rf", "tmpPath")
return nil
}
func (d Device) Install(localPath string) error {
res, err := d.ExecuteCommand(fmt.Sprintf("install -r %s", localPath))
if err != nil || !strings.Contains(res, "success") {
err = fmt.Errorf("failed to install %s %v, Msg: %s", localPath, err, res)
return err
}
return nil
}
func (d Device) Forward(remotePort int) (localPort int, err error) {
remote := fmt.Sprintf("tcp:%d", remotePort)
localPort, err = GetFreePort()
if err != nil {
err = fmt.Errorf("failed to get free port \n%v", err)
return
}
command := ""
local := fmt.Sprintf("tcp:%d", localPort)
command = fmt.Sprintf("fport %s %s", local, remote)
_, err = d.ExecuteCommand(command)
return
}
func (d Device) ForwardKill(localPort int) (err error) {
local := fmt.Sprintf("tcp:%d", localPort)
_, err = d.hdClient.executeCommand(fmt.Sprintf("-t %s fport rm %s", d.serial, local))
return
}
func (d Device) RunShellCommand(cmd string, args ...string) (string, error) {
if len(args) > 0 {
cmd = fmt.Sprintf("%s %s", cmd, strings.Join(args, " "))
}
if strings.TrimSpace(cmd) == "" {
return "", errors.New("hd shell: command cannot be empty")
}
return d.ExecuteCommand(fmt.Sprintf("shell %s", cmd))
}
func (d Device) createDeviceTransport() (tp transport, err error) {
return newTransport(fmt.Sprintf("%s:%d", d.hdClient.host, d.hdClient.port), false, d.serial)
}
func (d Device) createDeviceAliveTransport() (tp transport, err error) {
return newTransport(fmt.Sprintf("%s:%d", d.hdClient.host, d.hdClient.port), true, d.serial)
}
func (d Device) createUitestTransport() (uTp uitestTransport, err error) {
port, err := d.Forward(8012)
if err != nil {
err = fmt.Errorf("failed to forward uitest port \n%v", err)
return
}
return newUitestTransport(d.hdClient.host, fmt.Sprintf("%d", port))
}
func (d Device) createUitestKitTransport() (uTp uitestKitTransport, err error) {
port, err := d.Forward(8012)
if err != nil {
err = fmt.Errorf("failed to forward uitest port \n%v", err)
return
}
return newUitestKitTransport(d.serial, d.hdClient.host, fmt.Sprintf("%d", port))
}
func (d Device) ExecuteCommand(command string) (resp string, err error) {
var tp transport
if tp, err = d.createDeviceTransport(); err != nil {
return "", err
}
defer func() { _ = tp.Close() }()
time.Sleep(1 * time.Millisecond)
if err = tp.SendCommand(command); err != nil {
return "", err
}
resp, err = tp.ReadStringAll()
if err != nil {
return
}
if strings.Contains(resp, "[Fail]") {
return resp, fmt.Errorf("failed to execute command 「%s」 \nerror: %s", command, resp)
}
return resp, nil
}
func (d Device) PushFile(localPath string, remotePath string) (err error) {
var tp transport
if tp, err = d.createDeviceTransport(); err != nil {
return err
}
defer func() { _ = tp.Close() }()
if err = tp.SendCommand(fmt.Sprintf("file send %s %s", localPath, remotePath)); err != nil {
return err
}
_, err = tp.ReadAll()
return nil
}
func (d Device) PullFile(remotePath string, localPath string) (err error) {
var tp transport
if tp, err = d.createDeviceTransport(); err != nil {
return err
}
defer func() { _ = tp.Close() }()
if err = tp.SendCommand(fmt.Sprintf("file recv %s %s", remotePath, localPath)); err != nil {
return err
}
res, err := tp.ReadStringAll()
if err == nil {
if strings.Contains(res, "Fail") {
return fmt.Errorf("failed to pull: msg: %s", res)
}
}
return nil
}
func GetFreePort() (int, error) {
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
if err != nil {
return 0, fmt.Errorf("resolve tcp addr failed \n%v", err)
}
l, err := net.ListenTCP("tcp", addr)
if err != nil {
return 0, fmt.Errorf("listen tcp addr failed \n%v", err)
}
defer func() {
_ = l.Close()
}()
return l.Addr().(*net.TCPAddr).Port, nil
}

253
pkg/ghdc/device_test.go Normal file
View File

@@ -0,0 +1,253 @@
package ghdc
import (
"fmt"
"testing"
)
func TestDevice_Product(t *testing.T) {
hdClient, err := NewClient()
if err != nil {
t.Fatal(err)
}
devices, err := hdClient.DeviceList()
if err != nil {
t.Fatal(err)
}
for i := range devices {
dev := devices[i]
product, err := dev.Product()
if err != nil {
t.Fatal(err)
}
t.Log(dev.Serial(), product)
}
}
func TestDevice_Model(t *testing.T) {
hdClient, err := NewClient()
if err != nil {
t.Fatal(err)
}
devices, err := hdClient.DeviceList()
if err != nil {
t.Fatal(err)
}
for i := range devices {
dev := devices[i]
model, err := dev.Model()
if err != nil {
t.Fatal(err)
}
t.Log(dev.Serial(), model)
}
}
func TestDevice_Usb(t *testing.T) {
hdClient, err := NewClient()
if err != nil {
t.Fatal(err)
}
devices, err := hdClient.DeviceList()
if err != nil {
t.Fatal(err)
}
for i := range devices {
dev := devices[i]
usb, err := dev.Usb()
if err != nil {
t.Fatal(err)
}
isUsb, err := dev.IsUsb()
if err != nil {
t.Fatal(err)
}
t.Log(dev.Serial(), usb, isUsb)
}
}
func TestDevice_DeviceInfo(t *testing.T) {
hdClient, err := NewClient()
if err != nil {
t.Fatal(err)
}
devices, err := hdClient.DeviceList()
if err != nil {
t.Fatal(err)
}
for i := range devices {
dev := devices[i]
t.Log(dev.DeviceInfo())
}
}
func TestDevice_Forward(t *testing.T) {
hdClient, err := NewClient()
if err != nil {
t.Fatal(err)
}
devices, err := hdClient.DeviceList()
if err != nil {
t.Fatal(err)
}
if len(devices) == 0 {
t.Fatal("not found available device")
}
SetDebug(true)
localPort := 61000
localPort, err = devices[0].Forward(6790)
t.Log(fmt.Sprintf("forward local port %d \n", localPort))
if err != nil {
t.Fatal(err)
}
err = devices[0].ForwardKill(localPort)
if err != nil {
t.Fatal(err)
}
}
func TestDevice_ForwardKill(t *testing.T) {
hdClient, err := NewClient()
if err != nil {
t.Fatal(err)
}
devices, err := hdClient.DeviceList()
if err != nil {
t.Fatal(err)
}
if len(devices) == 0 {
t.Fatal("not found available device")
}
SetDebug(true)
err = devices[0].ForwardKill(6790)
if err != nil {
t.Fatal(err)
}
}
func TestDevice_RunShellCommand(t *testing.T) {
hdClient, err := NewClient()
if err != nil {
t.Fatal(err)
}
devices, err := hdClient.DeviceList()
if err != nil {
t.Fatal(err)
}
if len(devices) == 0 {
t.Fatal("not found available device")
}
dev := devices[0]
cmdOutput, err := dev.RunShellCommand("pwd")
if err != nil {
t.Fatal(dev.serial, err)
}
t.Log("\n⬇"+dev.serial+"⬇️\n", cmdOutput)
}
func TestDevice_Push(t *testing.T) {
hdClient, err := NewClient()
if err != nil {
t.Fatal(err)
}
devices, err := hdClient.DeviceList()
if err != nil {
t.Fatal(err)
}
if len(devices) == 0 {
t.Fatal("not found available device")
}
dev := devices[0]
SetDebug(true)
err = dev.PushFile("/tmp/test.txt", "/data/local/tmp/push.txt")
if err != nil {
t.Fatal(err)
}
}
func TestDevice_Pull(t *testing.T) {
hdClient, err := NewClient()
if err != nil {
t.Fatal(err)
}
devices, err := hdClient.DeviceList()
if err != nil {
t.Fatal(err)
}
if len(devices) == 0 {
t.Fatal("not found available device")
}
dev := devices[0]
SetDebug(true)
err = dev.PullFile("/data/local/tmp/push.txt", "/tmp/test2.txt")
if err != nil {
t.Fatal(err)
}
}
func TestDevice_Screenshot(t *testing.T) {
hdClient, err := NewClient()
if err != nil {
t.Fatal(err)
}
devices, err := hdClient.DeviceList()
if err != nil {
t.Fatal(err)
}
if len(devices) == 0 {
t.Fatal("not found available device")
}
dev := devices[0]
SetDebug(true)
err = dev.Screenshot("/tmp/test.jpeg")
if err != nil {
t.Fatal(err)
}
}
func TestDevice_GetSoVersion(t *testing.T) {
hdClient, err := NewClient()
if err != nil {
t.Fatal(err)
}
devices, err := hdClient.DeviceList()
if err != nil {
t.Fatal(err)
}
if len(devices) == 0 {
t.Fatal("not found available device")
}
dev := devices[0]
SetDebug(true)
res, err := dev.RunShellCommand("cat data/local/tmp/agent.so |grep -a UITEST_AGENT_LIBRARY")
if err != nil {
t.Fatal(err)
}
t.Log(res)
}

17
pkg/ghdc/ghdc.go Normal file
View File

@@ -0,0 +1,17 @@
package ghdc
import "log"
var debugFlag = false
// SetDebug set debug mode
func SetDebug(debug bool) {
debugFlag = debug
}
func debugLog(msg string) {
if !debugFlag {
return
}
log.Println("[DEBUG] [ghd] " + msg)
}

View File

@@ -0,0 +1 @@
4.1.4.0

236
pkg/ghdc/transport.go Normal file
View File

@@ -0,0 +1,236 @@
package ghdc
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"strconv"
"time"
)
var ErrConnBroken = errors.New("socket connection broken")
var DefaultReadTimeout time.Duration = 60 * time.Second
var DATA_UNIT_LENGTH = 4
type transport struct {
sock net.Conn
readTimeout time.Duration
connectKey string
}
func newTransport(address string, alive bool, connectKey string, readTimeout ...time.Duration) (tp transport, err error) {
if len(readTimeout) == 0 {
readTimeout = []time.Duration{DefaultReadTimeout}
}
tp.readTimeout = readTimeout[0]
tp.connectKey = connectKey
if tp.sock, err = net.Dial("tcp", address); err != nil {
err = fmt.Errorf("ghdc transport: %w", err)
return
}
if err = _handleShake(tp.sock, connectKey); err != nil {
return
}
if alive {
if err = tp.setAlive(); err != nil {
return
}
}
return
}
func _handleShake(sock net.Conn, connectKey string) (err error) {
data, err := _readN(sock, 48)
if err != nil {
return err
}
if !(data[4] == 79 && data[5] == 72 && data[6] == 79 && data[7] == 83 && data[9] == 72 && data[10] == 68 && data[11] == 67) {
return fmt.Errorf("handle shake error")
}
bannerStr := []byte("OHOS HDC\x00\x00\x00\x00")
connectKeyBytes256 := [256]byte{}
copy(connectKeyBytes256[:], connectKey)
size := 12 + 256
buffer := new(bytes.Buffer)
if err = binary.Write(buffer, binary.BigEndian, uint32(size)); err != nil {
return fmt.Errorf("transport write: %w", err)
}
if err = binary.Write(buffer, binary.BigEndian, bannerStr); err != nil {
return fmt.Errorf("transport write: %w", err)
}
if err = binary.Write(buffer, binary.BigEndian, connectKeyBytes256); err != nil {
return fmt.Errorf("transport write: %w", err)
}
return _send(sock, buffer.Bytes())
}
func (t transport) setAlive() (err error) {
return _sendCommand(t.sock, "alive")
}
func (t transport) SendCommand(command string) (err error) {
return _sendCommand(t.sock, command)
}
func _sendCommand(writer io.Writer, command string) (err error) {
command += "\x00"
buf := new(bytes.Buffer)
if err = binary.Write(buf, binary.BigEndian, uint32(len(command))); err != nil {
return err
}
if err = binary.Write(buf, binary.BigEndian, []byte(command)); err != nil {
return err
}
debugLog(fmt.Sprintf("--> %s", command))
return _send(writer, buf.Bytes())
}
func (t transport) SendBytes(data []byte) (err error) {
length := uint32(len(data))
newData := make([]byte, 4+len(data))
binary.BigEndian.PutUint32(newData[:4], length)
copy(newData[4:], data)
return _send(t.sock, newData)
}
func (t transport) ReadStringAll() (s string, err error) {
var raw []byte
raw, err = _readAll(t.sock)
return string(raw), err
}
func (t transport) ReadAll() (raw []byte, err error) {
return _readAll(t.sock)
}
func _readAll(reader io.Reader) (raw []byte, err error) {
buffer := new(bytes.Buffer)
for true {
lengthBuf := make([]byte, 4)
_, err := io.ReadFull(reader, lengthBuf)
if err != nil {
if err == io.EOF {
return buffer.Bytes(), nil
} else if errors.Is(err, io.ErrUnexpectedEOF) {
err = fmt.Errorf("reached unexpected EOF, read partial data: %s %v", string(buffer.Bytes()), err)
return nil, err
} else {
return nil, err
}
}
length := binary.BigEndian.Uint32(lengthBuf)
data, err := _readN(reader, int(length))
if err != nil {
return nil, err
}
buffer.Write(data)
}
return buffer.Bytes(), nil
}
func (t transport) UnpackString() (s string, err error) {
var raw []byte
raw, err = t.UnpackBytes()
return string(raw), err
}
func (t transport) UnpackBytes() (raw []byte, err error) {
var length string
if length, err = t.ReadStringN(4); err != nil {
return nil, err
}
var size int64
if size, err = strconv.ParseInt(length, 16, 64); err != nil {
return nil, err
}
raw, err = t.RehdytesN(int(size))
debugLog(fmt.Sprintf("\r%s", raw))
return
}
func (t transport) ReadStringN(size int) (s string, err error) {
var raw []byte
if raw, err = t.RehdytesN(size); err != nil {
return "", err
}
return string(raw), nil
}
func (t transport) RehdytesN(size int) (raw []byte, err error) {
_ = t.sock.SetReadDeadline(time.Now().Add(t.readTimeout))
return _readN(t.sock, size)
}
func _readResponse(reader io.Reader) error {
raw, err := _readN(reader, DATA_UNIT_LENGTH)
if err != nil {
return fmt.Errorf("failed to read response length: %w", err)
}
if !bytes.Equal(raw[0:4], []byte("OKAY")) {
fmt.Printf("failed to push file: %s\n", string(raw))
} else {
return nil
}
raw, err = _readAll(reader)
if err != nil {
return fmt.Errorf("failed to read response data: %w", err)
}
return fmt.Errorf("read response error %s", string(raw))
}
func (t transport) Close() (err error) {
if t.sock == nil {
return nil
}
return t.sock.Close()
}
func _send(writer io.Writer, msg []byte) (err error) {
for totalSent := 0; totalSent < len(msg); {
var sent int
if sent, err = writer.Write(msg[totalSent:]); err != nil {
return err
}
if sent == 0 {
return ErrConnBroken
}
totalSent += sent
}
return
}
func _read(reader io.Reader) (data []byte, err error) {
buf := make([]byte, 4*1024*1024)
n, err := reader.Read(buf)
if err != nil {
return nil, err
}
return buf[:n], nil
}
func _readN(reader io.Reader, size int) (raw []byte, err error) {
raw = make([]byte, 0, size)
for len(raw) < size {
buf := make([]byte, size-len(raw))
var n int
if n, err = io.ReadFull(reader, buf); err != nil {
return nil, err
}
if n == 0 {
return nil, ErrConnBroken
}
raw = append(raw, buf...)
}
return
}

927
pkg/ghdc/ui_driver.go Normal file
View File

@@ -0,0 +1,927 @@
package ghdc
import (
"embed"
"encoding/json"
"fmt"
"hash/fnv"
"math"
"os"
"path/filepath"
"strings"
"time"
)
//go:embed agent.so
var agentSO embed.FS
type UitestRequest struct {
Module string `json:"module,omitempty"`
Method string `json:"method,omitempty"`
Params interface{} `json:"params,omitempty"`
RequestId string `json:"request_id,omitempty"`
}
type UitestResponse struct {
Result interface{} `json:"result,omitempty"`
Exception *UitestException `json:"exception,omitempty"`
}
type UitestKitResponse struct {
Result interface{} `json:"result,omitempty"`
Exception string `json:"exception,omitempty"`
}
type UitestException struct {
Message string `json:"message,omitempty"`
Code int `json:"code,omitempty"`
}
type UIDriver struct {
Device
uTp *uitestTransport
uKTp *uitestKitTransport
}
type Dimension struct {
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
}
func NewUIDriver(device Device) (d *UIDriver, err error) {
d = new(UIDriver)
d.Device = device
err = d.prepareDevice()
if err != nil {
err = fmt.Errorf("[uitest] failed to prepare device \n%v", err)
return
}
uTp, err := device.createUitestTransport()
if err != nil {
err = fmt.Errorf("[uitest] failed to create uitest transport \n%v", err)
return
}
uKTp, err := device.createUitestKitTransport()
if err != nil {
err = fmt.Errorf("[uitest] failed to create uitest kit transport \n%v", err)
return
}
d.uTp = &uTp
d.uKTp = &uKTp
return
}
func (d *UIDriver) Close() {
if d.uTp != nil {
d.uTp.Close()
}
if d.uKTp != nil {
d.uKTp.Close()
}
}
func (d *UIDriver) Reconnect() error {
d.Close()
uTp, err := d.createUitestTransport()
if err != nil {
err = fmt.Errorf("[uitest] failed to create uitest transport \n%v", err)
return err
}
uKTp, err := d.createUitestKitTransport()
if err != nil {
err = fmt.Errorf("[uitest] failed to create uitest kit transport \n%v", err)
return err
}
d.uTp = &uTp
d.uKTp = &uKTp
return nil
}
func (d *UIDriver) prepareDevice() error {
uitestPid, err := d.Device.RunShellCommand("pidof uitest")
if err != nil {
return err
}
uitestPid = strings.TrimSpace(uitestPid)
isLowerVersion, err := d.needUpdateLib()
if err != nil {
return err
}
if uitestPid != "" && !isLowerVersion {
return nil
}
_, err = d.Device.RunShellCommand("param set persist.ace.testmode.enabled 1")
if err != nil {
return err
}
if isLowerVersion {
if uitestPid != "" {
_, err = d.Device.RunShellCommand("kill -9 " + uitestPid)
if err != nil {
return err
}
uitestPid = ""
}
err = d.updateLib()
if err != nil {
return err
}
}
if uitestPid == "" {
_, err = d.Device.RunShellCommand("uitest start-daemon singleness")
if err != nil {
return err
}
}
return nil
}
func (d *UIDriver) isServerRunning() bool {
res, err := d.Device.RunShellCommand("top -H -n 1 -p $(pidof uitest)")
if err != nil {
return false
}
if strings.Contains(res, "rpc-") {
return true
}
return false
}
func (d *UIDriver) updateLib() error {
tmpDir := os.TempDir()
soFileName := filepath.Join(tmpDir, "agent.so")
soRaw, err := agentSO.ReadFile("agent.so")
if err != nil {
return err
}
err = os.WriteFile(soFileName, soRaw, os.ModePerm)
if err != nil {
fmt.Println("Error writing file:", err)
return err
}
_, err = d.Device.RunShellCommand("rm /data/tmp/local/agent.so")
if err != nil {
return err
}
err = d.Device.PushFile(soFileName, "/data/local/tmp/agent.so")
if err != nil {
return err
}
return nil
}
func (d *UIDriver) needUpdateLib() (res bool, err error) {
deviceVersionStr, err := d.Device.RunShellCommand("cat data/local/tmp/agent.so |grep -a UITEST_AGENT_LIBRARY")
if err != nil {
return false, err
}
soRaw, err := agentSO.ReadFile("agent.so")
if err != nil {
return false, err
}
// 定义要搜索的字符串
searchString := "UITEST_AGENT_LIBRARY"
// 将二进制内容转换为字符串
content := string(soRaw)
// 按行分割内容
lines := strings.Split(content, "\n")
// 搜索包含特定字符串的行
for _, line := range lines {
if strings.Contains(line, searchString) {
update := false
deviceVersion, err := getVersion(deviceVersionStr)
if err != nil {
update = true
}
lowestVersion, err := getVersion(line)
if err != nil {
return false, err
}
if update || lowestVersion[0] > deviceVersion[0] || lowestVersion[1] > deviceVersion[1] || lowestVersion[2] > deviceVersion[2] {
return true, nil
}
return false, err
}
}
return false, err
}
func (d *UIDriver) supportDevice() error {
rootDir, err := os.Getwd()
if err != nil {
return err
}
raw, err := os.ReadFile(filepath.Join(rootDir, "minUiTestVersion.txt"))
if err != nil {
return err
}
lowestVersion := string(raw)
uitestVersion, err := d.Device.RunShellCommand("uitest --version")
if lowestVersion > uitestVersion {
return fmt.Errorf("not supprt uitest lowest version %s, device version %s", lowestVersion, uitestVersion)
}
return nil
}
func (d *UIDriver) createDriver() (driver string, err error) {
params := map[string]interface{}{
"api": "Driver.create",
"this": nil,
"args": []string{},
"message_type": "hypium",
}
res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi"))
if err != nil {
err = fmt.Errorf("[uitest] failed to create driver")
return
}
if res.Exception != nil {
err = fmt.Errorf("[uitest] failed to create driver msg: %s", res.Exception.Message)
return
}
driver = res.Result.(string)
return
}
func (d *UIDriver) createFocused() (onName string, err error) {
params := map[string]interface{}{
"api": "On.focused",
"this": "On#seed",
"args": []bool{true},
"message_type": "hypium",
}
res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi"))
if err != nil {
err = fmt.Errorf("[uitest] failed to create focused")
return
}
if res.Exception != nil {
err = fmt.Errorf("[uitest] failed to create focused msg: %s", res.Exception.Message)
return
}
onName = res.Result.(string)
return
}
func (d *UIDriver) findComponent(driverName, onName string) (componentName string, err error) {
params := map[string]interface{}{
"api": "Driver.waitForComponent",
"this": driverName,
"args": []any{onName, 5000},
"message_type": "hypium",
}
res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi"))
if err != nil {
err = fmt.Errorf("[uitest] failed to create focused")
return
}
if res.Exception != nil {
err = fmt.Errorf("[uitest] failed to create focused msg: %s", res.Exception.Message)
return
}
componentName = res.Result.(string)
return
}
func (d *UIDriver) createPointMatrix(pointerMatrix *PointerMatrix) (pointMatrixName string, err error) {
fingers, steps := pointerMatrix.fingerIndexStats()
params := map[string]interface{}{
"api": "PointerMatrix.create",
"this": nil,
"args": []int{fingers, steps},
"message_type": "hypium",
}
res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi"))
if err != nil {
err = fmt.Errorf("[uitest] failed to create PointerMatrix")
return
}
if res.Exception != nil {
err = fmt.Errorf("[uitest] failed to create PointerMatrix msg: %s", res.Exception.Message)
return
}
pointMatrixName = res.Result.(string)
return
}
func (d *UIDriver) releaseObj(obj []string) error {
params := map[string]interface{}{
"api": "BackendObjectsCleaner",
"this": nil,
"args": obj,
}
res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi"))
if err != nil {
err = fmt.Errorf("[uitest] failed to release driver")
return err
}
if res.Exception != nil {
err = fmt.Errorf("[uitest] failed to release driver msg: %s", res.Exception.Message)
return err
}
return nil
}
func (d *UIDriver) Touch(x, y int) error {
driverName, err := d.createDriver()
if err != nil {
return err
}
defer func() {
_ = d.releaseObj([]string{driverName})
}()
params := map[string]interface{}{
"api": "Driver.click",
"this": driverName,
"args": []int{x, y},
"message_type": "hypium",
}
res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi"))
if err != nil {
return err
}
if res.Exception != nil {
return fmt.Errorf("[uitest] failed to touch (%d, %d): %s", x, y, res.Exception.Message)
}
return nil
}
func (d *UIDriver) Drag(fromX, fromY, toX, toY int, duration float64) error {
driverName, err := d.createDriver()
if err != nil {
return err
}
defer func() {
_ = d.releaseObj([]string{driverName})
}()
distance := math.Sqrt(math.Pow(float64(fromX-toX), 2) + math.Pow(float64(fromX-toX), 2))
speed := int(distance / duration)
params := map[string]interface{}{
"api": "Driver.drag",
"this": driverName,
"args": []int{fromX, fromY, toX, toY, speed},
"message_type": "hypium",
}
res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi"))
if err != nil {
return err
}
if res.Exception != nil {
return fmt.Errorf("[uitest] failed to Drag from (%d, %d) to (%d, %d): %s", fromX, fromY, toX, toY, res.Exception.Message)
}
return nil
}
func (d *UIDriver) PressKey(key KeyCode) error {
driverName, err := d.createDriver()
if err != nil {
return err
}
defer func() {
_ = d.releaseObj([]string{driverName})
}()
params := map[string]interface{}{
"api": "Driver.triggerKey",
"this": driverName,
"args": []KeyCode{key},
"message_type": "hypium",
}
res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi"))
if err != nil {
return err
}
if res.Exception != nil {
return fmt.Errorf("[uitest] failed to Press Key code:%d: %s", key, res.Exception.Message)
}
return nil
}
func (d *UIDriver) PressKeys(keys []KeyCode) error {
driverName, err := d.createDriver()
if err != nil {
return err
}
defer func() {
_ = d.releaseObj([]string{driverName})
}()
params := map[string]interface{}{
"api": "Driver.triggerCombineKeys",
"this": driverName,
"args": keys,
"message_type": "hypium",
}
res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi"))
if err != nil {
return err
}
if res.Exception != nil {
return fmt.Errorf("[uitest] failed to Press Key code:%v: %s", keys, res.Exception.Message)
}
return nil
}
func (d *UIDriver) InjectGesture(gesture *Gesture, speedArg ...int) error {
return d.InjectMultiGesture([]*Gesture{gesture}, speedArg...)
}
func (d *UIDriver) InjectMultiGesture(gestures []*Gesture, speedArg ...int) error {
speed := 2000
var releaseObj []string
defer func() {
if len(releaseObj) > 0 {
_ = d.releaseObj(releaseObj)
}
}()
if len(speedArg) > 0 && speedArg[0] > 0 {
speed = speedArg[0]
}
driverName, err := d.createDriver()
if err != nil {
return err
}
releaseObj = append(releaseObj, driverName)
pointerMatrix := d.gestureToPointMatrix(gestures)
pointerMatrixName, err := d.createPointMatrix(pointerMatrix)
if err != nil {
return err
}
releaseObj = append(releaseObj, pointerMatrixName)
for step, point := range pointerMatrix.points {
err = d.setPoint(pointerMatrixName, point.index, step, point.point)
if err != nil {
return err
}
}
err = d.injectMultiPointerAction(driverName, pointerMatrixName, speed)
if err != nil {
return err
}
return nil
}
func (d *UIDriver) InputText(text string) error {
params := map[string]interface{}{
"api": "Driver.inputText",
"this": nil,
"args": []any{map[string]interface{}{
"x": 0,
"y": 0,
}, text},
"message_type": "hypium",
}
res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi"))
if err != nil {
return err
}
if res.Exception != nil {
return fmt.Errorf("[uitest] failed to input text %s: %s", text, res.Exception.Message)
}
return nil
}
func (d *UIDriver) InputTextOnFocused(text string) error {
driverName, err := d.createDriver()
if err != nil {
return err
}
defer func() {
_ = d.releaseObj([]string{driverName})
}()
onName, err := d.createFocused()
if err != nil {
return err
}
defer func() {
_ = d.releaseObj([]string{onName})
}()
componentName, err := d.findComponent(driverName, onName)
if err != nil {
return err
}
defer func() {
_ = d.releaseObj([]string{componentName})
}()
params := map[string]interface{}{
"api": "Component.inputText",
"this": componentName,
"args": []string{text},
"message_type": "hypium",
}
res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi"))
if err != nil {
return err
}
if res.Exception != nil {
return fmt.Errorf("[uitest] failed to input text %s: %s", text, res.Exception.Message)
}
return nil
}
func (d *UIDriver) TouchDown(x, y int) error {
params := map[string]interface{}{
"api": "touchDown",
"this": nil,
"args": map[string]interface{}{
"x": x,
"y": y,
},
"message_type": "hypium",
}
return d.sendUitestKitNoResult(params, "Gestures", DEFAULT, nil)
}
func (d *UIDriver) TouchDownAsync(x, y int) {
go func() {
err := d.TouchDown(x, y)
if err != nil {
debugLog(fmt.Sprintf("%v", err))
}
}()
}
func (d *UIDriver) TouchMove(x, y int) error {
params := map[string]interface{}{
"api": "touchMove",
"this": nil,
"args": map[string]interface{}{
"x": x,
"y": y,
},
"message_type": "hypium",
}
return d.sendUitestKitNoResult(params, "Gestures", DEFAULT, nil)
}
func (d *UIDriver) TouchMoveAsync(x, y int) {
go func() {
err := d.TouchMove(x, y)
if err != nil {
debugLog(fmt.Sprintf("%v", err))
}
}()
}
func (d *UIDriver) TouchUp(x, y int) error {
params := map[string]interface{}{
"api": "touchUp",
"this": nil,
"args": map[string]interface{}{
"x": x,
"y": y,
},
"message_type": "hypium",
}
return d.sendUitestKitNoResult(params, "Gestures", DEFAULT, nil)
}
func (d *UIDriver) TouchUpAsync(x, y int) {
go func() {
err := d.TouchUp(x, y)
if err != nil {
debugLog(fmt.Sprintf("%v", err))
}
}()
}
func (d *UIDriver) PressRecentApp() error {
params := map[string]interface{}{
"api": "pressRecentApp",
"this": nil,
"args": map[string]interface{}{},
"message_type": "hypium",
}
return d.sendUitestKitNoResult(params, "Gestures", DEFAULT, nil)
}
func (d *UIDriver) PressBack() error {
params := map[string]interface{}{
"api": "pressBack",
"this": nil,
"args": map[string]interface{}{},
"message_type": "hypium",
}
return d.sendUitestKitNoResult(params, "Gestures", DEFAULT, nil)
}
func (d *UIDriver) PressPowerKey() error {
params := map[string]interface{}{
"api": "pressPowerKey",
"this": nil,
"args": map[string]interface{}{},
"message_type": "hypium",
}
return d.sendUitestKitNoResult(params, "CtrlCmd", DEFAULT, nil)
}
func (d *UIDriver) GetDisplayRotation() (direction int, err error) {
params := map[string]interface{}{
"api": "getDisplayRotation",
"this": nil,
"args": map[string]interface{}{},
"message_type": "hypium",
}
res, err := d.sendUitestKit(params, "CtrlCmd", DEFAULT, nil)
if err != nil {
return
}
if res.Result == false {
err = fmt.Errorf("[uitest] failed to exec method getDisplayRotation msg: %s", res.Exception)
return
}
direction = (int)(res.Result.(float64))
return direction, err
}
func (d *UIDriver) UpVolume() error {
params := map[string]interface{}{
"api": "upVolume",
"this": nil,
"args": map[string]interface{}{},
"message_type": "hypium",
}
return d.sendUitestKitNoResult(params, "CtrlCmd", DEFAULT, nil)
}
func (d *UIDriver) DownVolume() error {
params := map[string]interface{}{
"api": "downVolume",
"this": nil,
"args": map[string]interface{}{},
"message_type": "hypium",
}
return d.sendUitestKitNoResult(params, "CtrlCmd", DEFAULT, nil)
}
func (d *UIDriver) RotationDisplay(direction int) error {
params := map[string]interface{}{
"api": "rotationDisplay",
"this": nil,
"args": map[string]interface{}{
"direction": direction,
},
"message_type": "hypium",
}
return d.sendUitestKitNoResult(params, "CtrlCmd", DEFAULT, nil)
}
func (d *UIDriver) GetDisplaySize() (display Dimension, err error) {
params := map[string]interface{}{
"api": "getDisplaySize",
"this": nil,
"args": map[string]interface{}{},
"message_type": "hypium",
}
res, err := d.sendUitestKit(params, "CtrlCmd", DEFAULT, nil)
if err != nil {
return
}
if res.Result == false {
err = fmt.Errorf("[uitest] failed to exec method getDisplaySize msg: %s", res.Exception)
return
}
raw, err := json.Marshal(res.Result)
if err != nil {
return
}
err = json.Unmarshal(raw, &display)
return
}
func (d *UIDriver) StartCaptureScreen(callback UitestKitCallback) error {
params := map[string]interface{}{
"api": "startCaptureScreen",
"this": nil,
"args": map[string]interface{}{},
"message_type": "hypium",
}
return d.sendUitestKitNoResult(params, "Captures", SCREEN_CAPTURE, callback)
}
func (d *UIDriver) StopCaptureScreen() error {
params := map[string]interface{}{
"api": "stopCaptureScreen",
"this": nil,
"args": map[string]interface{}{},
"message_type": "hypium",
}
return d.sendUitestKitNoResult(params, "Captures", DEFAULT, nil)
}
func (d *UIDriver) StartCaptureUiAction(callback UitestKitCallback) error {
params := map[string]interface{}{
"api": "startCaptureUiAction",
"this": nil,
"args": map[string]interface{}{},
"message_type": "hypium",
}
return d.sendUitestKitNoResult(params, "Captures", UI_ACTION_CAPTURE, callback)
}
func (d *UIDriver) StopCaptureUiAction() error {
params := map[string]interface{}{
"api": "stopCaptureUiAction",
"this": nil,
"args": map[string]interface{}{},
"message_type": "hypium",
}
return d.sendUitestKitNoResult(params, "Captures", DEFAULT, nil)
}
func (d *UIDriver) CaptureLayout() (layout interface{}, err error) {
params := map[string]interface{}{
"api": "captureLayout",
"this": nil,
"args": map[string]interface{}{},
"message_type": "hypium",
}
res, err := d.sendUitestKit(params, "Captures", DEFAULT, nil)
if err != nil {
return
}
if res.Result == false {
err = fmt.Errorf("[uitest] failed to exec method captureLayout msg: %s", res.Exception)
return
}
return res.Result, err
}
func (d *UIDriver) sendUitestKitNoResult(params map[string]interface{}, method string, reqType ReqTypeEnum, callback UitestKitCallback) error {
res, err := d.sendUitestKit(params, method, reqType, callback)
if err != nil {
return err
}
if res.Result == false {
err = fmt.Errorf("[uitest] failed to exec method %s params %v msg: %s", method, params, res.Exception)
return err
}
return nil
}
func (d *UIDriver) sendUitestReq(req UitestRequest) (res UitestResponse, err error) {
res, err = d.uTp.SendReq(req)
if err != nil {
fmt.Printf("[uitest] failed to send req first. try reconnect \n%v \n", err)
if err = d.Reconnect(); err != nil {
return
}
res, err = d.uTp.SendReq(req)
}
return
}
func (d *UIDriver) sendUitestKit(params map[string]interface{}, method string, reqType ReqTypeEnum, callback UitestKitCallback) (response UitestKitResponse, err error) {
request := newHypiumRequest(params, method)
requestStr, err := request.ToString()
if err != nil {
err = fmt.Errorf("[uitest] failed to create req while exec method %s %v", method, err)
return
}
sessionId := hashCode(fmt.Sprintf("%s%d", requestStr, time.Now().Unix()))
if sessionId <= (1 << 24) {
sessionId += 1 << 24
}
err = d.uKTp.registerCallback(reqType, sessionId, nil)
if err != nil {
fmt.Printf("[uitest] failed to register callback try reconnect %s %v", method, err)
if err = d.Reconnect(); err != nil {
return
}
if err = d.uKTp.registerCallback(reqType, sessionId, nil); err != nil {
return
}
}
res, err := d.uKTp.sendMessage(reqType, sessionId, requestStr)
if err != nil {
err = fmt.Errorf("[uitest] failed to send message while exec method %s sessionId: %d %v", method, sessionId, err)
return
}
err = d.uKTp.registerCallback(reqType, sessionId, callback)
if err != nil {
err = fmt.Errorf("[uitest] failed to register callback while exec method %s %v", method, err)
return
}
return res, nil
}
func (d *UIDriver) gestureToPointMatrix(gestures []*Gesture) *PointerMatrix {
pointerMatrix := &PointerMatrix{}
for fingerIndex, gestures := range gestures {
var curPoint Point
for _, gestureStep := range gestures.steps {
if gestureStep.GestureType == "start" {
pointerMatrix.setPoint(gestureStep.Point, fingerIndex, gestureStep.Duration)
curPoint = gestureStep.Point
}
if gestureStep.GestureType == "move" {
toPoint := gestureStep.Point
offsetX := toPoint.X - curPoint.X
offsetY := toPoint.Y - curPoint.Y
steps := gestureStep.calculateSteps()
for i := 0; i < steps-1; i++ {
curPoint = Point{X: curPoint.X + (offsetX / steps), Y: curPoint.Y + (offsetY / steps)}
pointerMatrix.setPoint(curPoint, fingerIndex, EVENT_INJECTION_DELAY_MS)
}
curPoint = toPoint
if steps == 1 {
pointerMatrix.setPoint(curPoint, fingerIndex, gestureStep.Duration%EVENT_INJECTION_DELAY_MS)
} else {
pointerMatrix.setPoint(curPoint, fingerIndex, EVENT_INJECTION_DELAY_MS+(gestureStep.Duration%EVENT_INJECTION_DELAY_MS))
}
}
if gestureStep.GestureType == "pause" {
steps := gestureStep.calculateSteps()
for i := 0; i < steps-1; i++ {
pointerMatrix.setPoint(curPoint, fingerIndex, EVENT_INJECTION_DELAY_MS)
}
pointerMatrix.setPoint(curPoint, fingerIndex, EVENT_INJECTION_DELAY_MS+(gestureStep.Duration%EVENT_INJECTION_DELAY_MS))
}
}
}
return pointerMatrix
}
func (d *UIDriver) setPoint(pointerMatrixName string, fingerIndex int, step int, point Point) error {
params := map[string]interface{}{
"api": "PointerMatrix.setPoint",
"this": pointerMatrixName,
"args": []any{fingerIndex, step, point},
"message_type": "hypium",
}
res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi"))
if err != nil {
return err
}
if res.Exception != nil {
return fmt.Errorf("[uitest] failed to setPoint from: %s", res.Exception.Message)
}
return nil
}
func (d *UIDriver) injectMultiPointerAction(driverName, pointerMatrixName string, speed int) error {
params := map[string]interface{}{
"api": "Driver.injectMultiPointerAction",
"this": driverName,
"args": []any{pointerMatrixName, speed},
"message_type": "hypium",
}
res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi"))
if err != nil {
return err
}
if res.Exception != nil {
return fmt.Errorf("[uitest] failed to injectMultiPointerAction from: %s", res.Exception.Message)
}
return nil
}
func hashCode(s string) uint32 {
h := fnv.New32a()
_, _ = h.Write([]byte(s))
return h.Sum32()
}
func (r UitestRequest) ToString() (result string, err error) {
data, err := json.Marshal(r)
if err != nil {
err = fmt.Errorf("error: \n%v", err)
return
}
return string(data), nil
}
func getVersion(str string) (version []string, err error) {
index := strings.Index(str, "@")
if index == -1 {
err = fmt.Errorf("invalid version str")
return
}
version = strings.Split(str[index+1:], ".")
return
}

274
pkg/ghdc/ui_driver_test.go Normal file
View File

@@ -0,0 +1,274 @@
package ghdc
import (
"fmt"
"sync"
"testing"
"time"
)
var (
client Client
device Device
driver *UIDriver
)
func setUp(t *testing.T) {
var err error
client, err = NewClient()
if err != nil {
t.Fatal(err)
}
devices, err := client.DeviceList()
if err != nil {
t.Fatal(err)
}
if len(devices) == 0 {
t.Fatal("not found available device")
}
device = devices[0]
driver, err = NewUIDriver(device)
if err != nil {
t.Fatal(err)
}
}
func TestDevice_Touch(t *testing.T) {
setUp(t)
err := driver.Touch(1038, 798)
if err != nil {
t.Fatal(err)
}
}
func TestDevice_Drag(t *testing.T) {
setUp(t)
err := driver.Drag(800, 1000, 200, 1000, 0.2)
if err != nil {
t.Fatal(err)
}
}
type CaptureScreenCallback struct {
count int
mux sync.Mutex
}
// OnData handles the data received
func (cb *CaptureScreenCallback) OnData(data []byte) {
cb.mux.Lock()
cb.count++
cb.mux.Unlock()
fmt.Printf("Data received: %s\n", string(data))
}
// onError handles the error received
func (cb *CaptureScreenCallback) OnError(err error) {
fmt.Printf("Error received: %v\n", err)
}
func (cb *CaptureScreenCallback) startCounter() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for range ticker.C {
cb.mux.Lock()
fmt.Printf("Screen Data received count in the last second: %d\n", cb.count)
cb.count = 0
cb.mux.Unlock()
}
}
func TestDevice_StartCaptureScreen(t *testing.T) {
setUp(t)
err := driver.StopCaptureScreen()
if err != nil {
t.Fatal(err)
}
callback := &CaptureScreenCallback{}
err = driver.StartCaptureScreen(callback)
if err != nil {
t.Fatal(err)
}
callback.startCounter()
time.Sleep(1 * time.Minute)
}
func TestDevice_StartCaptureUIAction(t *testing.T) {
setUp(t)
err := driver.StopCaptureUiAction()
if err != nil {
t.Fatal(err)
}
callback := &CaptureScreenCallback{}
err = driver.StartCaptureUiAction(callback)
if err != nil {
t.Fatal(err)
}
time.Sleep(1 * time.Minute)
}
func TestDevice_TouchDownMoveUp(t *testing.T) {
setUp(t)
for i := 0; i < 10000; i++ {
// time.Sleep(1 * time.Second)
debugLog(fmt.Sprintf("running... time %d", i))
err := driver.TouchDown(225, 1700)
if err != nil {
debugLog(err.Error())
// t.Fatal(err)
}
time.Sleep(20 * time.Millisecond)
err = driver.TouchMove(325, 1700)
if err != nil {
debugLog(err.Error())
// t.Fatal(err)
}
time.Sleep(20 * time.Millisecond)
err = driver.TouchMove(425, 1700)
if err != nil {
debugLog(err.Error())
// t.Fatal(err)
}
time.Sleep(20 * time.Millisecond)
err = driver.TouchMove(525, 1700)
if err != nil {
debugLog(err.Error())
// t.Fatal(err)
}
err = driver.TouchUp(625, 1700)
if err != nil {
debugLog(err.Error())
// t.Fatal(err)
}
}
}
func TestDevice_TouchDownUp(t *testing.T) {
setUp(t)
err := driver.TouchDown(200, 2000)
if err != nil {
debugLog(err.Error())
}
err = driver.TouchUp(200, 2000)
if err != nil {
debugLog(err.Error())
t.Fatal(err)
}
}
func TestDevice_TouchDownMoveUpAsync(t *testing.T) {
setUp(t)
driver.TouchDown(225, 1700)
time.Sleep(60 * time.Millisecond)
driver.TouchMoveAsync(325, 1700)
time.Sleep(60 * time.Millisecond)
driver.TouchMoveAsync(425, 1700)
time.Sleep(60 * time.Millisecond)
driver.TouchMoveAsync(525, 1700)
time.Sleep(60 * time.Millisecond)
driver.TouchUpAsync(625, 1700)
time.Sleep(4 * time.Second)
}
func TestDevice_GetDisplay(t *testing.T) {
setUp(t)
display, err := driver.GetDisplaySize()
if err != nil {
t.Fatal(err)
}
t.Log(display)
}
func TestDevice_GetRotation(t *testing.T) {
setUp(t)
rotation, err := driver.GetDisplayRotation()
if err != nil {
t.Fatal(err)
}
t.Log(rotation)
}
func TestDevice_PressRecentApp(t *testing.T) {
setUp(t)
err := driver.PressRecentApp()
if err != nil {
t.Fatal(err)
}
}
func TestDevice_PressBack(t *testing.T) {
setUp(t)
err := driver.PressBack()
if err != nil {
t.Fatal(err)
}
}
func TestDevice_PressPowerKey(t *testing.T) {
setUp(t)
err := driver.PressPowerKey()
if err != nil {
t.Fatal(err)
}
}
func TestDevice_UpVolume(t *testing.T) {
setUp(t)
err := driver.UpVolume()
if err != nil {
t.Fatal(err)
}
}
func TestDevice_DownVolume(t *testing.T) {
setUp(t)
err := driver.DownVolume()
if err != nil {
t.Fatal(err)
}
}
func TestDevice_CaptureLayout(t *testing.T) {
setUp(t)
layout, err := driver.CaptureLayout()
if err != nil {
t.Fatal(err)
}
t.Log(layout)
}
func TestDevice_InputText(t *testing.T) {
setUp(t)
err := driver.InputText("abcdef")
if err != nil {
t.Fatal(err)
}
}
func TestDevice_InjectPoint(t *testing.T) {
setUp(t)
err := driver.InjectGesture(NewGesture().Start(Point{800, 2000}).MoveTo(Point{200, 2000}, 2000))
if err != nil {
t.Fatal(err)
}
time.Sleep(2 * time.Second)
}
func TestDevice_PressKey(t *testing.T) {
setUp(t)
err := driver.PressKey(KEYCODE_NUM_1)
if err != nil {
t.Fatal(err)
}
}
func TestDevice_PressKeys(t *testing.T) {
setUp(t)
err := driver.PressKeys([]KeyCode{KEYCODE_SHIFT_LEFT, KEYCODE_NUM_1})
if err != nil {
t.Fatal(err)
}
}

83
pkg/ghdc/ui_gesture.go Normal file
View File

@@ -0,0 +1,83 @@
package ghdc
import (
"math"
)
var EVENT_INJECTION_DELAY_MS = 50
type Point struct {
X int `json:"x,omitempty"`
Y int `json:"y,omitempty"`
}
func (p Point) distance(p0 Point) float64 {
return math.Sqrt(math.Pow(float64(p.X-p0.X), 2) + math.Pow(float64(p.Y-p0.Y), 2))
}
type GestureStep struct {
Point
GestureType string
Duration int
}
type EmptyGesture struct{}
type Gesture struct {
steps []*GestureStep
}
type PointerMatrix struct {
points []*FingerPoint
}
type FingerPoint struct {
index int
point Point
}
func NewGesture() *EmptyGesture {
return new(EmptyGesture)
}
func (g *EmptyGesture) Start(point Point) *Gesture {
gesture := &Gesture{}
gesture.steps = append(gesture.steps, &GestureStep{Point: point, GestureType: "start"})
return gesture
}
func (g *Gesture) MoveTo(point Point, duration int) *Gesture {
g.steps = append(g.steps, &GestureStep{Point: point, GestureType: "move", Duration: duration})
return g
}
func (g *Gesture) Pause(duration int) *Gesture {
g.steps = append(g.steps, &GestureStep{GestureType: "pause", Duration: duration})
return g
}
func (gs *GestureStep) calculateSteps() int {
return (gs.Duration / EVENT_INJECTION_DELAY_MS) + 1
}
func (pm *PointerMatrix) setPoint(point Point, fingerIndex, duration int) {
point.X += 65536 * duration
pm.points = append(pm.points, &FingerPoint{index: fingerIndex, point: point})
}
func (pm *PointerMatrix) fingerIndexStats() (fingers int, maxSteps int) {
indexMap := make(map[int]int)
for _, fp := range pm.points {
indexMap[fp.index]++
}
fingers = len(indexMap)
for _, count := range indexMap {
if count > maxSteps {
maxSteps = count
}
}
return fingers, maxSteps
}

View File

@@ -0,0 +1,345 @@
package ghdc
import (
"bytes"
"container/list"
"encoding/binary"
"encoding/json"
"fmt"
"net"
"strconv"
"sync"
"time"
)
type uitestKitTransport struct {
connectionPool *ConnectionPool
socketMap map[string]*SocketContext
serial string
mu sync.Mutex
}
type SocketContext struct {
conn net.Conn
socketId string
writeLock sync.Mutex
callbackMap map[string]UitestKitCallback
queue *responseList
}
type response struct {
sessionId uint32
payload []byte
}
type responseList struct {
list *list.List
lock sync.Mutex
}
func (r *responseList) Clear() {
r.lock.Lock()
defer r.lock.Unlock()
r.list.Init()
}
func (r *responseList) push(v interface{}) {
defer r.lock.Unlock()
r.lock.Lock()
r.list.PushBack(v)
}
func (r *responseList) traverse(action func(response response)) {
r.lock.Lock()
defer r.lock.Unlock()
for e := r.list.Front(); e != nil; e = e.Next() {
action(e.Value.(response))
}
}
func (r *responseList) remove(sessionId uint32) *response {
r.lock.Lock()
defer r.lock.Unlock()
for e := r.list.Front(); e != nil; e = e.Next() {
response := e.Value.(response)
if response.sessionId == sessionId {
r.list.Remove(e)
return &response
}
}
return nil
}
type UitestKitCallback interface {
OnData([]byte)
OnError(error)
}
type ReqTypeEnum int
const (
DEFAULT ReqTypeEnum = iota
SCREEN_CAPTURE
UI_ACTION_CAPTURE
)
const (
HEADER_BYTES = "_uitestkit_rpc_message_head_"
TAILER_BYTES = "_uitestkit_rpc_message_tail_"
)
func newUitestKitTransport(serial string, host string, port string) (uKtp uitestKitTransport, err error) {
pool, err := newConnectionPool(host, port, 3)
if err != nil {
err = fmt.Errorf("[uitest] failed to init connection pool \n%v", err)
return
}
uKtp.connectionPool = pool
uKtp.socketMap = make(map[string]*SocketContext)
uKtp.serial = serial
return
}
func (uKtp *uitestKitTransport) initializeSocket(reqType ReqTypeEnum) error {
socketId := fmt.Sprintf("socket_%d_%s", reqType, uKtp.serial)
uKtp.mu.Lock()
defer uKtp.mu.Unlock()
if uKtp.socketMap[socketId] != nil {
return nil
}
connection, err := uKtp.connectionPool.getConnection()
if err != nil {
return err
}
uKtp.socketMap[socketId] = &SocketContext{socketId: socketId, conn: connection, callbackMap: make(map[string]UitestKitCallback), queue: newResponseList()}
go uKtp.receive(reqType)
return nil
}
func (uKtp *uitestKitTransport) disconnect(reqType ReqTypeEnum) {
socketId := fmt.Sprintf("socket_%d_%s", reqType, uKtp.serial)
uKtp.mu.Lock()
defer uKtp.mu.Unlock()
socketContext := uKtp.socketMap[socketId]
if socketContext == nil {
return
}
socketContext.Close()
delete(uKtp.socketMap, socketId)
}
func (uKtp *uitestKitTransport) registerCallback(reqType ReqTypeEnum, sessionId uint32, callback UitestKitCallback) error {
socketId := fmt.Sprintf("socket_%d_%s", reqType, uKtp.serial)
socketContext := uKtp.socketMap[socketId]
if socketContext == nil {
if err := uKtp.initializeSocket(reqType); err != nil {
return err
}
socketContext = uKtp.socketMap[socketId]
}
for {
socketContext.writeLock.Lock()
res := socketContext.queue.remove(sessionId)
socketContext.writeLock.Unlock()
if res != nil && callback != nil {
callback.OnData(res.payload)
}
if res == nil {
break
}
}
socketContext.writeLock.Lock()
socketContext.callbackMap[strconv.Itoa(int(sessionId))] = callback
socketContext.writeLock.Unlock()
return nil
}
func (uKtp *uitestKitTransport) receive(reqType ReqTypeEnum) {
socketId := fmt.Sprintf("socket_%d_%s", reqType, uKtp.serial)
defer uKtp.disconnect(reqType)
socketContext := uKtp.socketMap[socketId]
var receiveError error
defer func() {
if receiveError != nil {
socketContext.onException(receiveError)
}
}()
if socketContext == nil {
if receiveError = uKtp.initializeSocket(reqType); receiveError != nil {
return
}
socketContext = uKtp.socketMap[socketId]
}
for {
headerSize := len(HEADER_BYTES)
raw, err := _readN(socketContext.conn, headerSize+8)
if err != nil {
receiveError = err
break
}
if len(raw) == 0 {
continue
}
header := raw[:len(HEADER_BYTES)]
if !bytes.Equal(header, []byte(HEADER_BYTES)) {
receiveError = fmt.Errorf("verify message head failed on channel: %s", socketId)
break
}
var sessionId, length uint32
if receiveError = binary.Read(bytes.NewReader(raw[headerSize:headerSize+4]), binary.BigEndian, &sessionId); receiveError != nil {
break
}
if receiveError = binary.Read(bytes.NewReader(raw[headerSize+4:headerSize+8]), binary.BigEndian, &length); receiveError != nil {
break
}
payload, err := _readN(socketContext.conn, int(length))
if err != nil {
receiveError = err
break
}
tail, err := _readN(socketContext.conn, len(TAILER_BYTES))
if err != nil {
receiveError = err
break
}
if !bytes.Equal(tail, []byte(TAILER_BYTES)) {
receiveError = fmt.Errorf("verify message tail failed on channel: %s", socketId)
break
}
socketContext.writeLock.Lock()
callback := socketContext.callbackMap[strconv.Itoa(int(sessionId))]
socketContext.writeLock.Unlock()
if callback != nil {
callback.OnData(payload)
} else {
socketContext.queue.push(response{sessionId: sessionId, payload: payload})
}
}
}
func (uKtp *uitestKitTransport) sendMessage(reqType ReqTypeEnum, sessionId uint32, message string) (response UitestKitResponse, err error) {
defer func() {
if err != nil {
uKtp.disconnect(reqType)
}
}()
if err = uKtp._sendMessage(reqType, sessionId, message); err != nil {
return
}
return uKtp.receiveMessage(reqType, sessionId)
}
func (uKtp *uitestKitTransport) _sendMessage(reqType ReqTypeEnum, sessionId uint32, message string) (err error) {
socketId := fmt.Sprintf("socket_%d_%s", reqType, uKtp.serial)
socketContext := uKtp.socketMap[socketId]
if socketContext == nil {
if err = uKtp.initializeSocket(reqType); err != nil {
return
}
socketContext = uKtp.socketMap[socketId]
}
buffer := new(bytes.Buffer)
if err = binary.Write(buffer, binary.BigEndian, []byte(HEADER_BYTES)); err != nil {
return
}
if err = binary.Write(buffer, binary.BigEndian, sessionId); err != nil {
return
}
if err = binary.Write(buffer, binary.BigEndian, uint32(len(message))); err != nil {
return
}
if err = binary.Write(buffer, binary.BigEndian, []byte(message)); err != nil {
return
}
if err = binary.Write(buffer, binary.BigEndian, []byte(TAILER_BYTES)); err != nil {
return
}
socketContext.writeLock.Lock()
defer socketContext.writeLock.Unlock()
return _send(socketContext.conn, buffer.Bytes())
}
func (sc *SocketContext) Close() {
sc.writeLock.Lock()
defer sc.writeLock.Unlock()
if sc.conn != nil {
_ = sc.conn.Close()
}
// 清空callbackMap
for key := range sc.callbackMap {
delete(sc.callbackMap, key)
}
sc.callbackMap = nil
// 清理队列
if sc.queue != nil {
sc.queue.Clear()
}
}
func (uKtp *uitestKitTransport) Close() {
uKtp.mu.Lock()
defer uKtp.mu.Unlock()
// 关闭所有的SocketContext
if uKtp.socketMap != nil {
for _, socketContext := range uKtp.socketMap {
socketContext.Close()
}
}
// 关闭连接池
if uKtp.connectionPool != nil {
uKtp.connectionPool.close()
}
uKtp.socketMap = nil
uKtp.connectionPool = nil
}
func (uKtp *uitestKitTransport) receiveMessage(reqType ReqTypeEnum, sessionId uint32) (response UitestKitResponse, err error) {
socketId := fmt.Sprintf("socket_%d_%s", reqType, uKtp.serial)
socketContext := uKtp.socketMap[socketId]
if socketContext == nil {
err = fmt.Errorf("failed to read message. not found target connection")
return
}
// 创建一个计时器,设置超时时间为 10 秒
timeout := time.After(2 * time.Second)
// 创建一个 ticker每秒触发一次
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-timeout:
err = fmt.Errorf("failed to read message in 2 second")
return
case <-ticker.C:
res := socketContext.queue.remove(sessionId)
if res != nil {
err = json.Unmarshal(res.payload, &response)
return
}
}
}
}
func (sc *SocketContext) onException(err error) {
for _, callback := range sc.callbackMap {
if callback != nil {
callback.OnError(err)
}
}
}
func newResponseList() *responseList {
return &responseList{list: list.New()}
}

View File

@@ -0,0 +1,82 @@
package ghdc
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"time"
)
type uitestTransport struct {
connectionPool *ConnectionPool
readTimeout time.Duration
}
func newUitestTransport(host string, port string, readTimeout ...time.Duration) (uTp uitestTransport, err error) {
if len(readTimeout) == 0 {
readTimeout = []time.Duration{DefaultReadTimeout}
}
uTp.readTimeout = readTimeout[0]
pool, err := newConnectionPool(host, port, 2)
if err != nil {
err = fmt.Errorf("[uitest] failed to init connection pool \n%v", err)
return
}
uTp.connectionPool = pool
return
}
func newHypiumRequest(params interface{}, method string) UitestRequest {
return UitestRequest{
Module: "com.ohos.devicetest.hypiumApiHelper",
Method: method,
Params: params,
RequestId: MD5(fmt.Sprintf("%d", time.Now().UnixNano())),
}
}
func (uTp *uitestTransport) SendReq(req UitestRequest) (res UitestResponse, err error) {
requestBytes, err := json.Marshal(req)
if err != nil {
err = fmt.Errorf("[uitest] failed to marshal %v request %v", req, err)
return
}
requestBytes = append(requestBytes, '\n')
conn, err := uTp.connectionPool.getConnection()
if err != nil {
err = fmt.Errorf("[uitest] failed to get connection \n%v", err)
return
}
defer func() {
uTp.connectionPool.releaseConnection(conn)
}()
_ = conn.SetReadDeadline(time.Now().Add(uTp.readTimeout))
debugLog(fmt.Sprintf("[uitest] send Request %v", req))
err = _send(conn, requestBytes)
if err != nil {
err = fmt.Errorf("[uitest] failed to get send %v request %v", req, err)
return
}
raw, err := _read(conn)
if err != nil {
err = fmt.Errorf("[uitest] failed to read %v response %v", req, err)
return
}
res = UitestResponse{}
err = json.Unmarshal(raw, &res)
return
}
func (uTp *uitestTransport) Close() {
if uTp.connectionPool != nil {
uTp.connectionPool.close()
}
uTp.connectionPool = nil
}
func MD5(str string) string {
hash := md5.New()
hash.Write([]byte(str))
return hex.EncodeToString(hash.Sum(nil))
}