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