diff --git a/adb/connect.go b/adb/connect.go index f9537f9..e82426f 100644 --- a/adb/connect.go +++ b/adb/connect.go @@ -182,3 +182,41 @@ func ReadResponse(conn net.Conn, debug bool) (string, []byte, error) { // unexpected token return st, nil, nil } + +// LaunchUiautomator connects to adb server, transports to device, starts uiautomator2, +// and returns an io.Reader for streaming logs. +func LaunchUiautomator(addr, serial string) { + conn, err := DialADB(addr, 15*time.Second) + if err != nil { + fmt.Println("dial error:", err) + return + } + // transport + adbSend(conn, "host:transport:"+serial) + resp, _ := adbReadResponse(conn) + fmt.Println("transport resp:", resp) + + // shell + cmd := "shell:CLASSPATH=/data/local/tmp/u2.jar app_process / com.wetest.uia2.Main" + adbSend(conn, cmd) + resp, _ = adbReadResponse(conn) + fmt.Println("shell resp:", resp) + + // 输出流 + io.Copy(os.Stdout, conn) +} + +func adbSend(conn net.Conn, cmd string) error { + length := fmt.Sprintf("%04x", len(cmd)) + _, err := conn.Write([]byte(length + cmd)) + return err +} + +func adbReadResponse(conn net.Conn) (string, error) { + buf := make([]byte, 4) + _, err := conn.Read(buf) + if err != nil { + return "", err + } + return string(buf), nil +} diff --git a/cmd/main.go b/cmd/main.go index fb03fa1..433fe31 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -9,15 +9,16 @@ import ( ) func main() { - + addr := "127.0.0.1:5037" + serial := "emulator-5556" + FilePushInstall() + go adb.LaunchUiautomator(addr, serial) + select {} } // 文件推送加安装 func FilePushInstall() { addr := "127.0.0.1:5037" - // local := "./assets/app-uiautomator.apk" - // remote := "/sdcard/app-uiautomator.apk" - // mode := 0644 serial := "emulator-5556" payload, _ := adb.ListDevicesRaw(addr, 15*time.Second) m := adb.ParseDevicesPayload(payload) @@ -26,7 +27,7 @@ func FilePushInstall() { services.InstallServiceJar(addr, serial) - services.InstallServiceApk(addr, serial) + // services.InstallServiceApk(addr, serial) } // 文件推送验证 diff --git a/libs/selector.go b/libs/selector.go new file mode 100644 index 0000000..f777ab3 --- /dev/null +++ b/libs/selector.go @@ -0,0 +1,223 @@ +package libs + +import ( + "fmt" + "sort" + "strings" +) + +const ( + MASK_TEXT uint32 = 0x00000001 + MASK_TEXT_CONTAINS uint32 = 0x00000002 + MASK_TEXT_MATCHES uint32 = 0x00000004 + MASK_TEXT_STARTS_WITH uint32 = 0x00000008 + MASK_CLASS_NAME uint32 = 0x00000010 + MASK_CLASS_NAME_MATCHES uint32 = 0x00000020 + MASK_DESCRIPTION uint32 = 0x00000040 + MASK_DESCRIPTION_CONTAINS uint32 = 0x00000080 + MASK_DESCRIPTION_MATCHES uint32 = 0x00000100 + MASK_DESCRIPTION_STARTS_WITH uint32 = 0x00000200 + MASK_CHECKABLE uint32 = 0x00000400 + MASK_CHECKED uint32 = 0x00000800 + MASK_CLICKABLE uint32 = 0x00001000 + MASK_LONG_CLICKABLE uint32 = 0x00002000 + MASK_SCROLLABLE uint32 = 0x00004000 + MASK_ENABLED uint32 = 0x00008000 + MASK_FOCUSABLE uint32 = 0x00010000 + MASK_FOCUSED uint32 = 0x00020000 + MASK_SELECTED uint32 = 0x00040000 + MASK_PACKAGE_NAME uint32 = 0x00080000 + MASK_PACKAGE_NAME_MATCHES uint32 = 0x00100000 + MASK_RESOURCE_ID uint32 = 0x00200000 + MASK_RESOURCE_ID_MATCHES uint32 = 0x00400000 + MASK_INDEX uint32 = 0x00800000 + MASK_INSTANCE uint32 = 0x01000000 +) + +// Relationship type for building child/sibling chains. +type relationType string + +const ( + relChild relationType = "child" + relSibling relationType = "sibling" +) + +// Field metadata mirrors Python __fields: name -> (mask, default) +type fieldMeta struct { + Mask uint32 + Default any +} + +var allowedFields = map[string]fieldMeta{ + "text": {MASK_TEXT, nil}, + "textContains": {MASK_TEXT_CONTAINS, nil}, + "textMatches": {MASK_TEXT_MATCHES, nil}, + "textStartsWith": {MASK_TEXT_STARTS_WITH, nil}, + "className": {MASK_CLASS_NAME, nil}, + "classNameMatches": {MASK_CLASS_NAME_MATCHES, nil}, + "description": {MASK_DESCRIPTION, nil}, + "descriptionContains": {MASK_DESCRIPTION_CONTAINS, nil}, + "descriptionMatches": {MASK_DESCRIPTION_MATCHES, nil}, + "descriptionStartsWith": {MASK_DESCRIPTION_STARTS_WITH, nil}, + "checkable": {MASK_CHECKABLE, false}, + "checked": {MASK_CHECKED, false}, + "clickable": {MASK_CLICKABLE, false}, + "longClickable": {MASK_LONG_CLICKABLE, false}, + "scrollable": {MASK_SCROLLABLE, false}, + "enabled": {MASK_ENABLED, false}, + "focusable": {MASK_FOCUSABLE, false}, + "focused": {MASK_FOCUSED, false}, + "selected": {MASK_SELECTED, false}, + "packageName": {MASK_PACKAGE_NAME, nil}, + "packageNameMatches": {MASK_PACKAGE_NAME_MATCHES, nil}, + "resourceId": {MASK_RESOURCE_ID, nil}, + "resourceIdMatches": {MASK_RESOURCE_ID_MATCHES, nil}, + "index": {MASK_INDEX, 0}, + "instance": {MASK_INSTANCE, 0}, +} + +// Selector replicates Python Selector(dict) behavior with explicit APIs. +type Selector struct { + mask uint32 + fields map[string]any // only allowedFields keys permitted + childOrSibling []relationType + childOrSiblingSelect []*Selector +} + +// NewSelector initializes with kwargs like Python's Selector(**kwargs). +func NewSelector(kwargs map[string]any) (*Selector, error) { + s := &Selector{ + mask: 0, + fields: make(map[string]any), + childOrSibling: make([]relationType, 0), + childOrSiblingSelect: make([]*Selector, 0), + } + for k, v := range kwargs { + if err := s.Set(k, v); err != nil { + return nil, err + } + } + return s, nil +} + +// Set assigns a field and updates the bitmask. Only allowed fields permitted. +func (s *Selector) Set(key string, val any) error { + meta, ok := allowedFields[key] + if !ok { + return fmt.Errorf("%s is not allowed", key) + } + s.fields[key] = val + s.mask |= meta.Mask + return nil +} + +// Delete removes a field and clears its bit in mask. +func (s *Selector) Delete(key string) error { + meta, ok := allowedFields[key] + if !ok { + return fmt.Errorf("%s is not allowed", key) + } + delete(s.fields, key) + s.mask &^= meta.Mask // AND NOT + return nil +} + +// Get retrieves a field (nil if absent). +func (s *Selector) Get(key string) (any, bool) { + v, ok := s.fields[key] + return v, ok +} + +// Mask returns current bit mask. +func (s *Selector) Mask() uint32 { return s.mask } + +// Child appends a child selector (in-place) and returns the receiver. +func (s *Selector) Child(kwargs map[string]any) (*Selector, error) { + sub, err := NewSelector(kwargs) + if err != nil { + return nil, err + } + s.childOrSibling = append(s.childOrSibling, relChild) + s.childOrSiblingSelect = append(s.childOrSiblingSelect, sub) + return s, nil +} + +// Sibling appends a sibling selector (in-place) and returns the receiver. +func (s *Selector) Sibling(kwargs map[string]any) (*Selector, error) { + sub, err := NewSelector(kwargs) + if err != nil { + return nil, err + } + s.childOrSibling = append(s.childOrSibling, relSibling) + s.childOrSiblingSelect = append(s.childOrSiblingSelect, sub) + return s, nil +} + +// Clone deep-copies selector, including child/sibling chains. +func (s *Selector) Clone() *Selector { + cp := &Selector{ + mask: s.mask, + fields: make(map[string]any, len(s.fields)), + childOrSibling: make([]relationType, len(s.childOrSibling)), + childOrSiblingSelect: make([]*Selector, 0, len(s.childOrSiblingSelect)), + } + for k, v := range s.fields { + cp.fields[k] = v + } + copy(cp.childOrSibling, s.childOrSibling) + for _, sub := range s.childOrSiblingSelect { + cp.childOrSiblingSelect = append(cp.childOrSiblingSelect, sub.Clone()) + } + return cp +} + +// UpdateInstance updates 'instance' on the last child/sibling selector if present; +// otherwise updates the current selector's 'instance'. +func (s *Selector) UpdateInstance(i int) error { + if len(s.childOrSiblingSelect) > 0 { + last := s.childOrSiblingSelect[len(s.childOrSiblingSelect)-1] + return last.Set("instance", i) + } + return s.Set("instance", i) +} + +// String prints Selector [k='v', ...] while skipping mask and empty child/sibling groups. +// Deterministic order for readability. +func (s *Selector) String() string { + parts := make([]string, 0, len(s.fields)+2) + + // stable sort keys for consistent output + keys := make([]string, 0, len(s.fields)) + for k := range s.fields { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + // repr-like rendering + parts = append(parts, fmt.Sprintf("%s=%q", k, fmtAny(s.fields[k]))) + } + + // child/sibling only if present + if len(s.childOrSibling) > 0 && len(s.childOrSiblingSelect) > 0 { + // render as chain: rel(type)->subselector + chunks := make([]string, 0, len(s.childOrSibling)) + for i, rel := range s.childOrSibling { + chunks = append(chunks, fmt.Sprintf("%s(%s)", rel, s.childOrSiblingSelect[i].String())) + } + parts = append(parts, "chain=["+strings.Join(chunks, ", ")+"]") + } + + return "Selector [" + strings.Join(parts, ", ") + "]" +} + +func fmtAny(v any) string { + switch t := v.(type) { + case string: + return t + case fmt.Stringer: + return t.String() + default: + return fmt.Sprintf("%v", v) + } +} diff --git a/test/test.py b/test/test.py new file mode 100644 index 0000000..87c6247 --- /dev/null +++ b/test/test.py @@ -0,0 +1,8 @@ +import uiautomator2 as u2 + +d = u2.connect('emulator-5556') +# d.app_start('com.android.chrome', stop=True) # Start Bilibili + +# d.implicitly_wait(10.0) + +# d(text="向设备添加账号").click()