package apis import ( "bufio" "bytes" "crypto/md5" "encoding/hex" "encoding/json" "encoding/xml" "errors" "fmt" "image" _ "image/jpeg" _ "image/png" "io" "math" "os" "regexp" "strconv" "strings" "time" m "code.xbase.vip/biao/go-mobile-automation" "code.xbase.vip/biao/go-mobile-automation/models" "github.com/antchfx/xmlquery" ) type Device struct { m.IOperation *InputMethodMixIn *XPathMixIn *UiObjectMixIn *CvMixIn settings *Settings } func NewDevice(ops m.IOperation) *Device { settings := DefaultSettings() d := &Device{ IOperation: ops, InputMethodMixIn: NewInputMethodMixIn(ops), XPathMixIn: &XPathMixIn{}, UiObjectMixIn: &UiObjectMixIn{}, CvMixIn: NewCvMixIn(ops), settings: settings, } d.XPathMixIn.d = d d.UiObjectMixIn.d = d return d } func (d *Device) SetNewCommandTimeout(timeout int) error { _, err := d.GetHttpRequest().Post("/newCommandTimeout", ([]byte)(strconv.Itoa(timeout))) return err } var deviceInfo *models.DeviceInfo func (d *Device) DeviceInfo() (*models.DeviceInfo, error) { if deviceInfo != nil { return deviceInfo, nil } data, err := d.GetHttpRequest().Get("/info") if err != nil { return nil, err } info := new(models.DeviceInfo) err = json.Unmarshal(data, info) if err != nil { return nil, err } deviceInfo = info return info, nil } func (d *Device) WindowSize() (int, int, error) { info, err := d.DeviceInfo() if err != nil { return 0, 0, err } w := info.Display.Width h := info.Display.Height o, err := d._get_orientation() if err != nil { return w, h, nil } if (w > h) != (o%2 == 1) { w, h = h, w } return w, h, nil } func (d *Device) _get_orientation() (int, error) { /* Rotaion of the phone 0: normal 1: home key on the right 2: home key on the top 3: home key on the left */ re := regexp.MustCompile(`.*DisplayViewport.*orientation=(?P\d+), .*deviceWidth=(?P\d+), deviceHeight=(?P\d+).*`) out, err := d.Shell("dumpsys display") if err != nil { return 0, err } lines := strings.Split(out, "\n") for _, line := range lines { matches := re.FindStringSubmatch(line) if matches == nil { continue } idx := re.SubexpIndex("orientation") o, err := strconv.Atoi(matches[idx]) if err != nil { return 0, err } return o, nil } return 0, errors.New("orientation not found") } func (d *Device) ScreenshotSave(fileName string) error { data, err := d.GetHttpRequest().Get("/screenshot/0") if err != nil { return err } file, err := os.Create(fileName) if err != nil { return err } writer := bufio.NewWriter(file) _, err = writer.Write(data) if err != nil { return err } writer.Flush() return nil } func (d *Device) ScreenshotBytes() ([]byte, error) { return d.GetHttpRequest().Get("/screenshot/0") } func (d *Device) Screenshot() (image.Image, string, error) { data, err := d.GetHttpRequest().Get("/screenshot/0") if err != nil { return nil, "", err } reader := bytes.NewReader(data) return image.Decode(reader) } type JsonRpcDto struct { Jsonrpc string `json:"jsonrpc"` Id string `json:"id"` Method string `json:"method"` Params []interface{} `json:"params"` } func createJsonRpcDto(method string, parameters ...interface{}) *JsonRpcDto { txt := fmt.Sprintf("%s at %d", method, time.Now().Unix()) hash := md5.Sum([]byte(txt)) return &JsonRpcDto{ Jsonrpc: "2.0", Id: hex.EncodeToString(hash[:]), Method: method, Params: parameters, } } type JsonRpcResultError struct { Code int `json:"code"` Message string `json:"message"` Data interface{} `json:"data"` } type JsonRpcResult struct { Error *JsonRpcResultError `json:"error"` Result interface{} `json:"result"` } func createJsonRpcResult(data []byte) (interface{}, *JsonRpcResultError, error) { res := new(JsonRpcResult) err := json.Unmarshal(data, res) if err != nil { return nil, nil, err } if res.Error != nil { return nil, res.Error, nil } return res.Result, nil, nil } func (d *Device) requestJsonRpc(method string, parameters ...interface{}) (interface{}, error) { res, resend, restart, err := d.requestJsonRpcInternal(method, parameters...) if restart { s := NewService(SERVICE_UIAUTOMATOR, d.GetHttpRequest()) ok, err := _force_reset_uiautomator_v2(d, s, false) if err != nil { return nil, err } if !ok { return nil, errors.New("uiautomator failed to restart") } } if resend { res, _, _, err = d.requestJsonRpcInternal(method, parameters...) return res, err } if err != nil { return nil, err } return res, nil } func (d *Device) requestJsonRpcInternal(method string, parameters ...interface{}) (interface{}, bool, bool, error) { dto := createJsonRpcDto(method, parameters...) bytes, err := json.Marshal(dto) if err != nil { return nil, false, false, err } str := string(bytes) str = strings.ReplaceAll(str, `\\u`, "\\u") bytes = []byte(str) data, err := d.GetHttpRequest().Post("/jsonrpc/0", bytes) if err != nil { errTxt := err.Error() if strings.HasPrefix(errTxt, "[status:") { idx := strings.Index(errTxt, "]") statusCodeStr := errTxt[8:idx] if statusCode, _err := strconv.Atoi(statusCodeStr); _err == nil { if statusCode == 502 || statusCode == 410 { return nil, true, true, err } else { return nil, true, false, err } } } return nil, false, false, err } res, e, err := createJsonRpcResult(data) if err != nil { return nil, false, false, err } if e != nil { if e_data, e_data_ok := e.Data.(string); e_data_ok && strings.Contains(e_data, "UiAutomation not connected") { return nil, true, true, errors.New(strings.ToLower(e.Message)) } else { return nil, true, false, errors.New(strings.ToLower(e.Message)) } } return res, false, false, nil } func formatXML(data []byte) ([]byte, error) { b := &bytes.Buffer{} decoder := xml.NewDecoder(bytes.NewReader(data)) encoder := xml.NewEncoder(b) encoder.Indent("", " ") for { token, err := decoder.Token() if err == io.EOF { encoder.Flush() return b.Bytes(), nil } if err != nil { return nil, err } err = encoder.EncodeToken(token) if err != nil { return nil, err } } } func (d *Device) DumpHierarchy(compressed bool, pretty bool) (string, error) { // 965 content = self.jsonrpc.dumpWindowHierarchy(compressed, None) result, err := d.requestJsonRpc("dumpWindowHierarchy", compressed) if err != nil { return "", err } content, ok := result.(string) if !ok || content == "" { return "", errors.New("dump hierarchy is empty") } if pretty { _xml, err := formatXML([]byte(content)) if err != nil { return content, nil } return string(_xml), nil } return content, nil } func (d *Device) DumpHierarchyDefault() (string, error) { return d.DumpHierarchy(false, false) } func FormatHierachy(content string) (*xmlquery.Node, error) { doc, err := xmlquery.Parse(strings.NewReader(content)) if err != nil { return nil, err } els, err := xmlquery.QueryAll(doc, "//node") if err != nil { return nil, err } for _, t := range els { if len(t.Attr) == 0 { continue } for aidx, a := range t.Attr { if a.Name.Local == "class" { cls := a.Value t.Data = strings.ReplaceAll(cls, "$", "-") t.Attr = append(t.Attr[:aidx], t.Attr[aidx+1:]...) break } } } return doc, nil } func (d *Device) ImplicitlyWait(to time.Duration) { d.settings.ImplicitlyWait(to) } func (d *Device) pos_rel2abs(fast bool) (func(float32, float32) (int, int, error), error) { var _width, _height int if fast { _w, _h, err := d.WindowSize() if err != nil { return nil, err } _width = _w _height = _h } getSize := func() (int, int, error) { if fast { return _width, _height, nil } else { return d.WindowSize() } } return func(x, y float32) (int, int, error) { if x < 0 || y < 0 { return 0, 0, errors.New("坐标值不能为负") } var w, h int if x < 1 || y < 1 { _w, _h, err := getSize() if err != nil { return 0, 0, err } w = _w h = _h } else { return int(x), int(y), nil } _x := int(x) _y := int(y) if x < 1 { _x = (int)(x * float32(w)) } if y < 1 { _y = (int)(y * float32(h)) } return _x, _y, nil }, nil } type Touch struct { d *Device } func (t *Touch) Down(x float32, y float32) error { rel2abs, err := t.d.pos_rel2abs(t.d.settings.FastRel2Abs) if err != nil { return err } _x, _y, err := rel2abs(x, y) if err != nil { return err } _, err = t.d.requestJsonRpc("injectInputEvent", 0, _x, _y, 0) // fmt.Println(a) if err != nil { return err } return nil } func (t *Touch) Move(x float32, y float32) error { rel2abs, err := t.d.pos_rel2abs(t.d.settings.FastRel2Abs) if err != nil { return err } _x, _y, err := rel2abs(x, y) if err != nil { return err } _, err = t.d.requestJsonRpc("injectInputEvent", 2, _x, _y, 0) if err != nil { return err } return nil } func (t *Touch) Up(x float32, y float32) error { rel2abs, err := t.d.pos_rel2abs(t.d.settings.FastRel2Abs) if err != nil { return err } _x, _y, err := rel2abs(x, y) if err != nil { return err } _, err = t.d.requestJsonRpc("injectInputEvent", 1, _x, _y, 0) if err != nil { return err } return nil } func (d *Device) Touch() *Touch { return &Touch{ d: d, } } func (d *Device) Click(x float32, y float32) error { rel2abs, err := d.pos_rel2abs(d.settings.FastRel2Abs) if err != nil { return err } _x, _y, err := rel2abs(x, y) if err != nil { return err } delayAfter := d.settings.operation_delay("click") defer delayAfter() _, err = d.requestJsonRpc("click", _x, _y) if err != nil { return err } return nil } func (d *Device) Tap(x, y int) error { _, err := d.Shell(fmt.Sprintf("input tap %d %d", x, y)) return err } func (d *Device) DoubleClick(x float32, y float32, duration time.Duration) error { t := d.Touch() err := t.Down(x, y) if err != nil { return err } err = t.Up(x, y) if err != nil { return err } time.Sleep(duration) err = d.Click(x, y) if err != nil { return err } return nil } func (d *Device) DoubleClickDefault(x float32, y float32) error { return d.DoubleClick(x, y, 100*time.Millisecond) } func (d *Device) LongClick(x, y float32, duration time.Duration) error { t := d.Touch() err := t.Down(x, y) if err != nil { return err } time.Sleep(duration) err = t.Up(x, y) if err != nil { return err } return nil } func (d *Device) LongClickDefault(x, y float32) error { return d.LongClick(x, y, 500*time.Millisecond) } func (d *Device) Swipe(fx, fy, tx, ty float32, seconds float64) error { rel2abs, err := d.pos_rel2abs(d.settings.FastRel2Abs) if err != nil { return err } _fx, _fy, err := rel2abs(fx, fy) if err != nil { return err } _tx, _ty, err := rel2abs(tx, ty) if err != nil { return err } steps := int(math.Max(2, seconds*200)) delayAfter := d.settings.operation_delay("swipe") defer delayAfter() _, err = d.requestJsonRpc("swipe", _fx, _fy, _tx, _ty, steps) return err } func (d *Device) SwipeDefault(fx, fy, tx, ty float32) error { return d.Swipe(fx, fy, tx, ty, 0.275) } type Point4Swipe struct { X float32 Y float32 } func (d *Device) SwipePoints(seconds float64, points ...Point4Swipe) error { if points == nil || len(points) == 1 || len(points) == 0 { return nil } rel2abs, err := d.pos_rel2abs(d.settings.FastRel2Abs) if err != nil { return err } ppoints := make([]int, 0) for _, p := range points { x, y, err := rel2abs(p.X, p.Y) if err != nil { return err } ppoints = append(ppoints, x, y) } steps := int(math.Max(2, seconds*200)) _, err = d.requestJsonRpc("swipePoints", ppoints, steps) return err } func (d *Device) SwipePointsDefault(points ...Point4Swipe) error { return d.SwipePoints(0.275, points...) } func (d *Device) Drag(sx, sy, ex, ey float32, seconds float64) error { rel2abs, err := d.pos_rel2abs(d.settings.FastRel2Abs) if err != nil { return err } _fx, _fy, err := rel2abs(sx, sy) if err != nil { return err } _tx, _ty, err := rel2abs(ex, ey) if err != nil { return err } steps := int(math.Max(2, seconds*200)) delayAfter := d.settings.operation_delay("drag") defer delayAfter() _, err = d.requestJsonRpc("drag", _fx, _fy, _tx, _ty, steps) return err } func (d *Device) DragDefault(sx, sy, ex, ey float32) error { return d.Drag(sx, sy, ex, ey, 0.275) } const ( VSK_HOME = "home" VSK_BACK = "back" VSK_LEFT = "left" VSK_RIGHT = "right" VSK_UP = "up" VSK_DOWN = "down" VSK_CENTER = "center" VSK_MENU = "menu" VSK_SEARCH = "search" VSK_ENTER = "enter" VSK_DELETE = "delete" VSK_DEL = "del" VSK_RECENT = "recent" //recent apps VSK_VOLUME_UP = "volume_up" VSK_VOLUME_DOWN = "volume_down" VSK_VOLUME_MUTE = "volume_mute" VSK_CAMERA = "camera" VSK_POWER = "power" ) func (d *Device) Press(key string) error { delayAfter := d.settings.operation_delay("press") defer delayAfter() _, err := d.requestJsonRpc("pressKey", key) return err } func (d *Device) Press2(key int) error { delayAfter := d.settings.operation_delay("press") defer delayAfter() _, err := d.requestJsonRpc("pressKeyCode", key) return err } func (d *Device) Press2WithMeta(key int, meta int) error { delayAfter := d.settings.operation_delay("press") defer delayAfter() _, err := d.requestJsonRpc("pressKeyCode", key, meta) return err } func (d *Device) SetOrientation(orient string) error { switch orient { case "n": orient = "natural" case "l": orient = "left" case "u": orient = "upsidedown" case "r": orient = "right" default: return nil } _, err := d.requestJsonRpc("setOrientation", orient) return err } func (d *Device) LastTraversedText() (interface{}, error) { return d.requestJsonRpc("getLastTraversedText") } func (d *Device) ClearTraversedText() error { _, err := d.requestJsonRpc("clearLastTraversedText") return err } func (d *Device) OpenNotification() error { _, err := d.requestJsonRpc("openNotification") return err } func (d *Device) OpenQuickSettings() error { _, err := d.requestJsonRpc("openQuickSettings") return err } func (d *Device) OpenUrl(url string) error { if url == "" { return nil } _, err := d.Shell("am start -a android.intent.action.VIEW -d " + url) return err } func (d *Device) GetClipboard() (string, error) { txt, err := d.requestJsonRpc("getClipboard") if err != nil { return "", err } return txt.(string), nil } func (d *Device) SetClipboard(text string) error { _, err := d.requestJsonRpc("setClipboard", nil, text) return err } func (d *Device) SetClipboard2(text string, label string) error { _, err := d.requestJsonRpc("setClipboard", label, text) return err } func (d *Device) KeyEvent(v int) error { _, err := d.Shell("input keyevent " + strconv.Itoa(v)) return err } func (d *Device) ShowFloatWindow(show bool) error { arg := strings.ToLower(strconv.FormatBool(show)) _, err := d.Shell("am start -n com.github.uiautomator/.ToastActivity -e showFloatWindow " + arg) return err } type Toast struct { d *Device } func (t *Toast) GetMessage(waitTimeout time.Duration, cacheTimeout time.Duration, defaultMessage string) (string, error) { now := time.Now() deadline := now.Add(waitTimeout) for time.Now().Before(deadline) { _msg, err := t.d.requestJsonRpc("getLastToast", cacheTimeout.Milliseconds()) if err != nil { return "", err } msg, ok := _msg.(string) if ok { return msg, nil } time.Sleep(500 * time.Millisecond) } return defaultMessage, nil } func (t *Toast) Reset() error { _, err := t.d.requestJsonRpc("clearLastToast") return err } func (t *Toast) Show(text string, duration time.Duration) error { _, err := t.d.requestJsonRpc("makeToast", text, duration.Milliseconds()) return err } func (d *Device) Toast() *Toast { return &Toast{ d: d, } } const ( IDENTIFY_THEME_BLACK = "black" IDENTIFY_THEME_RED = "red" ) func (d *Device) OpenIdentify(theme string) error { _, err := d.Shell("am start -W -n com.github.uiautomator/.IdentifyActivity -e theme " + theme) return err }