546 lines
12 KiB
Go
546 lines
12 KiB
Go
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
|
|
}
|