diff --git a/apis/cv.go b/apis/cv.go new file mode 100644 index 0000000..ad11cfc --- /dev/null +++ b/apis/cv.go @@ -0,0 +1,161 @@ +package apis + +import ( + "bytes" + "encoding/json" + "mime/multipart" + "strings" + "time" + + m "github.com/fantonglang/go-mobile-automation" +) + +type CvMixIn struct { + m.IOperation + launchCmd string + activateCmd string + dumpsysCmd string +} + +func NewCvMixIn(ops m.IOperation) *CvMixIn { + return &CvMixIn{ + IOperation: ops, + launchCmd: `am start -n "cn.amghok.opencvhelper/.MainActivity" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER`, + activateCmd: `am startservice -a ACTIVATE "cn.amghok.opencvhelper/.NetworkingService"`, + dumpsysCmd: `dumpsys activity services cn.amghok.opencvhelper`, + } +} + +type CvTemplateMatchingResult struct { + X int `json:"x"` + Y int `json:"y"` + Width int `json:"width"` + Height int `json:"height"` +} + +type CvContourRectResult struct { + X int `json:"x"` + Y int `json:"y"` + Width int `json:"width"` + Height int `json:"height"` +} + +type CvContourCircleResult struct { + CenterX int `json:"center_x"` + CenterY int `json:"center_y"` + Radius int `json:"radius"` +} + +func (cv *CvMixIn) testConnectivity() error { + req := cv.GetCvRequest() + _, _, err := req.GetWithTimeout("/ping", time.Second) + if err == nil { + return nil + } + dumpsysOut, err := cv.Shell(cv.dumpsysCmd) + if err != nil { + return err + } + if strings.HasSuffix(strings.Trim(dumpsysOut, " \n"), "(nothing)") { + _, err = cv.Shell(cv.launchCmd) + if err != nil { + return err + } + time.Sleep(2 * time.Second) + _, _, err = req.GetWithTimeout("/ping", time.Second) + return err + } + _, err = cv.Shell(cv.activateCmd) + if err != nil { + return err + } + time.Sleep(time.Second) + _, _, err = req.GetWithTimeout("/ping", time.Second) + return err +} + +func (cv *CvMixIn) preparaPostCvInput(imageData []byte, definitionData []byte) ([]byte, string) { + var b bytes.Buffer + w := multipart.NewWriter(&b) + fwImage, err := w.CreateFormFile("image", "screenshot.png") + if err != nil { + return nil, "" + } + _, err = fwImage.Write(imageData) + if err != nil { + return nil, "" + } + fwDefinition, err := w.CreateFormFile("definition", "definition.json") + if err != nil { + return nil, "" + } + _, err = fwDefinition.Write(definitionData) + if err != nil { + return nil, "" + } + w.Close() + return b.Bytes(), w.FormDataContentType() +} + +func (cv *CvMixIn) CvTemplateMatching(imageData []byte, definitionData []byte) *CvTemplateMatchingResult { + err := cv.testConnectivity() + if err != nil { + return nil + } + inputBytes, contentType := cv.preparaPostCvInput(imageData, definitionData) + if inputBytes == nil { + return nil + } + bytes, _, err := cv.GetCvRequest().PostWithTimeout("/cv", inputBytes, contentType, 2*time.Second) + if err != nil { + return nil + } + result := new(CvTemplateMatchingResult) + err = json.Unmarshal(bytes, result) + if err != nil { + return nil + } + return result +} + +func (cv *CvMixIn) CvContourRect(imageData []byte, definitionData []byte) *CvContourRectResult { + err := cv.testConnectivity() + if err != nil { + return nil + } + inputBytes, contentType := cv.preparaPostCvInput(imageData, definitionData) + if inputBytes == nil { + return nil + } + bytes, _, err := cv.GetCvRequest().PostWithTimeout("/cv", inputBytes, contentType, 2*time.Second) + if err != nil { + return nil + } + result := new(CvContourRectResult) + err = json.Unmarshal(bytes, result) + if err != nil { + return nil + } + return result +} + +func (cv *CvMixIn) CvContourCircle(imageData []byte, definitionData []byte) *CvContourCircleResult { + err := cv.testConnectivity() + if err != nil { + return nil + } + inputBytes, contentType := cv.preparaPostCvInput(imageData, definitionData) + if inputBytes == nil { + return nil + } + bytes, _, err := cv.GetCvRequest().PostWithTimeout("/cv", inputBytes, contentType, 2*time.Second) + if err != nil { + return nil + } + result := new(CvContourCircleResult) + err = json.Unmarshal(bytes, result) + if err != nil { + return nil + } + return result +} diff --git a/apis/device.go b/apis/device.go new file mode 100644 index 0000000..238fbdb --- /dev/null +++ b/apis/device.go @@ -0,0 +1,734 @@ +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 "github.com/fantonglang/go-mobile-automation" + "github.com/fantonglang/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, nil) + 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 +} diff --git a/apis/device_test.go b/apis/device_test.go new file mode 100644 index 0000000..48efc01 --- /dev/null +++ b/apis/device_test.go @@ -0,0 +1,408 @@ +package apis + +import ( + "fmt" + "os" + "testing" + "time" + + openatxclientgo "github.com/fantonglang/go-mobile-automation" +) + +func TestSetNewCommandTimeout(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + err = d.SetNewCommandTimeout(300) + if err != nil { + t.Error("set timeout failed") + return + } +} + +func TestDeviceInfo(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + info, err := d.DeviceInfo() + if err != nil { + t.Error("get device info failed") + return + } + fmt.Println(info) +} + +func TestWindowSize(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + w, h, err := d.WindowSize() + if err != nil { + t.Error("get window size failed") + return + } + fmt.Printf("w: %d, h: %d\n", w, h) +} + +func TestScreenshotSave(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + err = d.ScreenshotSave("sc.png") + if err != nil { + t.Error("screenshot failed") + return + } +} + +func TestDumpHierarchy(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + content, err := d.DumpHierarchy(false, false) + if err != nil { + t.Error("error dump hierachy") + return + } + doc, err := FormatHierachy(content) + if err != nil { + t.Error("error dump hierachy") + return + } + content = doc.OutputXML(true) + fmt.Println(content) +} + +func TestTouch(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + err = d.Touch().Down(0.5, 0.5) + if err != nil { + t.Error("error touch") + return + } + err = d.Touch().Move(0.5, 0.0) + if err != nil { + t.Error("error touch") + return + } + err = d.Touch().Up(0.5, 0.0) + if err != nil { + t.Error("error touch") + return + } +} + +func TestClick(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + err = d.Click(0.481, 0.246) + if err != nil { + t.Error("error click") + return + } +} + +func TestDoubleClick(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + err = d.DoubleClick(0.481, 0.246, 100*time.Millisecond) + if err != nil { + t.Error("error click") + return + } +} + +func TestLongClick(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + err = d.LongClick(0.481, 0.246, 500*time.Millisecond) + if err != nil { + t.Error("error click") + return + } +} + +func TestSwipe(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + err = d.SwipeDefault(0.5, 0.5, 0.5, 0) + if err != nil { + t.Error("error swipe") + return + } +} + +func TestSwipePoints(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + err = d.SwipePoints(0.1, Point4Swipe{0.5, 0.9}, Point4Swipe{0.5, 0.1}) + if err != nil { + t.Error("error swipe") + return + } +} + +func TestDrag(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + err = d.DragDefault(0.5, 0.5, 0.5, 0) + if err != nil { + t.Error("error drag") + return + } +} + +func TestPress(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + err = d.Press2(KEYCODE_WAKEUP) + if err != nil { + t.Error("error press") + return + } +} + +func TestSetOrientation(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + err = d.SetOrientation("n") + if err != nil { + t.Error("error set orientation") + return + } + +} + +func TestLastTraversedText(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + a, err := d.LastTraversedText() + if err != nil { + t.Error("error last_traversed_text") + return + } + fmt.Println(a) +} + +func TestClearTraversedText(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + err = d.ClearTraversedText() + if err != nil { + t.Error("error clear_traversed_text") + return + } +} + +func TestOpenNotification(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + err = d.OpenNotification() + if err != nil { + t.Error("error open_notification") + return + } +} + +func TestOpenQuickSettings(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + err = d.OpenQuickSettings() + if err != nil { + t.Error("error open_quick_settings") + return + } +} + +func TestOpenUrl(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + err = d.OpenUrl("https://www.baidu.com") + if err != nil { + t.Error("error open_url") + return + } +} + +func TestSetClipboard(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + err = d.SetClipboard("aaa") + if err != nil { + t.Error("error clipboard") + return + } +} + +func TestSetClipboard2(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + err = d.SetClipboard2("aaa", "a") + if err != nil { + t.Error("error clipboard") + return + } +} + +func TestGetClipboard(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + a, err := d.GetClipboard() + if err != nil { + t.Error("error clipboard") + return + } + fmt.Println(a) +} + +func TestKeyEvent(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + err = d.KeyEvent(KEYCODE_HOME) + if err != nil { + t.Error("error keyevent") + return + } +} + +func TestShowFloatWindow(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + err = d.ShowFloatWindow(true) + if err != nil { + t.Error("error float window") + return + } +} + +func TestToast(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + a, err := d.Toast().GetMessage(5*time.Second, 5*time.Second, "aaa") + if err != nil { + t.Error("error toast") + return + } + fmt.Println(a) +} + +func TestOpenIdentify(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + err = d.OpenIdentify(IDENTIFY_THEME_RED) + if err != nil { + t.Error("error open identify") + return + } +} + +func TestPath(t *testing.T) { + path := os.Getenv("PATH") + fmt.Println(path) +} + +func TestSlice(t *testing.T) { + a := "12345" + b := a[1 : len(a)-1] + fmt.Println(b) +} diff --git a/apis/ime.go b/apis/ime.go new file mode 100644 index 0000000..34e4f7d --- /dev/null +++ b/apis/ime.go @@ -0,0 +1,153 @@ +package apis + +import ( + "encoding/base64" + "errors" + "regexp" + "strconv" + "strings" + "time" + + m "github.com/fantonglang/go-mobile-automation" +) + +type InputMethodMixIn struct { + m.IOperation +} + +func NewInputMethodMixIn(ops m.IOperation) *InputMethodMixIn { + return &InputMethodMixIn{ + IOperation: ops, + } +} + +func (ime *InputMethodMixIn) set_fastinput_ime(enable bool) error { + fast_ime := "com.github.uiautomator/.FastInputIME" + if enable { + _, err := ime.Shell("ime enable " + fast_ime) + if err != nil { + return err + } + _, err = ime.Shell("ime set " + fast_ime) + if err != nil { + return err + } + } else { + _, err := ime.Shell("ime disable " + fast_ime) + if err != nil { + return err + } + } + return nil +} + +type ImeInfo struct { + MethodId string + Shown bool +} + +func (ime *InputMethodMixIn) current_ime() (*ImeInfo, error) { + out, err := ime.Shell("dumpsys input_method") + if err != nil { + return nil, err + } + re := regexp.MustCompile(`mCurMethodId=([-_./\w]+)`) + matches := re.FindStringSubmatch(out) + if len(matches) == 0 { + return nil, nil + } + shown := false + if strings.Contains(out, "mInputShown=true") { + shown = true + } + return &ImeInfo{ + MethodId: matches[1], + Shown: shown, + }, nil +} + +func (ime *InputMethodMixIn) wait_fastinput_ime(timeout time.Duration) (bool, error) { + now := time.Now() + deadline := now.Add(timeout) + for time.Now().Before(deadline) { + info, err := ime.current_ime() + if err != nil { + return false, err + } + if info == nil || info.MethodId != "com.github.uiautomator/.FastInputIME" { + ime.set_fastinput_ime(true) + time.Sleep(500 * time.Millisecond) + continue + } + if info.Shown { + return true, nil + } + time.Sleep(200 * time.Millisecond) + } + info, err := ime.current_ime() + if err != nil { + return false, err + } + if info == nil || info.MethodId != "com.github.uiautomator/.FastInputIME" { + return false, errors.New("fastInputIME start failed") + } else if info.Shown { + return true, nil + } else { + return false, nil + } + +} + +func (ime *InputMethodMixIn) wait_fastinput_ime_default() (bool, error) { + return ime.wait_fastinput_ime(5 * time.Second) +} + +func (ime *InputMethodMixIn) ClearText() error { + ok, err := ime.wait_fastinput_ime_default() + if err != nil { + return err + } + if !ok { + return nil + } + _, err = ime.Shell("am broadcast -a ADB_CLEAR_TEXT") + return err +} + +const ( + SENDACTION_GO = 2 + SENDACTION_SEARCH = 3 + SENDACTION_SEND = 4 + SENDACTION_NEXT = 5 + SENDACTION_DONE = 6 + SENDACTION_PREVIOUS = 7 +) + +func (ime *InputMethodMixIn) SendAction(code int) error { + ok, err := ime.wait_fastinput_ime_default() + if err != nil { + return err + } + if !ok { + return nil + } + _, err = ime.Shell("am broadcast -a ADB_EDITOR_CODE --ei code " + strconv.Itoa(code)) + return err +} + +func (ime *InputMethodMixIn) SendKeys(text string, clear bool) error { + ok, err := ime.wait_fastinput_ime_default() + if err != nil { + return err + } + if !ok { + return nil + } + b64 := base64.StdEncoding.EncodeToString([]byte(text)) + cmd := "ADB_INPUT_TEXT" + if clear { + cmd = "ADB_SET_TEXT" + } + _, err = ime.Shell("am broadcast -a " + cmd + " --es text " + b64) + return err +} diff --git a/apis/ime_test.go b/apis/ime_test.go new file mode 100644 index 0000000..b54416a --- /dev/null +++ b/apis/ime_test.go @@ -0,0 +1,94 @@ +package apis + +import ( + "fmt" + "testing" + "time" + + openatxclientgo "github.com/fantonglang/go-mobile-automation" +) + +func Test_current_ime(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + ime := NewInputMethodMixIn(o) + info, err := ime.current_ime() + if err != nil { + t.Error("error current ime") + return + } + fmt.Println(*info) +} + +func Test_set_fastinput_ime(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + ime := NewInputMethodMixIn(o) + err = ime.set_fastinput_ime(true) + if err != nil { + t.Error("error set ime") + return + } +} + +func Test_wait_fastinput_ime(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + ime := NewInputMethodMixIn(o) + _, err = ime.wait_fastinput_ime(5 * time.Second) + if err != nil { + t.Error("error wait ime") + return + } +} + +func TestClearText(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + ime := NewInputMethodMixIn(o) + err = ime.ClearText() + if err != nil { + t.Error("error clear test") + return + } +} + +func TestSendAction(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + ime := NewInputMethodMixIn(o) + err = ime.SendAction(SENDACTION_SEARCH) + if err != nil { + t.Error("error send action") + return + } +} + +func TestSendKeys(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + ime := NewInputMethodMixIn(o) + err = ime.SendKeys("aaa", true) + if err != nil { + t.Error("error send keys") + return + } +} diff --git a/apis/instance.go b/apis/instance.go new file mode 100644 index 0000000..c6befb7 --- /dev/null +++ b/apis/instance.go @@ -0,0 +1,18 @@ +package apis + +import openatxclientgo "github.com/fantonglang/go-mobile-automation" + +func NewHostDevice(deviceId string) (*Device, error) { + o, err := openatxclientgo.NewHostOperation(deviceId) + if err != nil { + return nil, err + } + d := NewDevice(o) + return d, nil +} + +func NewNativeDevice() *Device { + o := openatxclientgo.NewDeviceOperation() + d := NewDevice(o) + return d +} diff --git a/apis/keycode.go b/apis/keycode.go new file mode 100644 index 0000000..69d80ce --- /dev/null +++ b/apis/keycode.go @@ -0,0 +1,966 @@ +package apis + +const ( + /** Key code constant: Unknown key code. */ + KEYCODE_UNKNOWN = 0 + /** Key code constant: Soft Left key. + * Usually situated below the display on phones and used as a multi-function + * feature key for selecting a software defined function shown on the bottom left + * of the display. */ + KEYCODE_SOFT_LEFT = 1 + /** Key code constant: Soft Right key. + * Usually situated below the display on phones and used as a multi-function + * feature key for selecting a software defined function shown on the bottom right + * of the display. */ + KEYCODE_SOFT_RIGHT = 2 + /** Key code constant: Home key. + * This key is handled by the framework and is never delivered to applications. */ + KEYCODE_HOME = 3 + /** Key code constant: Back key. */ + KEYCODE_BACK = 4 + /** Key code constant: Call key. */ + KEYCODE_CALL = 5 + /** Key code constant: End Call key. */ + KEYCODE_ENDCALL = 6 + /** Key code constant: '0' key. */ + KEYCODE_0 = 7 + /** Key code constant: '1' key. */ + KEYCODE_1 = 8 + /** Key code constant: '2' key. */ + KEYCODE_2 = 9 + /** Key code constant: '3' key. */ + KEYCODE_3 = 10 + /** Key code constant: '4' key. */ + KEYCODE_4 = 11 + /** Key code constant: '5' key. */ + KEYCODE_5 = 12 + /** Key code constant: '6' key. */ + KEYCODE_6 = 13 + /** Key code constant: '7' key. */ + KEYCODE_7 = 14 + /** Key code constant: '8' key. */ + KEYCODE_8 = 15 + /** Key code constant: '9' key. */ + KEYCODE_9 = 16 + /** Key code constant: '*' key. */ + KEYCODE_STAR = 17 + /** Key code constant: '#' key. */ + KEYCODE_POUND = 18 + /** Key code constant: Directional Pad Up key. + * May also be synthesized from trackball motions. */ + KEYCODE_DPAD_UP = 19 + /** Key code constant: Directional Pad Down key. + * May also be synthesized from trackball motions. */ + KEYCODE_DPAD_DOWN = 20 + /** Key code constant: Directional Pad Left key. + * May also be synthesized from trackball motions. */ + KEYCODE_DPAD_LEFT = 21 + /** Key code constant: Directional Pad Right key. + * May also be synthesized from trackball motions. */ + KEYCODE_DPAD_RIGHT = 22 + /** Key code constant: Directional Pad Center key. + * May also be synthesized from trackball motions. */ + KEYCODE_DPAD_CENTER = 23 + /** Key code constant: Volume Up key. + * Adjusts the speaker volume up. */ + KEYCODE_VOLUME_UP = 24 + /** Key code constant: Volume Down key. + * Adjusts the speaker volume down. */ + KEYCODE_VOLUME_DOWN = 25 + /** Key code constant: Power key. */ + KEYCODE_POWER = 26 + /** Key code constant: Camera key. + * Used to launch a camera application or take pictures. */ + KEYCODE_CAMERA = 27 + /** Key code constant: Clear key. */ + KEYCODE_CLEAR = 28 + /** Key code constant: 'A' key. */ + KEYCODE_A = 29 + /** Key code constant: 'B' key. */ + KEYCODE_B = 30 + /** Key code constant: 'C' key. */ + KEYCODE_C = 31 + /** Key code constant: 'D' key. */ + KEYCODE_D = 32 + /** Key code constant: 'E' key. */ + KEYCODE_E = 33 + /** Key code constant: 'F' key. */ + KEYCODE_F = 34 + /** Key code constant: 'G' key. */ + KEYCODE_G = 35 + /** Key code constant: 'H' key. */ + KEYCODE_H = 36 + /** Key code constant: 'I' key. */ + KEYCODE_I = 37 + /** Key code constant: 'J' key. */ + KEYCODE_J = 38 + /** Key code constant: 'K' key. */ + KEYCODE_K = 39 + /** Key code constant: 'L' key. */ + KEYCODE_L = 40 + /** Key code constant: 'M' key. */ + KEYCODE_M = 41 + /** Key code constant: 'N' key. */ + KEYCODE_N = 42 + /** Key code constant: 'O' key. */ + KEYCODE_O = 43 + /** Key code constant: 'P' key. */ + KEYCODE_P = 44 + /** Key code constant: 'Q' key. */ + KEYCODE_Q = 45 + /** Key code constant: 'R' key. */ + KEYCODE_R = 46 + /** Key code constant: 'S' key. */ + KEYCODE_S = 47 + /** Key code constant: 'T' key. */ + KEYCODE_T = 48 + /** Key code constant: 'U' key. */ + KEYCODE_U = 49 + /** Key code constant: 'V' key. */ + KEYCODE_V = 50 + /** Key code constant: 'W' key. */ + KEYCODE_W = 51 + /** Key code constant: 'X' key. */ + KEYCODE_X = 52 + /** Key code constant: 'Y' key. */ + KEYCODE_Y = 53 + /** Key code constant: 'Z' key. */ + KEYCODE_Z = 54 + /** Key code constant: ',' key. */ + KEYCODE_COMMA = 55 + /** Key code constant: '.' key. */ + KEYCODE_PERIOD = 56 + /** Key code constant: Left Alt modifier key. */ + KEYCODE_ALT_LEFT = 57 + /** Key code constant: Right Alt modifier key. */ + KEYCODE_ALT_RIGHT = 58 + /** Key code constant: Left Shift modifier key. */ + KEYCODE_SHIFT_LEFT = 59 + /** Key code constant: Right Shift modifier key. */ + KEYCODE_SHIFT_RIGHT = 60 + /** Key code constant: Tab key. */ + KEYCODE_TAB = 61 + /** Key code constant: Space key. */ + KEYCODE_SPACE = 62 + /** Key code constant: Symbol modifier key. + * Used to enter alternate symbols. */ + KEYCODE_SYM = 63 + /** Key code constant: Explorer special function key. + * Used to launch a browser application. */ + KEYCODE_EXPLORER = 64 + /** Key code constant: Envelope special function key. + * Used to launch a mail application. */ + KEYCODE_ENVELOPE = 65 + /** Key code constant: Enter key. */ + KEYCODE_ENTER = 66 + /** Key code constant: Backspace key. + * Deletes characters before the insertion point, unlike {@link #KEYCODE_FORWARD_DEL}. */ + KEYCODE_DEL = 67 + /** Key code constant: '`' (backtick) key. */ + KEYCODE_GRAVE = 68 + /** Key code constant: '-'. */ + KEYCODE_MINUS = 69 + /** Key code constant: '=' key. */ + KEYCODE_EQUALS = 70 + /** Key code constant: '[' key. */ + KEYCODE_LEFT_BRACKET = 71 + /** Key code constant: ']' key. */ + KEYCODE_RIGHT_BRACKET = 72 + /** Key code constant: '\' key. */ + KEYCODE_BACKSLASH = 73 + /** Key code constant: '' key. */ + KEYCODE_SEMICOLON = 74 + /** Key code constant: ''' (apostrophe) key. */ + KEYCODE_APOSTROPHE = 75 + /** Key code constant: '/' key. */ + KEYCODE_SLASH = 76 + /** Key code constant: '@' key. */ + KEYCODE_AT = 77 + /** Key code constant: Number modifier key. + * Used to enter numeric symbols. + * This key is not Num Lock it is more like {@link #KEYCODE_ALT_LEFT} and is + * interpreted as an ALT key by {@link android.text.method.MetaKeyKeyListener}. */ + KEYCODE_NUM = 78 + /** Key code constant: Headset Hook key. + * Used to hang up calls and stop media. */ + KEYCODE_HEADSETHOOK = 79 + /** Key code constant: Camera Focus key. + * Used to focus the camera. */ + KEYCODE_FOCUS = 80 // *Camera* focus + /** Key code constant: '+' key. */ + KEYCODE_PLUS = 81 + /** Key code constant: Menu key. */ + KEYCODE_MENU = 82 + /** Key code constant: Notification key. */ + KEYCODE_NOTIFICATION = 83 + /** Key code constant: Search key. */ + KEYCODE_SEARCH = 84 + /** Key code constant: Play/Pause media key. */ + KEYCODE_MEDIA_PLAY_PAUSE = 85 + /** Key code constant: Stop media key. */ + KEYCODE_MEDIA_STOP = 86 + /** Key code constant: Play Next media key. */ + KEYCODE_MEDIA_NEXT = 87 + /** Key code constant: Play Previous media key. */ + KEYCODE_MEDIA_PREVIOUS = 88 + /** Key code constant: Rewind media key. */ + KEYCODE_MEDIA_REWIND = 89 + /** Key code constant: Fast Forward media key. */ + KEYCODE_MEDIA_FAST_FORWARD = 90 + /** Key code constant: Mute key. + * Mutes the microphone, unlike {@link #KEYCODE_VOLUME_MUTE}. */ + KEYCODE_MUTE = 91 + /** Key code constant: Page Up key. */ + KEYCODE_PAGE_UP = 92 + /** Key code constant: Page Down key. */ + KEYCODE_PAGE_DOWN = 93 + /** Key code constant: Picture Symbols modifier key. + * Used to switch symbol sets (Emoji, Kao-moji). */ + KEYCODE_PICTSYMBOLS = 94 // switch symbol-sets (Emoji,Kao-moji) + /** Key code constant: Switch Charset modifier key. + * Used to switch character sets (Kanji, Katakana). */ + KEYCODE_SWITCH_CHARSET = 95 // switch char-sets (Kanji,Katakana) + /** Key code constant: A Button key. + * On a game controller, the A button should be either the button labeled A + * or the first button on the bottom row of controller buttons. */ + KEYCODE_BUTTON_A = 96 + /** Key code constant: B Button key. + * On a game controller, the B button should be either the button labeled B + * or the second button on the bottom row of controller buttons. */ + KEYCODE_BUTTON_B = 97 + /** Key code constant: C Button key. + * On a game controller, the C button should be either the button labeled C + * or the third button on the bottom row of controller buttons. */ + KEYCODE_BUTTON_C = 98 + /** Key code constant: X Button key. + * On a game controller, the X button should be either the button labeled X + * or the first button on the upper row of controller buttons. */ + KEYCODE_BUTTON_X = 99 + /** Key code constant: Y Button key. + * On a game controller, the Y button should be either the button labeled Y + * or the second button on the upper row of controller buttons. */ + KEYCODE_BUTTON_Y = 100 + /** Key code constant: Z Button key. + * On a game controller, the Z button should be either the button labeled Z + * or the third button on the upper row of controller buttons. */ + KEYCODE_BUTTON_Z = 101 + /** Key code constant: L1 Button key. + * On a game controller, the L1 button should be either the button labeled L1 (or L) + * or the top left trigger button. */ + KEYCODE_BUTTON_L1 = 102 + /** Key code constant: R1 Button key. + * On a game controller, the R1 button should be either the button labeled R1 (or R) + * or the top right trigger button. */ + KEYCODE_BUTTON_R1 = 103 + /** Key code constant: L2 Button key. + * On a game controller, the L2 button should be either the button labeled L2 + * or the bottom left trigger button. */ + KEYCODE_BUTTON_L2 = 104 + /** Key code constant: R2 Button key. + * On a game controller, the R2 button should be either the button labeled R2 + * or the bottom right trigger button. */ + KEYCODE_BUTTON_R2 = 105 + /** Key code constant: Left Thumb Button key. + * On a game controller, the left thumb button indicates that the left (or only) + * joystick is pressed. */ + KEYCODE_BUTTON_THUMBL = 106 + /** Key code constant: Right Thumb Button key. + * On a game controller, the right thumb button indicates that the right + * joystick is pressed. */ + KEYCODE_BUTTON_THUMBR = 107 + /** Key code constant: Start Button key. + * On a game controller, the button labeled Start. */ + KEYCODE_BUTTON_START = 108 + /** Key code constant: Select Button key. + * On a game controller, the button labeled Select. */ + KEYCODE_BUTTON_SELECT = 109 + /** Key code constant: Mode Button key. + * On a game controller, the button labeled Mode. */ + KEYCODE_BUTTON_MODE = 110 + /** Key code constant: Escape key. */ + KEYCODE_ESCAPE = 111 + /** Key code constant: Forward Delete key. + * Deletes characters ahead of the insertion point, unlike {@link #KEYCODE_DEL}. */ + KEYCODE_FORWARD_DEL = 112 + /** Key code constant: Left Control modifier key. */ + KEYCODE_CTRL_LEFT = 113 + /** Key code constant: Right Control modifier key. */ + KEYCODE_CTRL_RIGHT = 114 + /** Key code constant: Caps Lock key. */ + KEYCODE_CAPS_LOCK = 115 + /** Key code constant: Scroll Lock key. */ + KEYCODE_SCROLL_LOCK = 116 + /** Key code constant: Left Meta modifier key. */ + KEYCODE_META_LEFT = 117 + /** Key code constant: Right Meta modifier key. */ + KEYCODE_META_RIGHT = 118 + /** Key code constant: Function modifier key. */ + KEYCODE_FUNCTION = 119 + /** Key code constant: System Request / Print Screen key. */ + KEYCODE_SYSRQ = 120 + /** Key code constant: Break / Pause key. */ + KEYCODE_BREAK = 121 + /** Key code constant: Home Movement key. + * Used for scrolling or moving the cursor around to the start of a line + * or to the top of a list. */ + KEYCODE_MOVE_HOME = 122 + /** Key code constant: End Movement key. + * Used for scrolling or moving the cursor around to the end of a line + * or to the bottom of a list. */ + KEYCODE_MOVE_END = 123 + /** Key code constant: Insert key. + * Toggles insert / overwrite edit mode. */ + KEYCODE_INSERT = 124 + /** Key code constant: Forward key. + * Navigates forward in the history stack. Complement of {@link #KEYCODE_BACK}. */ + KEYCODE_FORWARD = 125 + /** Key code constant: Play media key. */ + KEYCODE_MEDIA_PLAY = 126 + /** Key code constant: Pause media key. */ + KEYCODE_MEDIA_PAUSE = 127 + /** Key code constant: Close media key. + * May be used to close a CD tray, for example. */ + KEYCODE_MEDIA_CLOSE = 128 + /** Key code constant: Eject media key. + * May be used to eject a CD tray, for example. */ + KEYCODE_MEDIA_EJECT = 129 + /** Key code constant: Record media key. */ + KEYCODE_MEDIA_RECORD = 130 + /** Key code constant: F1 key. */ + KEYCODE_F1 = 131 + /** Key code constant: F2 key. */ + KEYCODE_F2 = 132 + /** Key code constant: F3 key. */ + KEYCODE_F3 = 133 + /** Key code constant: F4 key. */ + KEYCODE_F4 = 134 + /** Key code constant: F5 key. */ + KEYCODE_F5 = 135 + /** Key code constant: F6 key. */ + KEYCODE_F6 = 136 + /** Key code constant: F7 key. */ + KEYCODE_F7 = 137 + /** Key code constant: F8 key. */ + KEYCODE_F8 = 138 + /** Key code constant: F9 key. */ + KEYCODE_F9 = 139 + /** Key code constant: F10 key. */ + KEYCODE_F10 = 140 + /** Key code constant: F11 key. */ + KEYCODE_F11 = 141 + /** Key code constant: F12 key. */ + KEYCODE_F12 = 142 + /** Key code constant: Num Lock key. + * This is the Num Lock key it is different from {@link #KEYCODE_NUM}. + * This key alters the behavior of other keys on the numeric keypad. */ + KEYCODE_NUM_LOCK = 143 + /** Key code constant: Numeric keypad '0' key. */ + KEYCODE_NUMPAD_0 = 144 + /** Key code constant: Numeric keypad '1' key. */ + KEYCODE_NUMPAD_1 = 145 + /** Key code constant: Numeric keypad '2' key. */ + KEYCODE_NUMPAD_2 = 146 + /** Key code constant: Numeric keypad '3' key. */ + KEYCODE_NUMPAD_3 = 147 + /** Key code constant: Numeric keypad '4' key. */ + KEYCODE_NUMPAD_4 = 148 + /** Key code constant: Numeric keypad '5' key. */ + KEYCODE_NUMPAD_5 = 149 + /** Key code constant: Numeric keypad '6' key. */ + KEYCODE_NUMPAD_6 = 150 + /** Key code constant: Numeric keypad '7' key. */ + KEYCODE_NUMPAD_7 = 151 + /** Key code constant: Numeric keypad '8' key. */ + KEYCODE_NUMPAD_8 = 152 + /** Key code constant: Numeric keypad '9' key. */ + KEYCODE_NUMPAD_9 = 153 + /** Key code constant: Numeric keypad '/' key (for division). */ + KEYCODE_NUMPAD_DIVIDE = 154 + /** Key code constant: Numeric keypad '*' key (for multiplication). */ + KEYCODE_NUMPAD_MULTIPLY = 155 + /** Key code constant: Numeric keypad '-' key (for subtraction). */ + KEYCODE_NUMPAD_SUBTRACT = 156 + /** Key code constant: Numeric keypad '+' key (for addition). */ + KEYCODE_NUMPAD_ADD = 157 + /** Key code constant: Numeric keypad '.' key (for decimals or digit grouping). */ + KEYCODE_NUMPAD_DOT = 158 + /** Key code constant: Numeric keypad ',' key (for decimals or digit grouping). */ + KEYCODE_NUMPAD_COMMA = 159 + /** Key code constant: Numeric keypad Enter key. */ + KEYCODE_NUMPAD_ENTER = 160 + /** Key code constant: Numeric keypad '=' key. */ + KEYCODE_NUMPAD_EQUALS = 161 + /** Key code constant: Numeric keypad '(' key. */ + KEYCODE_NUMPAD_LEFT_PAREN = 162 + /** Key code constant: Numeric keypad ')' key. */ + KEYCODE_NUMPAD_RIGHT_PAREN = 163 + /** Key code constant: Volume Mute key. + * Mutes the speaker, unlike {@link #KEYCODE_MUTE}. + * This key should normally be implemented as a toggle such that the first press + * mutes the speaker and the second press restores the original volume. */ + KEYCODE_VOLUME_MUTE = 164 + /** Key code constant: Info key. + * Common on TV remotes to show additional information related to what is + * currently being viewed. */ + KEYCODE_INFO = 165 + /** Key code constant: Channel up key. + * On TV remotes, increments the television channel. */ + KEYCODE_CHANNEL_UP = 166 + /** Key code constant: Channel down key. + * On TV remotes, decrements the television channel. */ + KEYCODE_CHANNEL_DOWN = 167 + /** Key code constant: Zoom in key. */ + KEYCODE_ZOOM_IN = 168 + /** Key code constant: Zoom out key. */ + KEYCODE_ZOOM_OUT = 169 + /** Key code constant: TV key. + * On TV remotes, switches to viewing live TV. */ + KEYCODE_TV = 170 + /** Key code constant: Window key. + * On TV remotes, toggles picture-in-picture mode or other windowing functions. + * On Android Wear devices, triggers a display offset. */ + KEYCODE_WINDOW = 171 + /** Key code constant: Guide key. + * On TV remotes, shows a programming guide. */ + KEYCODE_GUIDE = 172 + /** Key code constant: DVR key. + * On some TV remotes, switches to a DVR mode for recorded shows. */ + KEYCODE_DVR = 173 + /** Key code constant: Bookmark key. + * On some TV remotes, bookmarks content or web pages. */ + KEYCODE_BOOKMARK = 174 + /** Key code constant: Toggle captions key. + * Switches the mode for closed-captioning text, for example during television shows. */ + KEYCODE_CAPTIONS = 175 + /** Key code constant: Settings key. + * Starts the system settings activity. */ + KEYCODE_SETTINGS = 176 + /** + * Key code constant: TV power key. + * On HDMI TV panel devices and Android TV devices that don't support HDMI, toggles the power + * state of the device. + * On HDMI source devices, toggles the power state of the HDMI-connected TV via HDMI-CEC and + * makes the source device follow this power state. + */ + KEYCODE_TV_POWER = 177 + /** Key code constant: TV input key. + * On TV remotes, switches the input on a television screen. */ + KEYCODE_TV_INPUT = 178 + /** Key code constant: Set-top-box power key. + * On TV remotes, toggles the power on an external Set-top-box. */ + KEYCODE_STB_POWER = 179 + /** Key code constant: Set-top-box input key. + * On TV remotes, switches the input mode on an external Set-top-box. */ + KEYCODE_STB_INPUT = 180 + /** Key code constant: A/V Receiver power key. + * On TV remotes, toggles the power on an external A/V Receiver. */ + KEYCODE_AVR_POWER = 181 + /** Key code constant: A/V Receiver input key. + * On TV remotes, switches the input mode on an external A/V Receiver. */ + KEYCODE_AVR_INPUT = 182 + /** Key code constant: Red "programmable" key. + * On TV remotes, acts as a contextual/programmable key. */ + KEYCODE_PROG_RED = 183 + /** Key code constant: Green "programmable" key. + * On TV remotes, actsas a contextual/programmable key. */ + KEYCODE_PROG_GREEN = 184 + /** Key code constant: Yellow "programmable" key. + * On TV remotes, acts as a contextual/programmable key. */ + KEYCODE_PROG_YELLOW = 185 + /** Key code constant: Blue "programmable" key. + * On TV remotes, acts as a contextual/programmable key. */ + KEYCODE_PROG_BLUE = 186 + /** Key code constant: App switch key. + * Should bring up the application switcher dialog. */ + KEYCODE_APP_SWITCH = 187 + /** Key code constant: Generic Game Pad Button #1.*/ + KEYCODE_BUTTON_1 = 188 + /** Key code constant: Generic Game Pad Button #2.*/ + KEYCODE_BUTTON_2 = 189 + /** Key code constant: Generic Game Pad Button #3.*/ + KEYCODE_BUTTON_3 = 190 + /** Key code constant: Generic Game Pad Button #4.*/ + KEYCODE_BUTTON_4 = 191 + /** Key code constant: Generic Game Pad Button #5.*/ + KEYCODE_BUTTON_5 = 192 + /** Key code constant: Generic Game Pad Button #6.*/ + KEYCODE_BUTTON_6 = 193 + /** Key code constant: Generic Game Pad Button #7.*/ + KEYCODE_BUTTON_7 = 194 + /** Key code constant: Generic Game Pad Button #8.*/ + KEYCODE_BUTTON_8 = 195 + /** Key code constant: Generic Game Pad Button #9.*/ + KEYCODE_BUTTON_9 = 196 + /** Key code constant: Generic Game Pad Button #10.*/ + KEYCODE_BUTTON_10 = 197 + /** Key code constant: Generic Game Pad Button #11.*/ + KEYCODE_BUTTON_11 = 198 + /** Key code constant: Generic Game Pad Button #12.*/ + KEYCODE_BUTTON_12 = 199 + /** Key code constant: Generic Game Pad Button #13.*/ + KEYCODE_BUTTON_13 = 200 + /** Key code constant: Generic Game Pad Button #14.*/ + KEYCODE_BUTTON_14 = 201 + /** Key code constant: Generic Game Pad Button #15.*/ + KEYCODE_BUTTON_15 = 202 + /** Key code constant: Generic Game Pad Button #16.*/ + KEYCODE_BUTTON_16 = 203 + /** Key code constant: Language Switch key. + * Toggles the current input language such as switching between English and Japanese on + * a QWERTY keyboard. On some devices, the same function may be performed by + * pressing Shift+Spacebar. */ + KEYCODE_LANGUAGE_SWITCH = 204 + /** Key code constant: Manner Mode key. + * Toggles silent or vibrate mode on and off to make the device behave more politely + * in certain settings such as on a crowded train. On some devices, the key may only + * operate when long-pressed. */ + KEYCODE_MANNER_MODE = 205 + /** Key code constant: 3D Mode key. + * Toggles the display between 2D and 3D mode. */ + KEYCODE_3D_MODE = 206 + /** Key code constant: Contacts special function key. + * Used to launch an address book application. */ + KEYCODE_CONTACTS = 207 + /** Key code constant: Calendar special function key. + * Used to launch a calendar application. */ + KEYCODE_CALENDAR = 208 + /** Key code constant: Music special function key. + * Used to launch a music player application. */ + KEYCODE_MUSIC = 209 + /** Key code constant: Calculator special function key. + * Used to launch a calculator application. */ + KEYCODE_CALCULATOR = 210 + /** Key code constant: Japanese full-width / half-width key. */ + KEYCODE_ZENKAKU_HANKAKU = 211 + /** Key code constant: Japanese alphanumeric key. */ + KEYCODE_EISU = 212 + /** Key code constant: Japanese non-conversion key. */ + KEYCODE_MUHENKAN = 213 + /** Key code constant: Japanese conversion key. */ + KEYCODE_HENKAN = 214 + /** Key code constant: Japanese katakana / hiragana key. */ + KEYCODE_KATAKANA_HIRAGANA = 215 + /** Key code constant: Japanese Yen key. */ + KEYCODE_YEN = 216 + /** Key code constant: Japanese Ro key. */ + KEYCODE_RO = 217 + /** Key code constant: Japanese kana key. */ + KEYCODE_KANA = 218 + /** Key code constant: Assist key. + * Launches the global assist activity. Not delivered to applications. */ + KEYCODE_ASSIST = 219 + /** Key code constant: Brightness Down key. + * Adjusts the screen brightness down. */ + KEYCODE_BRIGHTNESS_DOWN = 220 + /** Key code constant: Brightness Up key. + * Adjusts the screen brightness up. */ + KEYCODE_BRIGHTNESS_UP = 221 + /** Key code constant: Audio Track key. + * Switches the audio tracks. */ + KEYCODE_MEDIA_AUDIO_TRACK = 222 + /** Key code constant: Sleep key. + * Puts the device to sleep. Behaves somewhat like {@link #KEYCODE_POWER} but it + * has no effect if the device is already asleep. */ + KEYCODE_SLEEP = 223 + /** Key code constant: Wakeup key. + * Wakes up the device. Behaves somewhat like {@link #KEYCODE_POWER} but it + * has no effect if the device is already awake. */ + KEYCODE_WAKEUP = 224 + /** Key code constant: Pairing key. + * Initiates peripheral pairing mode. Useful for pairing remote control + * devices or game controllers, especially if no other input mode is + * available. */ + KEYCODE_PAIRING = 225 + /** Key code constant: Media Top Menu key. + * Goes to the top of media menu. */ + KEYCODE_MEDIA_TOP_MENU = 226 + /** Key code constant: '11' key. */ + KEYCODE_11 = 227 + /** Key code constant: '12' key. */ + KEYCODE_12 = 228 + /** Key code constant: Last Channel key. + * Goes to the last viewed channel. */ + KEYCODE_LAST_CHANNEL = 229 + /** Key code constant: TV data service key. + * Displays data services like weather, sports. */ + KEYCODE_TV_DATA_SERVICE = 230 + /** Key code constant: Voice Assist key. + * Launches the global voice assist activity. Not delivered to applications. */ + KEYCODE_VOICE_ASSIST = 231 + /** Key code constant: Radio key. + * Toggles TV service / Radio service. */ + KEYCODE_TV_RADIO_SERVICE = 232 + /** Key code constant: Teletext key. + * Displays Teletext service. */ + KEYCODE_TV_TELETEXT = 233 + /** Key code constant: Number entry key. + * Initiates to enter multi-digit channel nubmber when each digit key is assigned + * for selecting separate channel. Corresponds to Number Entry Mode (0x1D) of CEC + * User Control Code. */ + KEYCODE_TV_NUMBER_ENTRY = 234 + /** Key code constant: Analog Terrestrial key. + * Switches to analog terrestrial broadcast service. */ + KEYCODE_TV_TERRESTRIAL_ANALOG = 235 + /** Key code constant: Digital Terrestrial key. + * Switches to digital terrestrial broadcast service. */ + KEYCODE_TV_TERRESTRIAL_DIGITAL = 236 + /** Key code constant: Satellite key. + * Switches to digital satellite broadcast service. */ + KEYCODE_TV_SATELLITE = 237 + /** Key code constant: BS key. + * Switches to BS digital satellite broadcasting service available in Japan. */ + KEYCODE_TV_SATELLITE_BS = 238 + /** Key code constant: CS key. + * Switches to CS digital satellite broadcasting service available in Japan. */ + KEYCODE_TV_SATELLITE_CS = 239 + /** Key code constant: BS/CS key. + * Toggles between BS and CS digital satellite services. */ + KEYCODE_TV_SATELLITE_SERVICE = 240 + /** Key code constant: Toggle Network key. + * Toggles selecting broacast services. */ + KEYCODE_TV_NETWORK = 241 + /** Key code constant: Antenna/Cable key. + * Toggles broadcast input source between antenna and cable. */ + KEYCODE_TV_ANTENNA_CABLE = 242 + /** Key code constant: HDMI #1 key. + * Switches to HDMI input #1. */ + KEYCODE_TV_INPUT_HDMI_1 = 243 + /** Key code constant: HDMI #2 key. + * Switches to HDMI input #2. */ + KEYCODE_TV_INPUT_HDMI_2 = 244 + /** Key code constant: HDMI #3 key. + * Switches to HDMI input #3. */ + KEYCODE_TV_INPUT_HDMI_3 = 245 + /** Key code constant: HDMI #4 key. + * Switches to HDMI input #4. */ + KEYCODE_TV_INPUT_HDMI_4 = 246 + /** Key code constant: Composite #1 key. + * Switches to composite video input #1. */ + KEYCODE_TV_INPUT_COMPOSITE_1 = 247 + /** Key code constant: Composite #2 key. + * Switches to composite video input #2. */ + KEYCODE_TV_INPUT_COMPOSITE_2 = 248 + /** Key code constant: Component #1 key. + * Switches to component video input #1. */ + KEYCODE_TV_INPUT_COMPONENT_1 = 249 + /** Key code constant: Component #2 key. + * Switches to component video input #2. */ + KEYCODE_TV_INPUT_COMPONENT_2 = 250 + /** Key code constant: VGA #1 key. + * Switches to VGA (analog RGB) input #1. */ + KEYCODE_TV_INPUT_VGA_1 = 251 + /** Key code constant: Audio description key. + * Toggles audio description off / on. */ + KEYCODE_TV_AUDIO_DESCRIPTION = 252 + /** Key code constant: Audio description mixing volume up key. + * Louden audio description volume as compared with normal audio volume. */ + KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP = 253 + /** Key code constant: Audio description mixing volume down key. + * Lessen audio description volume as compared with normal audio volume. */ + KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN = 254 + /** Key code constant: Zoom mode key. + * Changes Zoom mode (Normal, Full, Zoom, Wide-zoom, etc.) */ + KEYCODE_TV_ZOOM_MODE = 255 + /** Key code constant: Contents menu key. + * Goes to the title list. Corresponds to Contents Menu (0x0B) of CEC User Control + * Code */ + KEYCODE_TV_CONTENTS_MENU = 256 + /** Key code constant: Media context menu key. + * Goes to the context menu of media contents. Corresponds to Media Context-sensitive + * Menu (0x11) of CEC User Control Code. */ + KEYCODE_TV_MEDIA_CONTEXT_MENU = 257 + /** Key code constant: Timer programming key. + * Goes to the timer recording menu. Corresponds to Timer Programming (0x54) of + * CEC User Control Code. */ + KEYCODE_TV_TIMER_PROGRAMMING = 258 + /** Key code constant: Help key. */ + KEYCODE_HELP = 259 + /** Key code constant: Navigate to previous key. + * Goes backward by one item in an ordered collection of items. */ + KEYCODE_NAVIGATE_PREVIOUS = 260 + /** Key code constant: Navigate to next key. + * Advances to the next item in an ordered collection of items. */ + KEYCODE_NAVIGATE_NEXT = 261 + /** Key code constant: Navigate in key. + * Activates the item that currently has focus or expands to the next level of a navigation + * hierarchy. */ + KEYCODE_NAVIGATE_IN = 262 + /** Key code constant: Navigate out key. + * Backs out one level of a navigation hierarchy or collapses the item that currently has + * focus. */ + KEYCODE_NAVIGATE_OUT = 263 + /** Key code constant: Primary stem key for Wear + * Main power/reset button on watch. */ + KEYCODE_STEM_PRIMARY = 264 + /** Key code constant: Generic stem key 1 for Wear */ + KEYCODE_STEM_1 = 265 + /** Key code constant: Generic stem key 2 for Wear */ + KEYCODE_STEM_2 = 266 + /** Key code constant: Generic stem key 3 for Wear */ + KEYCODE_STEM_3 = 267 + /** Key code constant: Directional Pad Up-Left */ + KEYCODE_DPAD_UP_LEFT = 268 + /** Key code constant: Directional Pad Down-Left */ + KEYCODE_DPAD_DOWN_LEFT = 269 + /** Key code constant: Directional Pad Up-Right */ + KEYCODE_DPAD_UP_RIGHT = 270 + /** Key code constant: Directional Pad Down-Right */ + KEYCODE_DPAD_DOWN_RIGHT = 271 + /** Key code constant: Skip forward media key. */ + KEYCODE_MEDIA_SKIP_FORWARD = 272 + /** Key code constant: Skip backward media key. */ + KEYCODE_MEDIA_SKIP_BACKWARD = 273 + /** Key code constant: Step forward media key. + * Steps media forward, one frame at a time. */ + KEYCODE_MEDIA_STEP_FORWARD = 274 + /** Key code constant: Step backward media key. + * Steps media backward, one frame at a time. */ + KEYCODE_MEDIA_STEP_BACKWARD = 275 + /** Key code constant: put device to sleep unless a wakelock is held. */ + KEYCODE_SOFT_SLEEP = 276 + /** Key code constant: Cut key. */ + KEYCODE_CUT = 277 + /** Key code constant: Copy key. */ + KEYCODE_COPY = 278 + /** Key code constant: Paste key. */ + KEYCODE_PASTE = 279 + /** Key code constant: Consumed by the system for navigation up */ + KEYCODE_SYSTEM_NAVIGATION_UP = 280 + /** Key code constant: Consumed by the system for navigation down */ + KEYCODE_SYSTEM_NAVIGATION_DOWN = 281 + /** Key code constant: Consumed by the system for navigation left*/ + KEYCODE_SYSTEM_NAVIGATION_LEFT = 282 + /** Key code constant: Consumed by the system for navigation right */ + KEYCODE_SYSTEM_NAVIGATION_RIGHT = 283 + /** Key code constant: Show all apps */ + KEYCODE_ALL_APPS = 284 + /** Key code constant: Refresh key. */ + KEYCODE_REFRESH = 285 + /** Key code constant: Thumbs up key. Apps can use this to let user upvote content. */ + KEYCODE_THUMBS_UP = 286 + /** Key code constant: Thumbs down key. Apps can use this to let user downvote content. */ + KEYCODE_THUMBS_DOWN = 287 + /** + * Key code constant: Used to switch current {@link android.accounts.Account} that is + * consuming content. May be consumed by system to set account globally. + */ + KEYCODE_PROFILE_SWITCH = 288 + /** Key code constant: Video Application key #1. */ + KEYCODE_VIDEO_APP_1 = 289 + /** Key code constant: Video Application key #2. */ + KEYCODE_VIDEO_APP_2 = 290 + /** Key code constant: Video Application key #3. */ + KEYCODE_VIDEO_APP_3 = 291 + /** Key code constant: Video Application key #4. */ + KEYCODE_VIDEO_APP_4 = 292 + /** Key code constant: Video Application key #5. */ + KEYCODE_VIDEO_APP_5 = 293 + /** Key code constant: Video Application key #6. */ + KEYCODE_VIDEO_APP_6 = 294 + /** Key code constant: Video Application key #7. */ + KEYCODE_VIDEO_APP_7 = 295 + /** Key code constant: Video Application key #8. */ + KEYCODE_VIDEO_APP_8 = 296 + /** Key code constant: Featured Application key #1. */ + KEYCODE_FEATURED_APP_1 = 297 + /** Key code constant: Featured Application key #2. */ + KEYCODE_FEATURED_APP_2 = 298 + /** Key code constant: Featured Application key #3. */ + KEYCODE_FEATURED_APP_3 = 299 + /** Key code constant: Featured Application key #4. */ + KEYCODE_FEATURED_APP_4 = 300 + /** Key code constant: Demo Application key #1. */ + KEYCODE_DEMO_APP_1 = 301 + /** Key code constant: Demo Application key #2. */ + KEYCODE_DEMO_APP_2 = 302 + /** Key code constant: Demo Application key #3. */ + KEYCODE_DEMO_APP_3 = 303 + /** Key code constant: Demo Application key #4. */ + KEYCODE_DEMO_APP_4 = 304 +) + +const ( + /** + * SHIFT key locked in CAPS mode. + * Reserved for use by {@link MetaKeyKeyListener} for a published constant in its API. + * @hide + */ + META_CAP_LOCKED = 0x100 + /** + * ALT key locked. + * Reserved for use by {@link MetaKeyKeyListener} for a published constant in its API. + * @hide + */ + META_ALT_LOCKED = 0x200 + /** + * SYM key locked. + * Reserved for use by {@link MetaKeyKeyListener} for a published constant in its API. + * @hide + */ + META_SYM_LOCKED = 0x400 + /** + * Text is in selection mode. + * Reserved for use by {@link MetaKeyKeyListener} for a private unpublished constant + * in its API that is currently being retained for legacy reasons. + * @hide + */ + META_SELECTING = 0x800 + /** + *

This mask is used to check whether one of the ALT meta keys is pressed.

+ * + * @see #isAltPressed() + * @see #getMetaState() + * @see #KEYCODE_ALT_LEFT + * @see #KEYCODE_ALT_RIGHT + */ + META_ALT_ON = 0x02 + /** + *

This mask is used to check whether the left ALT meta key is pressed.

+ * + * @see #isAltPressed() + * @see #getMetaState() + * @see #KEYCODE_ALT_LEFT + */ + META_ALT_LEFT_ON = 0x10 + /** + *

This mask is used to check whether the right the ALT meta key is pressed.

+ * + * @see #isAltPressed() + * @see #getMetaState() + * @see #KEYCODE_ALT_RIGHT + */ + META_ALT_RIGHT_ON = 0x20 + /** + *

This mask is used to check whether one of the SHIFT meta keys is pressed.

+ * + * @see #isShiftPressed() + * @see #getMetaState() + * @see #KEYCODE_SHIFT_LEFT + * @see #KEYCODE_SHIFT_RIGHT + */ + META_SHIFT_ON = 0x1 + /** + *

This mask is used to check whether the left SHIFT meta key is pressed.

+ * + * @see #isShiftPressed() + * @see #getMetaState() + * @see #KEYCODE_SHIFT_LEFT + */ + META_SHIFT_LEFT_ON = 0x40 + /** + *

This mask is used to check whether the right SHIFT meta key is pressed.

+ * + * @see #isShiftPressed() + * @see #getMetaState() + * @see #KEYCODE_SHIFT_RIGHT + */ + META_SHIFT_RIGHT_ON = 0x80 + /** + *

This mask is used to check whether the SYM meta key is pressed.

+ * + * @see #isSymPressed() + * @see #getMetaState() + */ + META_SYM_ON = 0x4 + /** + *

This mask is used to check whether the FUNCTION meta key is pressed.

+ * + * @see #isFunctionPressed() + * @see #getMetaState() + */ + META_FUNCTION_ON = 0x8 + /** + *

This mask is used to check whether one of the CTRL meta keys is pressed.

+ * + * @see #isCtrlPressed() + * @see #getMetaState() + * @see #KEYCODE_CTRL_LEFT + * @see #KEYCODE_CTRL_RIGHT + */ + META_CTRL_ON = 0x1000 + /** + *

This mask is used to check whether the left CTRL meta key is pressed.

+ * + * @see #isCtrlPressed() + * @see #getMetaState() + * @see #KEYCODE_CTRL_LEFT + */ + META_CTRL_LEFT_ON = 0x2000 + /** + *

This mask is used to check whether the right CTRL meta key is pressed.

+ * + * @see #isCtrlPressed() + * @see #getMetaState() + * @see #KEYCODE_CTRL_RIGHT + */ + META_CTRL_RIGHT_ON = 0x4000 + /** + *

This mask is used to check whether one of the META meta keys is pressed.

+ * + * @see #isMetaPressed() + * @see #getMetaState() + * @see #KEYCODE_META_LEFT + * @see #KEYCODE_META_RIGHT + */ + META_META_ON = 0x10000 + /** + *

This mask is used to check whether the left META meta key is pressed.

+ * + * @see #isMetaPressed() + * @see #getMetaState() + * @see #KEYCODE_META_LEFT + */ + META_META_LEFT_ON = 0x20000 + /** + *

This mask is used to check whether the right META meta key is pressed.

+ * + * @see #isMetaPressed() + * @see #getMetaState() + * @see #KEYCODE_META_RIGHT + */ + META_META_RIGHT_ON = 0x40000 + /** + *

This mask is used to check whether the CAPS LOCK meta key is on.

+ * + * @see #isCapsLockOn() + * @see #getMetaState() + * @see #KEYCODE_CAPS_LOCK + */ + META_CAPS_LOCK_ON = 0x100000 + /** + *

This mask is used to check whether the NUM LOCK meta key is on.

+ * + * @see #isNumLockOn() + * @see #getMetaState() + * @see #KEYCODE_NUM_LOCK + */ + META_NUM_LOCK_ON = 0x200000 + /** + *

This mask is used to check whether the SCROLL LOCK meta key is on.

+ * + * @see #isScrollLockOn() + * @see #getMetaState() + * @see #KEYCODE_SCROLL_LOCK + */ + META_SCROLL_LOCK_ON = 0x400000 + /** + * This mask is a combination of {@link #META_SHIFT_ON}, {@link #META_SHIFT_LEFT_ON} + * and {@link #META_SHIFT_RIGHT_ON}. + */ + META_SHIFT_MASK = META_SHIFT_ON | META_SHIFT_LEFT_ON | META_SHIFT_RIGHT_ON + /** + * This mask is a combination of {@link #META_ALT_ON}, {@link #META_ALT_LEFT_ON} + * and {@link #META_ALT_RIGHT_ON}. + */ + META_ALT_MASK = META_ALT_ON | META_ALT_LEFT_ON | META_ALT_RIGHT_ON + /** + * This mask is a combination of {@link #META_CTRL_ON}, {@link #META_CTRL_LEFT_ON} + * and {@link #META_CTRL_RIGHT_ON}. + */ + META_CTRL_MASK = META_CTRL_ON | META_CTRL_LEFT_ON | META_CTRL_RIGHT_ON + /** + * This mask is a combination of {@link #META_META_ON}, {@link #META_META_LEFT_ON} + * and {@link #META_META_RIGHT_ON}. + */ + META_META_MASK = META_META_ON | META_META_LEFT_ON | META_META_RIGHT_ON +) diff --git a/apis/service.go b/apis/service.go new file mode 100644 index 0000000..ff10c1d --- /dev/null +++ b/apis/service.go @@ -0,0 +1,63 @@ +package apis + +import ( + "encoding/json" + + m "github.com/fantonglang/go-mobile-automation" +) + +type Service struct { + req *m.SharedRequest + path string +} + +const ( + SERVICE_UIAUTOMATOR = "uiautomator" +) + +func NewService(name string, req *m.SharedRequest) *Service { + return &Service{ + req: req, + path: "/services/" + name, + } +} + +type ServiceResponse struct { + Success bool `json:"success"` + Running bool `json:"running"` + Description string `json:"description"` +} + +func (s *Service) Start() (*ServiceResponse, error) { + inputBytes := make([]byte, 0) + bytes, err := s.req.Post(s.path, inputBytes) + if err != nil { + return nil, err + } + r := new(ServiceResponse) + err = json.Unmarshal(bytes, r) + return r, err +} + +func (s *Service) Stop() (*ServiceResponse, error) { + bytes, err := s.req.Delete(s.path) + if err != nil { + return nil, err + } + r := new(ServiceResponse) + err = json.Unmarshal(bytes, r) + return r, err +} + +func (s *Service) Running() (bool, error) { + bytes, err := s.req.Get(s.path) + if err != nil { + return false, err + } + r := new(ServiceResponse) + err = json.Unmarshal(bytes, r) + if err != nil { + return false, err + } + return r.Running, nil +} diff --git a/apis/service_test.go b/apis/service_test.go new file mode 100644 index 0000000..32f05ef --- /dev/null +++ b/apis/service_test.go @@ -0,0 +1,57 @@ +package apis + +import ( + "fmt" + "testing" + + openatxclientgo "github.com/fantonglang/go-mobile-automation" +) + +func TestServiceRunning(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + s := NewService(SERVICE_UIAUTOMATOR, o.Req) + running, err := s.Running() + if err != nil { + t.Error("error running") + return + } + if running { + fmt.Println("uiautomator is running") + } else { + fmt.Println("uiautomator is not running") + } +} + +func TestServiceStop(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + s := NewService(SERVICE_UIAUTOMATOR, o.Req) + info, err := s.Stop() + if err != nil { + t.Error("error stop") + return + } + fmt.Println(*info) +} + +func TestServiceStart(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + s := NewService(SERVICE_UIAUTOMATOR, o.Req) + info, err := s.Start() + if err != nil { + t.Error("error start") + return + } + fmt.Println(*info) +} diff --git a/apis/settings.go b/apis/settings.go new file mode 100644 index 0000000..cc2320c --- /dev/null +++ b/apis/settings.go @@ -0,0 +1,43 @@ +package apis + +import ( + "time" +) + +type Settings struct { + Timeout time.Duration + OperationDelayMethods []string + OperationDelay [2]time.Duration + FastRel2Abs bool +} + +func DefaultSettings() *Settings { + return &Settings{ + Timeout: 20 * time.Second, + OperationDelayMethods: []string{"click", "swipe"}, + OperationDelay: [2]time.Duration{200 * time.Microsecond, 200 * time.Microsecond}, + FastRel2Abs: true, + } +} + +func (s *Settings) ImplicitlyWait(to time.Duration) { + s.Timeout = to +} + +func (s *Settings) operation_delay(operation_name string) func() { + methodsContains := false + for _, m := range s.OperationDelayMethods { + if m == operation_name { + methodsContains = true + break + } + } + if !methodsContains { + return func() {} + } + before, after := s.OperationDelay[0], s.OperationDelay[1] + time.Sleep(before) + return func() { + time.Sleep(after) + } +} diff --git a/apis/setup.go b/apis/setup.go new file mode 100644 index 0000000..72a3481 --- /dev/null +++ b/apis/setup.go @@ -0,0 +1,173 @@ +package apis + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "strconv" + "strings" + "time" +) + +func _split_words(line string) []string { + result := make([]string, 0) + item := new(strings.Builder) + for _, c := range line { + if c != ' ' && c != '\t' { + item.WriteRune(c) + continue + } + if item.Len() == 0 { + continue + } + result = append(result, item.String()) + item.Reset() + } + if item.Len() != 0 { + result = append(result, item.String()) + } + return result +} + +func _kill_process_uiautomator(d *Device) error { + out, err := d.Shell("ps -A|grep uiautomator") + if err != nil { + return nil + } + lines := strings.Split(out, "\n") + if len(lines) == 0 { + return nil + } + pids := make([]int, 0) + for _, line := range lines { + if line == "" { + continue + } + words := _split_words(line) + pid, err := strconv.Atoi(words[1]) + if err != nil { + return err + } + name := words[len(words)-1] + if name == "uiautomator" { + pids = append(pids, pid) + } + } + for _, pid := range pids { + _, err := d.Shell("kill -9 " + strconv.Itoa(pid)) + if err != nil { + return err + } + } + return nil +} + +func _is_alive(d *Device) (bool, error) { + input := make(map[string]interface{}) + input["jsonrpc"] = "2.0" + input["id"] = 1 + input["method"] = "deviceInfo" + inputBytes, err := json.Marshal(input) + if err != nil { + return false, err + } + resp, err := http.Post(d.GetHttpRequest().BaseUrl+"/jsonrpc/0", "application/json", bytes.NewBuffer(inputBytes)) + if err != nil { + return false, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return false, nil + } + bytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return false, err + } + var resMap map[string]interface{} + err = json.Unmarshal(bytes, &resMap) + if err != nil { + return false, err + } + if _, ok := resMap["error"]; ok { + return false, nil + } + return true, nil +} + +func _force_reset_uiautomator_v2(d *Device, s *Service, launch_test_app bool) (bool, error) { + package_name := "com.github.uiautomator" + info, err := s.Stop() + if err != nil { + return false, err + } + if !info.Success { + return false, errors.New(strings.ToLower(info.Description)) + } + err = _kill_process_uiautomator(d) + if err != nil { + return false, err + } + if launch_test_app { + for _, permission := range []string{"android.permission.SYSTEM_ALERT_WINDOW", + "android.permission.ACCESS_FINE_LOCATION", + "android.permission.READ_PHONE_STATE"} { + _, err = d.Shell("pm grant " + package_name + " " + permission) + if err != nil { + fmt.Println(err) + } + } + _, err = d.Shell("am start -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -n " + package_name + "/.ToastActivity") + if err != nil { + return false, err + } + } + info, err = s.Start() + if err != nil { + return false, err + } + if !info.Success { + return false, errors.New(strings.ToLower(info.Description)) + } + + time.Sleep(500 * time.Millisecond) + flow_window_showed := false + now := time.Now() + deadline := now.Add(40 * time.Second) + for time.Now().Before(deadline) { + fmt.Printf("uiautomator-v2 is starting ... left: %ds\n", int(deadline.UnixMilli()-time.Now().UnixMilli())/1000) + running, err := s.Running() + if err != nil { + return false, err + } + if !running { + break + } + time.Sleep(time.Second) + alive, err := _is_alive(d) + if err != nil { + return false, err + } + if !alive { + continue + } + if !flow_window_showed { + flow_window_showed = true + err = d.ShowFloatWindow(true) + if err != nil { + return false, err + } + fmt.Println("show float window") + time.Sleep(time.Second) + continue + } + return true, nil + } + _, err = s.Stop() + if err != nil { + return false, err + } + return false, nil +} diff --git a/apis/setup_test.go b/apis/setup_test.go new file mode 100644 index 0000000..a274d34 --- /dev/null +++ b/apis/setup_test.go @@ -0,0 +1,53 @@ +package apis + +import ( + "fmt" + "testing" + + openatxclientgo "github.com/fantonglang/go-mobile-automation" +) + +func Test_kill_process_uiautomator(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + err = _kill_process_uiautomator(d) + if err != nil { + t.Error("error kill") + return + } +} + +func Test_is_alive(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + alive, err := _is_alive(d) + if err != nil { + t.Error("error is alive") + return + } + fmt.Println(alive) +} + +func Test_force_reset_uiautomator_v2(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + s := NewService(SERVICE_UIAUTOMATOR, d.GetHttpRequest()) + ok, err := _force_reset_uiautomator_v2(d, s, true) + if err != nil { + t.Error("error reset") + return + } + fmt.Println(ok) +} diff --git a/apis/uiobject.go b/apis/uiobject.go new file mode 100644 index 0000000..7cd2248 --- /dev/null +++ b/apis/uiobject.go @@ -0,0 +1,413 @@ +package apis + +import ( + "encoding/json" + "image" + "image/draw" + "reflect" + "strconv" + "strings" + "time" +) + +type UiObjectMixIn struct { + d *Device +} + +type UiObject struct { + d *Device + query map[string]interface{} +} + +type UiObjectQuery struct { + key string + val interface{} +} + +type UiElement struct { + parent *UiObject + info temp_info +} + +var UIOBJECT_FIELDS map[string]int + +func init() { + UIOBJECT_FIELDS = map[string]int{ + "text": 0x01, + "textContains": 0x02, + "textMatches": 0x04, + "textStartsWith": 0x08, + "className": 0x10, + "classNameMatches": 0x20, + "description": 0x40, + "descriptionContains": 0x80, + "descriptionMatches": 0x0100, + "descriptionStartsWith": 0x0200, + "checkable": 0x0400, + "checked": 0x0800, + "clickable": 0x1000, + "longClickable": 0x2000, + "scrollable": 0x4000, + "enabled": 0x8000, + "focusable": 0x010000, + "focused": 0x020000, + "selected": 0x040000, + "packageName": 0x080000, + "packageNameMatches": 0x100000, + "resourceId": 0x200000, + "resourceIdMatches": 0x400000, + "index": 0x800000, + "instance": 0x01000000, + } +} + +func NewUiObjectQuery(key string, val interface{}) *UiObjectQuery { + if _, ok := UIOBJECT_FIELDS[key]; ok { + if str, _ok := val.(string); _ok { + a := strconv.QuoteToASCII(str) + a = strings.ReplaceAll(a, `"`, "") + val = a + } + return &UiObjectQuery{ + key: key, + val: val, + } + } + return nil +} + +func (u *UiObjectMixIn) UiObject(queries ...*UiObjectQuery) *UiObject { + m := make(map[string]interface{}) + mask := 0 + for _, q := range queries { + if q == nil { + continue + } + m[q.key] = q.val + _m := UIOBJECT_FIELDS[q.key] + mask |= _m + } + if len(m) == 0 { + return nil + } + m["mask"] = mask + m["childOrSibling"] = make([]string, 0) + m["childOrSiblingSelector"] = make([]string, 0) + return &UiObject{ + d: u.d, + query: m, + } +} + +type temp_bound_type struct { + Bottom int `json:"bottom"` + Left int `json:"left"` + Right int `json:"right"` + Top int `json:"top"` +} +type temp_info struct { + Bounds *temp_bound_type `json:"bounds"` + Checkable bool `json:"checkable"` + Checked bool `json:"checked"` + ChildCount int `json:"childCount"` + ClassName string `json:"className"` + Clickable bool `json:"clickable"` + ContentDescription string `json:"contentDescription"` + Enabled bool `json:"enabled"` + Focusable bool `json:"focusable"` + Focused bool `json:"focused"` + LongClickable bool `json:"longClickable"` + PackageName string `json:"packageName"` + ResourceName string `json:"resourceName"` + Scrollable bool `json:"scrollable"` + Selected bool `json:"selected"` + Text string `json:"text"` + VisibleBounds *temp_bound_type `json:"visibleBounds"` +} + +func (u *UiObject) Child(queries ...*UiObjectQuery) *UiObject { + if reflect.ValueOf(u.query["childOrSibling"]).Len() != 0 { + return nil + } + var q map[string]interface{} + bytes, _ := json.Marshal(u.query) + json.Unmarshal(bytes, &q) + _u := u.d.UiObjectMixIn.UiObject(queries...) + if _u == nil { + return nil + } + childOrSibling := q["childOrSibling"].([]interface{}) + childOrSibling = append(childOrSibling, "child") + q["childOrSibling"] = childOrSibling + childOrSiblingSelector := q["childOrSiblingSelector"].([]interface{}) + childOrSiblingSelector = append(childOrSiblingSelector, _u.query) + q["childOrSiblingSelector"] = childOrSiblingSelector + return &UiObject{ + d: u.d, + query: q, + } +} + +func (u *UiObject) Sibling(queries ...*UiObjectQuery) *UiObject { + if reflect.ValueOf(u.query["childOrSibling"]).Len() != 0 { + return nil + } + var q map[string]interface{} + bytes, _ := json.Marshal(u.query) + json.Unmarshal(bytes, &q) + _u := u.d.UiObjectMixIn.UiObject(queries...) + if _u == nil { + return nil + } + childOrSibling := q["childOrSibling"].([]interface{}) + childOrSibling = append(childOrSibling, "sibling") + q["childOrSibling"] = childOrSibling + childOrSiblingSelector := q["childOrSiblingSelector"].([]interface{}) + childOrSiblingSelector = append(childOrSiblingSelector, _u.query) + q["childOrSiblingSelector"] = childOrSiblingSelector + return &UiObject{ + d: u.d, + query: q, + } +} + +func (u *UiObject) Count() (int, error) { + c, err := u.d.requestJsonRpc("count", u.query) + if err != nil { + return 0, err + } + return interface2int(c), nil +} + +func (u *UiObject) Index(idx int) *UiObject { + var q map[string]interface{} + bytes, _ := json.Marshal(u.query) + json.Unmarshal(bytes, &q) + childOrSiblingSelector := q["childOrSiblingSelector"].([]interface{}) + if len(childOrSiblingSelector) != 0 { + a := childOrSiblingSelector[0].(map[string]interface{}) + a["instance"] = idx + a["mask"] = interface2int(a["mask"]) | UIOBJECT_FIELDS["instance"] + } else { + q["instance"] = idx + q["mask"] = interface2int(q["mask"]) | UIOBJECT_FIELDS["instance"] + } + return &UiObject{ + d: u.d, + query: q, + } +} + +func interface2int(val interface{}) int { + bytes, _ := json.Marshal(val) + i, _ := strconv.Atoi(string(bytes)) + return i +} + +func (u *UiObject) Get() *UiElement { + raw, err := u.d.requestJsonRpc("objInfo", u.query) + if err != nil { + return nil + } + bytes, err := json.Marshal(raw) + if err != nil { + return nil + } + var _info temp_info + err = json.Unmarshal(bytes, &_info) + if err != nil { + return nil + } + return &UiElement{ + parent: u, + info: _info, + } +} + +func (u *UiObject) Wait(timeout time.Duration) int { + now := time.Now() + deadline := now.Add(timeout) + for time.Now().Before(deadline) { + c, err := u.Count() + if err != nil { + panic(err) + } + if c > 0 { + return c + } + time.Sleep(200 * time.Millisecond) + } + return -1 +} + +func (u *UiObject) WaitGone(timeout time.Duration) bool { + now := time.Now() + deadline := now.Add(timeout) + for time.Now().Before(deadline) { + c, err := u.Count() + if err != nil { + panic(err) + } + if c == 0 { + return true + } + time.Sleep(200 * time.Millisecond) + } + return false +} + +func (u *UiElement) Info() *Info { + _info := u.info + info := &Info{ + Text: _info.Text, + Focusable: _info.Focusable, + Enabled: _info.Enabled, + Focused: _info.Focused, + Scrollable: _info.Scrollable, + Selected: _info.Selected, + ClassName: _info.ClassName, + ContentDescription: _info.ContentDescription, + LongClickable: _info.LongClickable, + PackageName: _info.PackageName, + ResourceName: _info.ResourceName, + ResourceId: _info.ResourceName, + ChildCount: _info.ChildCount, + } + if _info.Bounds != nil { + info.Bounds = &Bounds{ + LX: _info.Bounds.Left, + LY: _info.Bounds.Top, + RX: _info.Bounds.Right, + RY: _info.Bounds.Bottom, + } + } + return info +} + +func (u *UiElement) Bounds() *Bounds { + return u.Info().Bounds +} + +func (u *UiElement) PercentBounds() *PercentBounds { + bounds := u.Bounds() + if bounds == nil { + return nil + } + w, h, err := u.parent.d.WindowSize() + if err != nil { + return nil + } + return &PercentBounds{ + LX: float32(bounds.LX) / float32(w), + LY: float32(bounds.LY) / float32(h), + RX: float32(bounds.RX) / float32(w), + RY: float32(bounds.RY) / float32(h), + } +} + +func (u *UiElement) Rect() *Rect { + bounds := u.Bounds() + if bounds == nil { + return nil + } + return &Rect{ + LX: bounds.LX, + LY: bounds.LY, + Width: bounds.RX - bounds.LX, + Height: bounds.RY - bounds.LY, + } +} + +func (u *UiElement) PercentSize() *PercentSize { + rect := u.Rect() + if rect == nil { + return nil + } + ww, wh, err := u.parent.d.WindowSize() + if err != nil { + return nil + } + return &PercentSize{ + Width: float32(rect.Width) / float32(ww), + Height: float32(rect.Height) / float32(wh), + } +} + +func (u *UiElement) Text() string { + return u.info.Text +} + +func (u *UiElement) Offset(px, py float32) (int, int, bool) { + rect := u.Rect() + if rect == nil { + return 0, 0, false + } + x := int(float32(rect.LX) + float32(rect.Width)*px) + y := int(float32(rect.LY) + float32(rect.Height)*py) + return x, y, true +} + +func (u *UiElement) Center() (int, int, bool) { + return u.Offset(0.5, 0.5) +} + +func (u *UiElement) Click() bool { + x, y, ok := u.Center() + if !ok { + return false + } + err := u.parent.d.Click(float32(x), float32(y)) + return err == nil +} + +func (u *UiElement) SwipeInsideList(direction int, scale float32) bool { + if scale <= 0 || scale >= 1 { + return false + } + bounds := u.Rect() + if bounds == nil { + return false + } + left := int(float32(bounds.LX) + float32(bounds.Width)*(1-scale)/2.0) + right := int(float32(bounds.LX) + float32(bounds.Width)*(1+scale)/2.0) + top := int(float32(bounds.LY) + float32(bounds.Height)*(1-scale)/2.0) + bottom := int(float32(bounds.LY) + float32(bounds.Height)*(1+scale)/2.0) + if direction == SWIPE_DIR_LEFT { + err := u.parent.d.SwipeDefault(float32(right), 0.5, float32(left), 0.5) + return err == nil + } else if direction == SWIPE_DIR_RIGHT { + err := u.parent.d.SwipeDefault(float32(left), 0.5, float32(right), 0.5) + return err == nil + } else if direction == SWIPE_DIR_UP { + err := u.parent.d.SwipeDefault(0.5, float32(bottom), 0.5, float32(top)) + return err == nil + } else if direction == SWIPE_DIR_DOWN { + err := u.parent.d.SwipeDefault(0.5, float32(top), 0.5, float32(bottom)) + return err == nil + } else { + return false + } +} + +func (u *UiElement) Type(text string) bool { + ok := u.Click() + if !ok { + return false + } + err := u.parent.d.SendKeys(text, true) + return err == nil +} + +func (u *UiElement) Screenshot() image.Image { + rect := u.Rect() + if rect == nil { + return nil + } + img, _, err := u.parent.d.Screenshot() + if err != nil { + return nil + } + m := image.NewRGBA(image.Rect(0, 0, rect.Width, rect.Height)) + draw.Draw(m, m.Bounds(), img, image.Point{rect.LX, rect.LY}, draw.Src) + return m +} diff --git a/apis/uiobject_test.go b/apis/uiobject_test.go new file mode 100644 index 0000000..ab4702d --- /dev/null +++ b/apis/uiobject_test.go @@ -0,0 +1,136 @@ +package apis + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + openatxclientgo "github.com/fantonglang/go-mobile-automation" +) + +func TestUiObjectChild(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + uo := d.UiObject(NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)) + c := uo.Child(NewUiObjectQuery("className", "android.widget.LinearLayout")) + res := c.Get().Info() + fmt.Println(res) +} + +func TestUiObjectSibling(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + now := time.Now() + uo := d.UiObject(NewUiObjectQuery("resourceId", `com.taobao.taobao:id/sv_search_view`)) + c := uo.Sibling(NewUiObjectQuery("className", "android.widget.FrameLayout")) + res := c.Get().Info() + fmt.Println(res) + fmt.Println((time.Now().UnixMilli() - now.UnixMilli())) +} + +func TestUiObjectCount(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + now := time.Now() + d := NewDevice(o) + uo := d.UiObject(NewUiObjectQuery("className", `android.widget.LinearLayout`)) + // c := uo.Sibling(NewUiObjectQuery("className", "android.widget.FrameLayout")) + cnt, err := uo.Count() + if err != nil { + t.Error("error info") + return + } + fmt.Println(cnt) + fmt.Println((time.Now().UnixMilli() - now.UnixMilli())) +} + +func TestUiObjectIndex(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + now := time.Now() + d := NewDevice(o) + cnt, err := d.UiObject( + NewUiObjectQuery("className", `android.support.v7.widget.RecyclerView`)).Index(0).Child( + NewUiObjectQuery("className", "android.widget.FrameLayout")).Count() + if err != nil { + t.Error("error info") + return + } + fmt.Println(cnt) + fmt.Println((time.Now().UnixMilli() - now.UnixMilli())) +} + +func TestUiObjectInfo(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + now := time.Now() + d := NewDevice(o) + info := d.UiObject( + NewUiObjectQuery("className", `android.support.v7.widget.RecyclerView`)).Index(2).Child( + NewUiObjectQuery("className", "android.widget.FrameLayout")).Get().Info() + bytes, _ := json.Marshal(info) + fmt.Println(string(bytes)) + fmt.Println((time.Now().UnixMilli() - now.UnixMilli())) +} + +func TestUiObjectWait(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + now := time.Now() + d := NewDevice(o) + c := d.UiObject( + NewUiObjectQuery("className", `android.support.v7.widget.RecyclerView`)).Index(0).Child( + NewUiObjectQuery("className", "android.widget.FrameLayout")).Wait(10 * time.Second) + fmt.Println(c) + fmt.Println((time.Now().UnixMilli() - now.UnixMilli())) +} + +func TestUiObjectWaitGone(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + now := time.Now() + d := NewDevice(o) + c := d.UiObject( + NewUiObjectQuery("className", `android.support.v7.widget.RecyclerView`)).Index(1).Child( + NewUiObjectQuery("className", "android.widget.FrameLayout")).WaitGone(10 * time.Second) + fmt.Println(c) + fmt.Println((time.Now().UnixMilli() - now.UnixMilli())) +} + +func TestUiObjectType(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + now := time.Now() + d := NewDevice(o) + d.UiObject( + NewUiObjectQuery("className", `android.widget.EditText`)).Index(0).Get().Type("饭饭里有红伞伞") + d.SendAction(SENDACTION_GO) + fmt.Println((time.Now().UnixMilli() - now.UnixMilli())) +} diff --git a/apis/xpath.go b/apis/xpath.go new file mode 100644 index 0000000..43fecc7 --- /dev/null +++ b/apis/xpath.go @@ -0,0 +1,545 @@ +package apis + +import ( + "fmt" + "image" + "image/draw" + "regexp" + "strconv" + "strings" + "time" + + "github.com/antchfx/xmlquery" +) + +func strict_xpath(xpath string) string { + if strings.HasPrefix(xpath, "/") { + return xpath + } else if strings.HasPrefix(xpath, "@") { + return fmt.Sprintf(`//*[@resource-id="%s"]`, xpath[1:]) + } else if strings.HasPrefix(xpath, "%") && strings.HasSuffix(xpath, "%") { + _template := `//*[contains(@text, "{0}") or contains(@content-desc, "{0}")]` + return strings.ReplaceAll(_template, "{0}", xpath[1:len(xpath)-1]) + } else if strings.HasPrefix(xpath, "%") { + text := xpath[1:] + _template := `//*[substring-before(@text, "{0}") or @text="{0}" or substring-before(@content-desc, "{0}") or @content-desc="{0}"]` + return strings.ReplaceAll(_template, "{0}", text) + } else if strings.HasSuffix(xpath, "%") { + text := xpath[:len(xpath)-1] + _template := `//*[starts-with(@text, "{0}") or starts-with(@content-desc, "{0}")]` + return strings.ReplaceAll(_template, "{0}", text) + } else { + _template := `//*[@text="{0}" or @content-desc="{0}" or @resource-id="{0}"]` + return strings.ReplaceAll(_template, "{0}", xpath) + } +} + +type XPathMixIn struct { + d *Device +} + +type XPath struct { + d *Device + xpath string + source *xmlquery.Node + pathType int + descendantXpath string +} + +const ( + XPATH_TYPE_ORIG = iota + XPATH_TYPE_CHILD + XPATH_TYPE_SIBLING + XPATH_TYPE_DESCENDANT +) + +type XMLElement struct { + parent *XPath + el *xmlquery.Node +} + +func (xp *XPath) all(useSource bool) ([]*XMLElement, error) { + // 1. get source + var _doc *xmlquery.Node + if xp.source != nil && useSource { + _doc = xp.source + } else { + hierachyTxt, err := xp.d.DumpHierarchyDefault() + if err != nil { + return nil, err + } + _doc, err = FormatHierachy(hierachyTxt) + if err != nil { + return nil, err + } + } + // 2. xpath find + xpath := strict_xpath(xp.xpath) + xp.xpath = xpath + els, err := xmlquery.QueryAll(_doc, xpath) + if err != nil { + return nil, err + } + if len(els) == 0 { + return nil, nil + } + xmlElements := make([]*XMLElement, 0) + for _, el := range els { + xmlElements = append(xmlElements, &XMLElement{ + parent: xp, + el: el, + }) + } + return xmlElements, nil +} + +func (xp *XPathMixIn) XPath(xpath string) *XPath { + return &XPath{ + d: xp.d, + xpath: xpath, + source: nil, + pathType: XPATH_TYPE_ORIG, + descendantXpath: "", + } +} + +func (xp *XPathMixIn) XPath2(xpath string, source *xmlquery.Node) *XPath { + return &XPath{ + d: xp.d, + xpath: xpath, + source: source, + pathType: XPATH_TYPE_ORIG, + descendantXpath: "", + } +} + +func (xp *XPath) All() []*XMLElement { + els, err := xp.all(true) + if err != nil { + panic(err) + } + return els +} + +func (xp *XPath) First() *XMLElement { + els, err := xp.all(true) + if err != nil { + panic(err) + } + if len(els) == 0 { + return nil + } + return els[0] +} + +func (xp *XPath) Wait(timeout time.Duration) *XMLElement { + now := time.Now() + deadline := now.Add(timeout) + for time.Now().Before(deadline) { + els, err := xp.all(false) + if err != nil { + panic(err) + } + if len(els) > 0 { + return els[0] + } + time.Sleep(200 * time.Millisecond) + } + return nil +} + +func (xp *XPath) WaitGone(timeout time.Duration) bool { + now := time.Now() + deadline := now.Add(timeout) + for time.Now().Before(deadline) { + els, err := xp.all(false) + if err != nil { + panic(err) + } + if len(els) == 0 { + return true + } + time.Sleep(200 * time.Millisecond) + } + return false +} + +type Bounds struct { + LX int + LY int + RX int + RY int +} + +type PercentBounds struct { + LX float32 + LY float32 + RX float32 + RY float32 +} + +type Rect struct { + LX int + LY int + Width int + Height int +} + +type PercentSize struct { + Width float32 + Height float32 +} + +type Info struct { + Text string + Focusable bool + Enabled bool + Focused bool + Scrollable bool + Selected bool + ClassName string + Bounds *Bounds + ContentDescription string + LongClickable bool + PackageName string + ResourceName string + ResourceId string + ChildCount int +} + +func findAttribute(attrs []xmlquery.Attr, name string) *xmlquery.Attr { + for _, a := range attrs { + if a.Name.Local == name { + return &a + } + } + return nil +} + +func (el *XMLElement) Bounds() *Bounds { + boundsAttr := findAttribute(el.el.Attr, "bounds") + if boundsAttr == nil { + return nil + } + str := boundsAttr.Value + if str == "" { + return nil + } + re := regexp.MustCompile(`^\[(\d+)\,(\d+)\]\[(\d+)\,(\d+)\]$`) + groups := re.FindStringSubmatch(str) + if len(groups) == 0 { + return nil + } + lx, err := strconv.Atoi(groups[1]) + if err != nil { + return nil + } + ly, err := strconv.Atoi(groups[2]) + if err != nil { + return nil + } + rx, err := strconv.Atoi(groups[3]) + if err != nil { + return nil + } + ry, err := strconv.Atoi(groups[4]) + if err != nil { + return nil + } + return &Bounds{ + LX: lx, + LY: ly, + RX: rx, + RY: ry, + } +} + +func (el *XMLElement) PercentBounds() *PercentBounds { + bounds := el.Bounds() + if bounds == nil { + return nil + } + w, h, err := el.parent.d.WindowSize() + if err != nil { + return nil + } + return &PercentBounds{ + LX: float32(bounds.LX) / float32(w), + LY: float32(bounds.LY) / float32(h), + RX: float32(bounds.RX) / float32(w), + RY: float32(bounds.RY) / float32(h), + } +} + +func (el *XMLElement) Rect() *Rect { + bounds := el.Bounds() + if bounds == nil { + return nil + } + return &Rect{ + LX: bounds.LX, + LY: bounds.LY, + Width: bounds.RX - bounds.LX, + Height: bounds.RY - bounds.LY, + } +} + +func (el *XMLElement) PercentSize() *PercentSize { + rect := el.Rect() + if rect == nil { + return nil + } + ww, wh, err := el.parent.d.WindowSize() + if err != nil { + return nil + } + return &PercentSize{ + Width: float32(rect.Width) / float32(ww), + Height: float32(rect.Height) / float32(wh), + } +} + +func (el *XMLElement) Text() string { + textVal := el.Attr("text") + if textVal != "" { + return textVal + } + contentDescVal := el.Attr("content-desc") + if contentDescVal != "" { + return contentDescVal + } + return "" +} + +func (el *XMLElement) Attr(name string) string { + a := findAttribute(el.el.Attr, name) + if a != nil { + return a.Value + } + return "" +} + +func (el *XMLElement) Info() *Info { + text := el.Attr("text") + focusable := el.Attr("focusable") + enabled := el.Attr("enabled") + focused := el.Attr("focused") + scrollable := el.Attr("scrollable") + selected := el.Attr("selected") + className := el.el.Data + bounds := el.Bounds() + contentDescription := el.Attr("content-desc") + longClickable := el.Attr("long-clickable") + packageName := el.Attr("package") + resourceName := el.Attr("resource-id") + resourceId := resourceName + childCount := 0 + if el.el.FirstChild != nil { + trimFunc := func(r rune) bool { + return r == ' ' || r == '\t' || r == '\n' + } + for n := el.el.FirstChild; n != el.el.LastChild; n = n.NextSibling { + if strings.TrimFunc(n.Data, trimFunc) != "" { + childCount++ + } + } + if el.el.FirstChild != el.el.LastChild { + if strings.TrimFunc(el.el.LastChild.Data, trimFunc) != "" { + childCount++ + } + } + } + return &Info{ + Text: text, + Focusable: focusable == "true", + Enabled: enabled == "true", + Focused: focused == "true", + Scrollable: scrollable == "true", + Selected: selected == "true", + ClassName: className, + Bounds: bounds, + ContentDescription: contentDescription, + LongClickable: longClickable == "true", + PackageName: packageName, + ResourceName: resourceName, + ResourceId: resourceId, + ChildCount: childCount, + } +} + +func (el *XMLElement) Offset(px, py float32) (int, int, bool) { + rect := el.Rect() + if rect == nil { + return 0, 0, false + } + x := int(float32(rect.LX) + float32(rect.Width)*px) + y := int(float32(rect.LY) + float32(rect.Height)*py) + return x, y, true +} + +func (el *XMLElement) Center() (int, int, bool) { + return el.Offset(0.5, 0.5) +} + +func (el *XMLElement) Click() bool { + x, y, ok := el.Center() + if !ok { + return false + } + err := el.parent.d.Click(float32(x), float32(y)) + return err == nil +} + +const ( + SWIPE_DIR_LEFT = iota + 1 + SWIPE_DIR_RIGHT + SWIPE_DIR_UP + SWIPE_DIR_DOWN +) + +func (el *XMLElement) SwipeInsideList(direction int, scale float32) bool { + if scale <= 0 || scale >= 1 { + return false + } + bounds := el.Rect() + if bounds == nil { + return false + } + left := int(float32(bounds.LX) + float32(bounds.Width)*(1-scale)/2.0) + right := int(float32(bounds.LX) + float32(bounds.Width)*(1+scale)/2.0) + top := int(float32(bounds.LY) + float32(bounds.Height)*(1-scale)/2.0) + bottom := int(float32(bounds.LY) + float32(bounds.Height)*(1+scale)/2.0) + if direction == SWIPE_DIR_LEFT { + err := el.parent.d.SwipeDefault(float32(right), 0.5, float32(left), 0.5) + return err == nil + } else if direction == SWIPE_DIR_RIGHT { + err := el.parent.d.SwipeDefault(float32(left), 0.5, float32(right), 0.5) + return err == nil + } else if direction == SWIPE_DIR_UP { + err := el.parent.d.SwipeDefault(0.5, float32(bottom), 0.5, float32(top)) + return err == nil + } else if direction == SWIPE_DIR_DOWN { + err := el.parent.d.SwipeDefault(0.5, float32(top), 0.5, float32(bottom)) + return err == nil + } else { + return false + } +} + +func (el *XMLElement) Type(text string) bool { + ok := el.Click() + if !ok { + return false + } + err := el.parent.d.SendKeys(text, true) + return err == nil +} + +func (el *XMLElement) Children() []*XMLElement { + if el.el.FirstChild == nil { + return nil + } + _children := make([]*xmlquery.Node, 0) + trimFunc := func(r rune) bool { + return r == ' ' || r == '\t' || r == '\n' + } + for n := el.el.FirstChild; n != el.el.LastChild; n = n.NextSibling { + if strings.TrimFunc(n.Data, trimFunc) != "" { + _children = append(_children, n) + } + } + if el.el.FirstChild != el.el.LastChild { + if strings.TrimFunc(el.el.LastChild.Data, trimFunc) != "" { + _children = append(_children, el.el.LastChild) + } + } + if len(_children) == 0 { + return nil + } + result := make([]*XMLElement, 0) + xpath_parent := *el.parent + xpath_parent.pathType = XPATH_TYPE_CHILD + xpath_parent.descendantXpath = "" + for _, c := range _children { + result = append(result, &XMLElement{ + parent: &xpath_parent, + el: c, + }) + } + return result +} + +func (el *XMLElement) Siblings() []*XMLElement { + elem := el.el + if elem.Parent == nil { + return nil + } + siblings := make([]*xmlquery.Node, 0) + head := elem.Parent.FirstChild + tail := elem.Parent.LastChild + n := head + trimFunc := func(r rune) bool { + return r == ' ' || r == '\t' || r == '\n' + } + for { + if n != elem && strings.TrimFunc(n.Data, trimFunc) != "" { + siblings = append(siblings, n) + } + if n != tail { + n = n.NextSibling + } else { + break + } + } + if len(siblings) == 0 { + return nil + } + result := make([]*XMLElement, 0) + xpath_parent := *el.parent + xpath_parent.pathType = XPATH_TYPE_SIBLING + xpath_parent.descendantXpath = "" + for _, c := range siblings { + result = append(result, &XMLElement{ + parent: &xpath_parent, + el: c, + }) + } + return result +} + +func (el *XMLElement) Find(xpath string) []*XMLElement { + elem := el.el + true_xpath := strict_xpath(xpath) + nodes, err := xmlquery.QueryAll(elem, true_xpath) + if err != nil || len(nodes) == 0 || (len(nodes) == 1 && nodes[0] == elem) { + return nil + } + result := make([]*XMLElement, 0) + xpath_parent := *el.parent + xpath_parent.pathType = XPATH_TYPE_DESCENDANT + xpath_parent.descendantXpath = true_xpath + for _, c := range nodes { + if c == elem { + continue + } + result = append(result, &XMLElement{ + parent: &xpath_parent, + el: c, + }) + } + return result +} + +func (el *XMLElement) Screenshot() image.Image { + rect := el.Rect() + if rect == nil { + return nil + } + img, _, err := el.parent.d.Screenshot() + if err != nil { + return nil + } + m := image.NewRGBA(image.Rect(0, 0, rect.Width, rect.Height)) + draw.Draw(m, m.Bounds(), img, image.Point{rect.LX, rect.LY}, draw.Src) + return m +} diff --git a/apis/xpath_test.go b/apis/xpath_test.go new file mode 100644 index 0000000..f7f52da --- /dev/null +++ b/apis/xpath_test.go @@ -0,0 +1,70 @@ +package apis + +import ( + "fmt" + "testing" + "time" + + openatxclientgo "github.com/fantonglang/go-mobile-automation" +) + +func TestXPathAll(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + now := time.Now() + els := d.XPath(`//*[@text="饿了么"]`).All() + for _, el := range els { + info := el.Info() + fmt.Println(*info) + } + if len(els) == 0 { + t.Error("error find els") + } + fmt.Println((time.Now().UnixMilli() - now.UnixMilli())) +} + +func TestXPathChildren(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + el := d.XPath(`//*[@resource-id="com.taobao.taobao:id/rv_main_container"]`).First() + children := el.Children() + for _, c := range children { + fmt.Println(*c.Info()) + } +} + +func TestXPathSiblings(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + el := d.XPath(`//*[@resource-id="com.taobao.taobao:id/rv_main_container"]/android.widget.FrameLayout[1]`).First() + children := el.Siblings() + for _, c := range children { + fmt.Println(*c.Info()) + } +} + +func TestXPathFind(t *testing.T) { + o, err := openatxclientgo.NewHostOperation("c574dd45") + if err != nil { + t.Error("error connect to device") + return + } + d := NewDevice(o) + el := d.XPath(`//*[@resource-id="com.taobao.taobao:id/rv_main_container"]`).First() + children := el.Find(`//android.support.v7.widget.RecyclerView`) + for _, c := range children { + fmt.Println(*c.Info()) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..890860f --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/fantonglang/go-mobile-automation + +go 1.17 + +require ( + github.com/antchfx/xmlquery v1.3.9 // indirect + github.com/antchfx/xpath v1.2.0 // indirect + github.com/fantonglang/adbutils-go v0.0.0-20211229042000-4a10b0d4726b // indirect + github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/openatx/androidutils v1.0.0 // indirect + golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc // indirect + golang.org/x/text v0.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9528ccb --- /dev/null +++ b/go.sum @@ -0,0 +1,25 @@ +github.com/antchfx/xmlquery v1.3.9 h1:Y+zyMdiUZ4fasTQTkDb3DflOXP7+obcYEh80SISBmnQ= +github.com/antchfx/xmlquery v1.3.9/go.mod h1:wojC/BxjEkjJt6dPiAqUzoXO5nIMWtxHS8PD8TmN4ks= +github.com/antchfx/xpath v1.2.0 h1:mbwv7co+x0RwgeGAOHdrKy89GvHaGvxxBtPK0uF9Zr8= +github.com/antchfx/xpath v1.2.0/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fantonglang/adbutils-go v0.0.0-20211229042000-4a10b0d4726b h1:9q2DmGuabH/hiGfrAAkuZfweHWlDP9+RTalp9ZFKbW0= +github.com/fantonglang/adbutils-go v0.0.0-20211229042000-4a10b0d4726b/go.mod h1:Ght6gx2cUVRvrRP3g2+jW9Gn4YUUsV7AlkxHU689cwA= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/openatx/androidutils v1.0.0 h1:gYKFX/LqOf4LxyO7dZrNfGtPNaCaSNrniUHL06MPATQ= +github.com/openatx/androidutils v1.0.0/go.mod h1:Pbja6rsE71OHQMhrK/tZm86fqB9Go8sXToi9CylrXEU= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc h1:zK/HqS5bZxDptfPJNq8v7vJfXtkU7r9TLIoSr1bXaP4= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/models/device_info.go b/models/device_info.go new file mode 100644 index 0000000..6aafbca --- /dev/null +++ b/models/device_info.go @@ -0,0 +1,97 @@ +package models + +import ( + "fmt" + "time" + + "github.com/openatx/androidutils" +) + +type CpuInfo struct { + Cores int `json:"cores"` + Hardware string `json:"hardware"` +} + +type MemoryInfo struct { + Total int `json:"total"` // unit kB + Around string `json:"around,omitempty"` +} + +type OwnerInfo struct { + IP string `json:"ip"` +} + +type DeviceInfo struct { + Udid string `json:"udid,omitempty"` // Unique device identifier + PropertyId string `json:"propertyId,omitempty"` // For device managerment, eg: HIH-PHO-1122 + Version string `json:"version,omitempty"` // ro.build.version.release + Serial string `json:"serial,omitempty"` // ro.serialno + Brand string `json:"brand,omitempty"` // ro.product.brand + Model string `json:"model,omitempty"` // ro.product.model + HWAddr string `json:"hwaddr,omitempty"` // persist.sys.wifi.mac + Notes string `json:"notes,omitempty"` // device notes + IP string `json:"ip,omitempty"` + Port int `json:"port,omitempty"` + ReverseProxyAddr string `json:"reverseProxyAddr,omitempty"` + ReverseProxyServerAddr string `json:"reverseProxyServerAddr,omitempty"` + Sdk int `json:"sdk,omitempty"` + AgentVersion string `json:"agentVersion,omitempty"` + Display *androidutils.Display `json:"display,omitempty"` + Battery *androidutils.Battery `json:"battery,omitempty"` + Memory *MemoryInfo `json:"memory,omitempty"` // proc/meminfo + Cpu *CpuInfo `json:"cpu,omitempty"` // proc/cpuinfo + Arch string `json:"arch"` + + Owner *OwnerInfo `json:"owner" gorethink:"owner,omitempty"` + Reserved string `json:"reserved,omitempty"` + + ConnectionCount int `json:"-"` // > 1 happended when phone redial server + CreatedAt time.Time `json:"-" gorethink:"createdAt,omitempty"` + PresenceChangedAt time.Time `json:"presenceChangedAt,omitempty"` + UsingBeganAt time.Time `json:"usingBeganAt,omitempty" gorethink:"usingBeganAt,omitempty"` + + Ready *bool `json:"ready,omitempty"` + Present *bool `json:"present,omitempty"` + Using *bool `json:"using,omitempty"` + + Product *Product `json:"product" gorethink:"product_id,reference,omitempty" gorethink_ref:"id"` + Provider *Provider `json:"provider" gorethink:"provider_id,reference,omitempty" gorethink_ref:"id"` + + // only works when there is provider + ProviderForwardedPort int `json:"providerForwardedPort,omitempty"` + + // used for provider to known agent server url + ServerURL string `json:"serverUrl,omitempty"` +} + +// "Brand Model Memory CPU" together can define a phone +type Product struct { + Id string `json:"id" gorethink:"id,omitempty"` + Name string `json:"name" gorethink:"name,omitempty"` + Brand string `json:"brand" gorethink:"brand,omitempty"` + Model string `json:"model" gorethink:"model,omitempty"` + Memory string `json:"memory,omitempty"` // eg: 4GB + Cpu string `json:"cpu,omitempty"` + + Coverage float32 `json:"coverage" gorethink:"coverage,omitempty"` + Gpu string `json:"gpu,omitempty"` + Link string `json:"link,omitempty"` // Outside link + // AntutuScore int `json:"antutuScore,omitempty"` +} + +// u2init +type Provider struct { + Id string `json:"id" gorethink:"id,omitempty"` // machine id + IP string `json:"ip" gorethink:"ip,omitempty"` + Port int `json:"port" gorethink:"port,omitempty"` + Present *bool `json:"present,omitempty"` + Notes string `json:"notes" gorethink:"notes,omitempty"` + Devices []DeviceInfo `json:"devices" gorethink:"devices,omitempty"` + CreatedAt time.Time `json:"createdAt,omitempty"` + PresenceChangedAt time.Time `json:"presenceChangedAt,omitempty"` +} + +// Addr combined with ip:port +func (p *Provider) Addr() string { + return fmt.Sprintf("%s:%d", p.IP, p.Port) +} diff --git a/operations.go b/operations.go new file mode 100644 index 0000000..3b5f401 --- /dev/null +++ b/operations.go @@ -0,0 +1,103 @@ +package openatxclientgo + +import ( + "errors" + "fmt" + "os/exec" + + adbutilsgo "github.com/fantonglang/adbutils-go" +) + +type HostOperation struct { + DeviceId string + Req *SharedRequest + ReqCv *SharedRequest +} + +type DeviceOperation struct { + Req *SharedRequest + ReqCv *SharedRequest +} + +func NewHostOperation(deviceId string) (*HostOperation, error) { + deviceIds := adbutilsgo.ListDevices() + if deviceIds == nil { + return nil, errors.New("no device attached") + } + deviceMatch := false + for _, d := range deviceIds { + if d == deviceId { + deviceMatch = true + break + } + } + if !deviceMatch { + return nil, fmt.Errorf("no such device: %s", deviceId) + } + hostPort, err := adbutilsgo.PortForward(deviceId, "7912", adbutilsgo.PortForwardOptions{}) //adbutilsgo.OpenAtxPortForward(deviceId) + if err != nil { + return nil, err + } + hostCvPort, err := adbutilsgo.PortForward(deviceId, "5000", adbutilsgo.PortForwardOptions{}) + if err != nil { + return nil, err + } + return &HostOperation{ + DeviceId: deviceId, + Req: NewSharedRequest("http://localhost:" + hostPort), + ReqCv: NewSharedRequest("http://localhost:" + hostCvPort), + }, nil +} + +func NewDeviceOperation() *DeviceOperation { + return &DeviceOperation{ + Req: NewSharedRequest("http://localhost:7912"), + ReqCv: NewSharedRequest("http://localhost:5000"), + } +} + +type IOperation interface { + Shell(cmd string) (string, error) + GetHttpRequest() *SharedRequest + GetCvRequest() *SharedRequest +} + +func (o *HostOperation) Shell(cmd string) (string, error) { + return adbutilsgo.Shell(o.DeviceId, cmd) +} + +func (o *DeviceOperation) Shell(cmd string) (string, error) { + out, err := exec.Command("/system/bin/sh", "-c", cmd).Output() + if err != nil { + return "", errors.New("execute shell command failed") + } + return string(out), nil +} + +func (o *HostOperation) GetHttpRequest() *SharedRequest { + if o.Req.onError == nil { + o.Req.onError = func() error { + _, err := o.Shell("/data/local/tmp/atx-agent server -d --stop") + return err + } + } + return o.Req +} + +func (o *DeviceOperation) GetHttpRequest() *SharedRequest { + if o.Req.onError == nil { + o.Req.onError = func() error { + _, err := o.Shell("/data/local/tmp/atx-agent server -d --stop") + return err + } + } + return o.Req +} + +func (o *HostOperation) GetCvRequest() *SharedRequest { + return o.ReqCv +} + +func (o *DeviceOperation) GetCvRequest() *SharedRequest { + return o.ReqCv +} diff --git a/shared_request.go b/shared_request.go new file mode 100644 index 0000000..194fc0f --- /dev/null +++ b/shared_request.go @@ -0,0 +1,151 @@ +package openatxclientgo + +import ( + "bytes" + "errors" + "io/ioutil" + "net/http" + "net/url" + "strconv" + "time" +) + +func init() { + http.DefaultClient.Timeout = 10 * time.Second +} + +type SharedRequest struct { + BaseUrl string + onError func() error +} + +func NewSharedRequest(baseUrl string) *SharedRequest { + return &SharedRequest{ + BaseUrl: baseUrl, + onError: nil, + } +} + +func (r *SharedRequest) Post(path string, data []byte) ([]byte, error) { + resp, err := http.Post(r.BaseUrl+path, "application/json", bytes.NewBuffer(data)) + if err != nil { + if r.onError != nil { + if _err := r.onError(); _err == nil { + time.Sleep(10 * time.Second) + resp, err = http.Post(r.BaseUrl+path, "application/json", bytes.NewBuffer(data)) + } + } + } + if err != nil { + return nil, err + } + defer resp.Body.Close() + bytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode/100 != 2 { + return nil, errors.New("[status:" + strconv.Itoa(resp.StatusCode) + "] " + string(bytes)) + } + return bytes, nil +} + +func (r *SharedRequest) Get(path string) ([]byte, error) { + resp, err := http.Get(r.BaseUrl + path) + if err != nil { + if r.onError != nil { + if _err := r.onError(); _err == nil { + time.Sleep(10 * time.Second) + resp, err = http.Get(r.BaseUrl + path) + } + } + } + if err != nil { + return nil, err + } + defer resp.Body.Close() + bytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode/100 != 2 { + return nil, errors.New("[status:" + strconv.Itoa(resp.StatusCode) + "] " + string(bytes)) + } + return bytes, nil +} + +func (r *SharedRequest) Delete(path string) ([]byte, error) { + req, err := http.NewRequest("DELETE", r.BaseUrl+path, nil) + if err != nil { + return nil, err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + if r.onError != nil { + if _err := r.onError(); _err == nil { + time.Sleep(10 * time.Second) + resp, err = http.DefaultClient.Do(req) + } + } + } + if err != nil { + return nil, err + } + defer resp.Body.Close() + bytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode/100 != 2 { + return nil, errors.New("[status:" + strconv.Itoa(resp.StatusCode) + "] " + string(bytes)) + } + return bytes, nil +} + +func (r *SharedRequest) GetWithTimeout(path string, timeout time.Duration) ([]byte, bool, error) { + client := &http.Client{ + Timeout: timeout, + } + resp, err := client.Get(r.BaseUrl + path) + if err != nil { + if _err, ok := err.(*url.Error); ok { + if _err.Timeout() { + return nil, true, err + } + } + return nil, false, err + } + defer resp.Body.Close() + bytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, false, err + } + if resp.StatusCode/100 != 2 { + return nil, false, errors.New("[status:" + strconv.Itoa(resp.StatusCode) + "] " + string(bytes)) + } + return bytes, false, nil +} + +func (r *SharedRequest) PostWithTimeout(path string, data []byte, contentType string, timeout time.Duration) ([]byte, bool, error) { + client := &http.Client{ + Timeout: timeout, + } + resp, err := client.Post(r.BaseUrl+path, contentType, bytes.NewBuffer(data)) + if err != nil { + if _err, ok := err.(*url.Error); ok { + if _err.Timeout() { + return nil, true, err + } + } + return nil, false, err + } + defer resp.Body.Close() + bytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, false, err + } + if resp.StatusCode/100 != 2 { + return nil, false, errors.New("[status:" + strconv.Itoa(resp.StatusCode) + "] " + string(bytes)) + } + return bytes, false, nil +}