go-mobile-automation/apis/xpath.go
2021-12-29 12:51:11 +08:00

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
}