initial commit - v0.0.1

This commit is contained in:
sunzhao 2021-12-29 12:51:11 +08:00
parent b1256965e3
commit 40a7bf14d2
21 changed files with 4477 additions and 0 deletions

161
apis/cv.go Normal file
View File

@ -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
}

734
apis/device.go Normal file
View File

@ -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<orientation>\d+), .*deviceWidth=(?P<width>\d+), deviceHeight=(?P<height>\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
}

408
apis/device_test.go Normal file
View File

@ -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)
}

153
apis/ime.go Normal file
View File

@ -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
}

94
apis/ime_test.go Normal file
View File

@ -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
}
}

18
apis/instance.go Normal file
View File

@ -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
}

966
apis/keycode.go Normal file
View File

@ -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
/**
* <p>This mask is used to check whether one of the ALT meta keys is pressed.</p>
*
* @see #isAltPressed()
* @see #getMetaState()
* @see #KEYCODE_ALT_LEFT
* @see #KEYCODE_ALT_RIGHT
*/
META_ALT_ON = 0x02
/**
* <p>This mask is used to check whether the left ALT meta key is pressed.</p>
*
* @see #isAltPressed()
* @see #getMetaState()
* @see #KEYCODE_ALT_LEFT
*/
META_ALT_LEFT_ON = 0x10
/**
* <p>This mask is used to check whether the right the ALT meta key is pressed.</p>
*
* @see #isAltPressed()
* @see #getMetaState()
* @see #KEYCODE_ALT_RIGHT
*/
META_ALT_RIGHT_ON = 0x20
/**
* <p>This mask is used to check whether one of the SHIFT meta keys is pressed.</p>
*
* @see #isShiftPressed()
* @see #getMetaState()
* @see #KEYCODE_SHIFT_LEFT
* @see #KEYCODE_SHIFT_RIGHT
*/
META_SHIFT_ON = 0x1
/**
* <p>This mask is used to check whether the left SHIFT meta key is pressed.</p>
*
* @see #isShiftPressed()
* @see #getMetaState()
* @see #KEYCODE_SHIFT_LEFT
*/
META_SHIFT_LEFT_ON = 0x40
/**
* <p>This mask is used to check whether the right SHIFT meta key is pressed.</p>
*
* @see #isShiftPressed()
* @see #getMetaState()
* @see #KEYCODE_SHIFT_RIGHT
*/
META_SHIFT_RIGHT_ON = 0x80
/**
* <p>This mask is used to check whether the SYM meta key is pressed.</p>
*
* @see #isSymPressed()
* @see #getMetaState()
*/
META_SYM_ON = 0x4
/**
* <p>This mask is used to check whether the FUNCTION meta key is pressed.</p>
*
* @see #isFunctionPressed()
* @see #getMetaState()
*/
META_FUNCTION_ON = 0x8
/**
* <p>This mask is used to check whether one of the CTRL meta keys is pressed.</p>
*
* @see #isCtrlPressed()
* @see #getMetaState()
* @see #KEYCODE_CTRL_LEFT
* @see #KEYCODE_CTRL_RIGHT
*/
META_CTRL_ON = 0x1000
/**
* <p>This mask is used to check whether the left CTRL meta key is pressed.</p>
*
* @see #isCtrlPressed()
* @see #getMetaState()
* @see #KEYCODE_CTRL_LEFT
*/
META_CTRL_LEFT_ON = 0x2000
/**
* <p>This mask is used to check whether the right CTRL meta key is pressed.</p>
*
* @see #isCtrlPressed()
* @see #getMetaState()
* @see #KEYCODE_CTRL_RIGHT
*/
META_CTRL_RIGHT_ON = 0x4000
/**
* <p>This mask is used to check whether one of the META meta keys is pressed.</p>
*
* @see #isMetaPressed()
* @see #getMetaState()
* @see #KEYCODE_META_LEFT
* @see #KEYCODE_META_RIGHT
*/
META_META_ON = 0x10000
/**
* <p>This mask is used to check whether the left META meta key is pressed.</p>
*
* @see #isMetaPressed()
* @see #getMetaState()
* @see #KEYCODE_META_LEFT
*/
META_META_LEFT_ON = 0x20000
/**
* <p>This mask is used to check whether the right META meta key is pressed.</p>
*
* @see #isMetaPressed()
* @see #getMetaState()
* @see #KEYCODE_META_RIGHT
*/
META_META_RIGHT_ON = 0x40000
/**
* <p>This mask is used to check whether the CAPS LOCK meta key is on.</p>
*
* @see #isCapsLockOn()
* @see #getMetaState()
* @see #KEYCODE_CAPS_LOCK
*/
META_CAPS_LOCK_ON = 0x100000
/**
* <p>This mask is used to check whether the NUM LOCK meta key is on.</p>
*
* @see #isNumLockOn()
* @see #getMetaState()
* @see #KEYCODE_NUM_LOCK
*/
META_NUM_LOCK_ON = 0x200000
/**
* <p>This mask is used to check whether the SCROLL LOCK meta key is on.</p>
*
* @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
)

63
apis/service.go Normal file
View File

@ -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
}

57
apis/service_test.go Normal file
View File

@ -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)
}

43
apis/settings.go Normal file
View File

@ -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)
}
}

173
apis/setup.go Normal file
View File

@ -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
}

53
apis/setup_test.go Normal file
View File

@ -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)
}

413
apis/uiobject.go Normal file
View File

@ -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
}

136
apis/uiobject_test.go Normal file
View File

@ -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()))
}

545
apis/xpath.go Normal file
View File

@ -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
}

70
apis/xpath_test.go Normal file
View File

@ -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())
}
}

14
go.mod Normal file
View File

@ -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
)

25
go.sum Normal file
View File

@ -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=

97
models/device_info.go Normal file
View File

@ -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)
}

103
operations.go Normal file
View File

@ -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
}

151
shared_request.go Normal file
View File

@ -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
}