initial commit - v0.0.1
This commit is contained in:
parent
b1256965e3
commit
40a7bf14d2
161
apis/cv.go
Normal file
161
apis/cv.go
Normal 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
734
apis/device.go
Normal 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
408
apis/device_test.go
Normal 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
153
apis/ime.go
Normal 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
94
apis/ime_test.go
Normal 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
18
apis/instance.go
Normal 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
966
apis/keycode.go
Normal 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
63
apis/service.go
Normal 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
57
apis/service_test.go
Normal 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
43
apis/settings.go
Normal 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
173
apis/setup.go
Normal 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
53
apis/setup_test.go
Normal 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
413
apis/uiobject.go
Normal 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
136
apis/uiobject_test.go
Normal 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
545
apis/xpath.go
Normal 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
70
apis/xpath_test.go
Normal 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
14
go.mod
Normal 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
25
go.sum
Normal 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
97
models/device_info.go
Normal 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
103
operations.go
Normal 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
151
shared_request.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user