Files
httprunner/hrp/pkg/uixt/android_device.go
2024-12-11 16:15:02 +08:00

1113 lines
33 KiB
Go

package uixt
import (
"bufio"
"bytes"
"context"
"crypto/md5"
"embed"
"encoding/base64"
"encoding/hex"
"fmt"
"os/exec"
"regexp"
"strconv"
"strings"
"time"
"github.com/httprunner/funplugin/myexec"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/code"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/config"
"github.com/httprunner/httprunner/v4/hrp/internal/json"
"github.com/httprunner/httprunner/v4/hrp/pkg/gadb"
)
var (
DouyinServerPort = 32316
// adb server
AdbServerHost = "localhost"
AdbServerPort = gadb.AdbServerPort // 5037
// uiautomator2 server
UIA2ServerHost = "localhost"
UIA2ServerPort = 6790
UIA2ServerPackageName = "io.appium.uiautomator2.server"
UIA2ServerTestPackageName = "io.appium.uiautomator2.server.test"
EvalInstallerPackageName = "sogou.mobile.explorer"
InstallViaInstallerCommand = "am start -S -n sogou.mobile.explorer/.PackageInstallerActivity -d"
)
//go:embed evalite
var evalite embed.FS
const forwardToPrefix = "forward-to-"
type AndroidDeviceOption func(*AndroidDevice)
func WithSerialNumber(serial string) AndroidDeviceOption {
return func(device *AndroidDevice) {
device.SerialNumber = serial
}
}
func WithUIA2(uia2On bool) AndroidDeviceOption {
return func(device *AndroidDevice) {
device.UIA2 = uia2On
}
}
func WithStub(stubOn bool) AndroidDeviceOption {
return func(device *AndroidDevice) {
device.STUB = stubOn
}
}
func WithUIA2IP(ip string) AndroidDeviceOption {
return func(device *AndroidDevice) {
device.UIA2IP = ip
}
}
func WithUIA2Port(port int) AndroidDeviceOption {
return func(device *AndroidDevice) {
device.UIA2Port = port
}
}
func WithAdbLogOn(logOn bool) AndroidDeviceOption {
return func(device *AndroidDevice) {
device.LogOn = logOn
}
}
func NewAndroidDevice(options ...AndroidDeviceOption) (device *AndroidDevice, err error) {
device = &AndroidDevice{
UIA2IP: UIA2ServerHost,
UIA2Port: UIA2ServerPort,
}
for _, option := range options {
option(device)
}
deviceList, err := GetAndroidDevices(device.SerialNumber)
if err != nil {
return nil, errors.Wrap(code.DeviceConnectionError, err.Error())
}
if device.SerialNumber == "" && len(deviceList) > 1 {
return nil, errors.Wrap(code.DeviceConnectionError, "more than one device connected, please specify the serial")
}
dev := deviceList[0]
if device.SerialNumber == "" {
selectSerial := dev.Serial()
device.SerialNumber = selectSerial
log.Warn().
Str("serial", device.SerialNumber).
Msg("android SerialNumber is not specified, select the first one")
}
device.d = dev
device.logcat = NewAdbLogcat(device.SerialNumber)
evalToolRaw, err := evalite.ReadFile("evalite")
if err != nil {
return nil, errors.Wrap(code.LoadFileError, err.Error())
}
err = dev.Push(bytes.NewReader(evalToolRaw), "/data/local/tmp/evalite", time.Now())
if err != nil {
return nil, errors.Wrap(code.DeviceShellExecError, err.Error())
}
log.Info().Str("serial", device.SerialNumber).Msg("init android device")
return device, nil
}
func GetAndroidDevices(serial ...string) (devices []*gadb.Device, err error) {
var adbClient gadb.Client
if adbClient, err = gadb.NewClientWith(AdbServerHost, AdbServerPort); err != nil {
return nil, err
}
if devices, err = adbClient.DeviceList(); err != nil {
return nil, err
}
var deviceList []*gadb.Device
// filter by serial
for _, d := range devices {
for _, s := range serial {
if s != "" && s != d.Serial() {
continue
}
deviceList = append(deviceList, d)
}
}
if len(deviceList) == 0 {
var err error
if serial == nil || (len(serial) == 1 && serial[0] == "") {
err = fmt.Errorf("no android device found")
} else {
err = fmt.Errorf("no android device found for serial %v", serial)
}
return nil, err
}
return deviceList, nil
}
type AndroidDevice struct {
d *gadb.Device
logcat *AdbLogcat
SerialNumber string `json:"serial,omitempty" yaml:"serial,omitempty"`
STUB bool `json:"stub,omitempty" yaml:"stub,omitempty"` // use stub
UIA2 bool `json:"uia2,omitempty" yaml:"uia2,omitempty"` // use uiautomator2
UIA2IP string `json:"uia2_ip,omitempty" yaml:"uia2_ip,omitempty"` // uiautomator2 server ip
UIA2Port int `json:"uia2_port,omitempty" yaml:"uia2_port,omitempty"` // uiautomator2 server port
LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"`
}
func (dev *AndroidDevice) Options() (deviceOptions []AndroidDeviceOption) {
if dev.SerialNumber != "" {
deviceOptions = append(deviceOptions, WithSerialNumber(dev.SerialNumber))
}
if dev.UIA2 {
deviceOptions = append(deviceOptions, WithUIA2(true))
}
if dev.UIA2IP != "" {
deviceOptions = append(deviceOptions, WithUIA2IP(dev.UIA2IP))
}
if dev.UIA2Port != 0 {
deviceOptions = append(deviceOptions, WithUIA2Port(dev.UIA2Port))
}
if dev.LogOn {
deviceOptions = append(deviceOptions, WithAdbLogOn(true))
}
return
}
func (dev *AndroidDevice) Init() error {
dev.d.RunShellCommand("ime", "enable", UnicodeImePackageName)
dev.d.RunShellCommand("rm", "-r", config.DeviceActionLogFilePath)
// Notice: brighten should be executed before unlock
// brighten android device screen
dev.d.RunShellCommand("input", "keyevent", "224")
// unlock android device screen
dev.d.RunShellCommand("input", "keyevent", "82")
if dev.UIA2 {
// uiautomator2 server must be started before
// check uiautomator server package installed
if !dev.d.IsPackageInstalled(UIA2ServerPackageName) {
return errors.Wrapf(code.MobileUIDriverAppNotInstalled,
"%s not installed", UIA2ServerPackageName)
}
if !dev.d.IsPackageInstalled(UIA2ServerTestPackageName) {
return errors.Wrapf(code.MobileUIDriverAppNotInstalled,
"%s not installed", UIA2ServerTestPackageName)
}
// TODO: check uiautomator server package running
// if dev.d.IsPackageRunning(UIA2ServerPackageName) {
// return nil
// }
// start uiautomator2 server
go func() {
if err := dev.startUIA2Server(); err != nil {
log.Error().Err(err).Msg("start UIA2 failed")
}
}()
time.Sleep(5 * time.Second) // wait for uiautomator2 server start
}
return nil
}
func (dev *AndroidDevice) UUID() string {
return dev.SerialNumber
}
func (dev *AndroidDevice) LogEnabled() bool {
return dev.LogOn
}
func (dev *AndroidDevice) NewDriver(options ...DriverOption) (driverExt *DriverExt, err error) {
driverOptions := NewDriverOptions()
for _, option := range options {
option(driverOptions)
}
var driver IWebDriver
if dev.UIA2 || dev.LogOn {
driver, err = dev.NewUSBDriver(driverOptions.capabilities)
} else if dev.STUB {
driver, err = dev.NewStubDriver(driverOptions.capabilities)
} else {
driver, err = dev.NewAdbDriver()
}
if err != nil {
return nil, errors.Wrap(err, "failed to init UIA driver")
}
driverExt, err = newDriverExt(dev, driver, options...)
if err != nil {
return nil, err
}
if dev.LogOn {
err = driverExt.Driver.StartCaptureLog("hrp_adb_log")
if err != nil {
return nil, err
}
}
return driverExt, nil
}
// NewUSBDriver creates new client via USB connected device, this will also start a new session.
func (dev *AndroidDevice) NewUSBDriver(capabilities Capabilities) (driver IWebDriver, err error) {
localPort, err := dev.d.Forward(dev.UIA2Port)
if err != nil {
return nil, errors.Wrap(code.DeviceConnectionError,
fmt.Sprintf("forward port %d->%d failed: %v",
localPort, dev.UIA2Port, err))
}
rawURL := fmt.Sprintf("http://%s%d:%d/wd/hub",
forwardToPrefix, localPort, dev.UIA2Port)
uiaDriver, err := NewUIADriver(capabilities, rawURL)
if err != nil {
_ = dev.d.ForwardKill(localPort)
return nil, errors.Wrap(code.DeviceConnectionError, err.Error())
}
uiaDriver.adbClient = dev.d
uiaDriver.logcat = dev.logcat
return uiaDriver, nil
}
func (dev *AndroidDevice) NewStubDriver(capabilities Capabilities) (driver *stubAndroidDriver, err error) {
socketLocalPort, err := dev.d.Forward(StubSocketName)
if err != nil {
return nil, errors.Wrap(code.DeviceConnectionError,
fmt.Sprintf("forward port %d->%s failed: %v",
socketLocalPort, StubSocketName, err))
}
serverLocalPort, err := dev.d.Forward(DouyinServerPort)
if err != nil {
return nil, errors.Wrap(code.DeviceConnectionError,
fmt.Sprintf("forward port %d->%d failed: %v",
serverLocalPort, DouyinServerPort, err))
}
rawURL := fmt.Sprintf("http://%s%d:%d",
forwardToPrefix, serverLocalPort, DouyinServerPort)
stubDriver, err := newStubAndroidDriver(fmt.Sprintf("127.0.0.1:%d", socketLocalPort), rawURL)
if err != nil {
_ = dev.d.ForwardKill(socketLocalPort)
_ = dev.d.ForwardKill(serverLocalPort)
return nil, errors.Wrap(code.DeviceConnectionError, err.Error())
}
stubDriver.adbClient = dev.d
stubDriver.logcat = dev.logcat
return stubDriver, nil
}
// NewHTTPDriver creates new remote HTTP client, this will also start a new session.
func (dev *AndroidDevice) NewHTTPDriver(capabilities Capabilities) (driver IWebDriver, err error) {
rawURL := fmt.Sprintf("http://%s:%d/wd/hub", dev.UIA2IP, dev.UIA2Port)
uiaDriver, err := NewUIADriver(capabilities, rawURL)
if err != nil {
return nil, err
}
uiaDriver.adbClient = dev.d
uiaDriver.logcat = dev.logcat
return uiaDriver, nil
}
func (dev *AndroidDevice) NewAdbDriver() (driver IWebDriver, err error) {
adbDriver := NewAdbDriver()
adbDriver.adbClient = dev.d
adbDriver.logcat = dev.logcat
return adbDriver, nil
}
func (dev *AndroidDevice) StartPerf() error {
// TODO
return nil
}
func (dev *AndroidDevice) StopPerf() string {
// TODO
return ""
}
func (dev *AndroidDevice) StartPcap() error {
// TODO
return nil
}
func (dev *AndroidDevice) StopPcap() string {
// TODO
return ""
}
func (dev *AndroidDevice) Uninstall(packageName string) error {
_, err := dev.d.RunShellCommand("uninstall", packageName)
return err
}
func (dev *AndroidDevice) Install(apkPath string, options ...InstallOption) error {
opts := NewInstallOptions(options...)
brand, err := dev.d.Brand()
if err != nil {
return err
}
args := []string{}
if opts.Reinstall {
args = append(args, "-r")
}
if opts.GrantPermission {
args = append(args, "-g")
}
if opts.Downgrade {
args = append(args, "-d")
}
switch strings.ToLower(brand) {
case "vivo":
return dev.installVivoSilent(apkPath, args...)
case "oppo", "realme", "oneplus":
if dev.d.IsPackageInstalled(EvalInstallerPackageName) {
return dev.installViaInstaller(apkPath, args...)
}
log.Warn().Msg("oppo not install eval installer")
return dev.installCommon(apkPath, args...)
default:
return dev.installCommon(apkPath, args...)
}
}
func (dev *AndroidDevice) installVivoSilent(apkPath string, args ...string) error {
currentTime := builtin.GetCurrentDay()
md5HashInBytes := md5.Sum([]byte(currentTime))
verifyCode := hex.EncodeToString(md5HashInBytes[:])
verifyCode = base64.StdEncoding.EncodeToString([]byte(verifyCode))
verifyCode = verifyCode[:8]
verifyCode = "-V" + verifyCode
args = append([]string{verifyCode}, args...)
_, err := dev.d.InstallAPK(apkPath, args...)
return err
}
func (dev *AndroidDevice) installViaInstaller(apkPath string, args ...string) error {
appRemotePath := "/data/local/tmp/" + strconv.FormatInt(time.Now().UnixMilli(), 10) + ".apk"
err := dev.d.PushFile(apkPath, appRemotePath, time.Now())
if err != nil {
return err
}
done := make(chan error)
defer func() {
close(done)
}()
logcat := NewAdbLogcatWithCallback(dev.d.Serial(), func(line string) {
re := regexp.MustCompile(`\{.*?}`)
match := re.FindString(line)
if match == "" {
return
}
var result InstallResult
err := json.Unmarshal([]byte(match), &result)
if err != nil {
log.Warn().Msg("parse Install msg line error: " + match)
return
}
if result.Result == 0 {
// 安装成功
done <- nil
} else {
done <- errors.New(match)
}
})
err = logcat.CatchLogcat("PackageInstallerCallback")
if err != nil {
return err
}
defer func() {
_ = logcat.Stop()
}()
// 需要监听是否完成安装
command := strings.Split(InstallViaInstallerCommand, " ")
args = append(command, appRemotePath)
_, err = dev.d.RunShellCommand("am", args[1:]...)
if err != nil {
return err
}
// 等待安装完成或超时
timeout := 3 * time.Minute
select {
case err := <-done:
return err
case <-time.After(timeout):
return fmt.Errorf("installation timed out after %v", timeout)
}
}
func (dev *AndroidDevice) installCommon(apkPath string, args ...string) error {
_, err := dev.d.InstallAPK(apkPath, args...)
return err
}
func (dev *AndroidDevice) GetCurrentWindow() (windowInfo WindowInfo, err error) {
// adb shell dumpsys window | grep -E 'mCurrentFocus|mFocusedApp'
output, err := dev.d.RunShellCommand("dumpsys", "window", "|", "grep", "-E", "'mCurrentFocus|mFocusedApp'")
if err != nil {
return WindowInfo{}, errors.Wrap(err, "get current window failed")
}
// mCurrentFocus=Window{a33bc55 u0 com.miui.home/com.miui.home.launcher.Launcher}
reFocus := regexp.MustCompile(`mCurrentFocus=Window{.*? (\S+)/(\S+)}`)
matches := reFocus.FindStringSubmatch(output)
if len(matches) == 3 {
windowInfo = WindowInfo{
PackageName: matches[1],
Activity: matches[2],
}
return windowInfo, nil
}
// mFocusedApp=ActivityRecord{2db504f u0 com.miui.home/.launcher.Launcher t2}
reApp := regexp.MustCompile(`mFocusedApp=ActivityRecord{.*? (\S+)/(\S+?)\s`)
matches = reApp.FindStringSubmatch(output)
if len(matches) == 3 {
windowInfo = WindowInfo{
PackageName: matches[1],
Activity: matches[2],
}
return windowInfo, nil
}
// adb shell dumpsys activity activities | grep mResumedActivity
output, err = dev.d.RunShellCommand("dumpsys", "activity", "activities", "|", "grep", "mResumedActivity")
if err != nil {
return WindowInfo{}, errors.Wrap(err, "get current activity failed")
}
// mResumedActivity: ActivityRecord{2db504f u0 com.miui.home/.launcher.Launcher t2}
reActivity := regexp.MustCompile(`mResumedActivity: ActivityRecord{.*? (\S+)/(\S+?)\s`)
matches = reActivity.FindStringSubmatch(output)
if len(matches) == 3 {
windowInfo = WindowInfo{
PackageName: matches[1],
Activity: matches[2],
}
return windowInfo, nil
}
return WindowInfo{}, errors.New("failed to extract current window")
}
func (dev *AndroidDevice) GetPackageInfo(packageName string) (AppInfo, error) {
appInfo := AppInfo{
Name: packageName,
}
// get package version
appVersion, err := dev.getPackageVersion(packageName)
if err == nil {
appInfo.AppBaseInfo.VersionName = appVersion
} else {
log.Warn().Msg("failed to get package version")
return appInfo, err
}
// get package path
packagePath, err := dev.getPackagePath(packageName)
if err == nil {
appInfo.AppBaseInfo.AppPath = packagePath
} else {
log.Warn().Msg("failed to get package path")
return appInfo, err
}
// get package md5
packageMD5, err := dev.getPackageMD5(packagePath)
if err == nil {
appInfo.AppBaseInfo.AppMD5 = packageMD5
} else {
log.Warn().Msg("failed to get package md5")
return appInfo, err
}
return appInfo, nil
}
func (dev *AndroidDevice) getPackageVersion(packageName string) (string, error) {
output, err := dev.d.RunShellCommand("dumpsys", "package", packageName, "|", "grep", "versionName")
if err != nil {
return "", errors.Wrap(err, "get package version failed")
}
appVersion := ""
re := regexp.MustCompile(`versionName=(.+)`)
matches := re.FindStringSubmatch(output)
if len(matches) > 1 {
appVersion = matches[1]
return appVersion, nil
}
return "", errors.New("failed to get package version")
}
func (dev *AndroidDevice) getPackagePath(packageName string) (string, error) {
output, err := dev.d.RunShellCommand("pm", "path", packageName)
if err != nil {
return "", errors.Wrap(err, "get package path failed")
}
re := regexp.MustCompile(`package:(.+)`)
matches := re.FindStringSubmatch(output)
if len(matches) > 1 {
return matches[1], nil
}
return "", errors.New("failed to get package path")
}
func (dev *AndroidDevice) getPackageMD5(packagePath string) (string, error) {
output, err := dev.d.RunShellCommand("md5sum", packagePath)
if err != nil {
return "", errors.Wrap(err, "get package md5 failed")
}
matches := strings.Split(output, " ")
if len(matches) > 1 {
return matches[0], nil
}
return "", errors.New("failed to get package md5")
}
func (dev *AndroidDevice) startUIA2Server() error {
const maxRetries = 3
for attempt := 1; attempt <= maxRetries; attempt++ {
log.Info().Str("package", UIA2ServerTestPackageName).
Int("attempt", attempt).Msg("start uiautomator server")
// $ adb shell am instrument -w $UIA2ServerTestPackageName
// -w: wait for instrumentation to finish before returning.
// Required for test runners.
out, err := dev.d.RunShellCommand("am", "instrument", "-w", UIA2ServerTestPackageName)
if err != nil {
return errors.Wrap(err, "start uiautomator server failed")
}
if strings.Contains(out, "Process crashed") {
log.Error().Msg("uiautomator server crashed, retrying...")
}
}
return errors.Wrapf(code.MobileUIDriverAppCrashed,
"uiautomator server crashed %d times", maxRetries)
}
func (dev *AndroidDevice) stopUIA2Server() error {
_, err := dev.d.RunShellCommand("am", "force-stop", UIA2ServerPackageName)
return err
}
type LineCallback func(string)
type AdbLogcat struct {
serial string
// logBuffer *bytes.Buffer
errs []error
stopping chan struct{}
done chan struct{}
cmd *exec.Cmd
callback LineCallback
logs []string
}
func NewAdbLogcatWithCallback(serial string, callback LineCallback) *AdbLogcat {
return &AdbLogcat{
serial: serial,
// logBuffer: new(bytes.Buffer),
stopping: make(chan struct{}),
done: make(chan struct{}),
callback: callback,
logs: make([]string, 0),
}
}
func NewAdbLogcat(serial string) *AdbLogcat {
return &AdbLogcat{
serial: serial,
// logBuffer: new(bytes.Buffer),
stopping: make(chan struct{}),
done: make(chan struct{}),
logs: make([]string, 0),
}
}
// CatchLogcatContext starts logcat with timeout context
func (l *AdbLogcat) CatchLogcatContext(timeoutCtx context.Context) (err error) {
if err = l.CatchLogcat(""); err != nil {
return
}
go func() {
select {
case <-timeoutCtx.Done():
_ = l.Stop()
case <-l.stopping:
}
}()
return
}
func (l *AdbLogcat) Stop() error {
select {
case <-l.stopping:
default:
close(l.stopping)
<-l.done
close(l.done)
}
return l.Errors()
}
func (l *AdbLogcat) Errors() (err error) {
for _, e := range l.errs {
if err != nil {
err = fmt.Errorf("%v |[DeviceLogcatErr] %v", err, e)
} else {
err = fmt.Errorf("[DeviceLogcatErr] %v", e)
}
}
return
}
func (l *AdbLogcat) CatchLogcat(filter string) (err error) {
if l.cmd != nil {
log.Warn().Msg("logcat already start")
return nil
}
// FIXME: replace with gadb shell command
// clear logcat
if err = myexec.RunCommand("adb", "-s", l.serial, "shell", "logcat", "-c"); err != nil {
return
}
args := []string{"-s", l.serial, "logcat", "--format", "time"}
if filter != "" {
args = append(args, "-s", filter)
}
// start logcat
l.cmd = myexec.Command("adb", args...)
// l.cmd.Stderr = l.logBuffer
// l.cmd.Stdout = l.logBuffer
reader, err := l.cmd.StdoutPipe()
if err != nil {
return err
}
if err = l.cmd.Start(); err != nil {
return
}
go func() {
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
line := scanner.Text()
if l.callback != nil {
l.callback(line) // Process each line with callback
} else {
l.logs = append(l.logs, line) // Store line if no callback
}
}
}()
go func() {
<-l.stopping
if e := reader.Close(); e != nil {
log.Error().Err(e).Msg("close logcat reader failed")
}
if e := myexec.KillProcessesByGpid(l.cmd); e != nil {
log.Error().Err(e).Msg("kill logcat process failed")
}
l.done <- struct{}{}
}()
return
}
type ExportPoint struct {
Start int `json:"start" yaml:"start"`
End int `json:"end" yaml:"end"`
From interface{} `json:"from" yaml:"from"`
To interface{} `json:"to" yaml:"to"`
Operation string `json:"operation" yaml:"operation"`
Ext string `json:"ext" yaml:"ext"`
RunTime int `json:"run_time,omitempty" yaml:"run_time,omitempty"`
}
func ConvertPoints(lines []string) (eps []ExportPoint) {
log.Info().Msg("ConvertPoints")
log.Info().Msg(strings.Join(lines, "\n"))
for _, line := range lines {
if strings.Contains(line, "ext") {
idx := strings.Index(line, "{")
if idx == -1 {
continue
}
line = line[idx:]
p := ExportPoint{}
err := json.Unmarshal([]byte(line), &p)
if err != nil {
log.Error().Msg("failed to parse point data")
continue
}
log.Info().Msg(line)
eps = append(eps, p)
}
}
return
}
type UiSelectorHelper struct {
value *bytes.Buffer
}
func NewUiSelectorHelper() UiSelectorHelper {
return UiSelectorHelper{value: bytes.NewBufferString("new UiSelector()")}
}
func (s UiSelectorHelper) String() string {
return s.value.String() + ";"
}
// Text Set the search criteria to match the visible text displayed
// in a widget (for example, the text label to launch an app).
//
// The text for the element must match exactly with the string in your input
// argument. Matching is case-sensitive.
func (s UiSelectorHelper) Text(text string) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.text("%s")`, text))
return s
}
// TextMatches Set the search criteria to match the visible text displayed in a layout
// element, using a regular expression.
//
// The text in the widget must match exactly with the string in your
// input argument.
func (s UiSelectorHelper) TextMatches(regex string) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.textMatches("%s")`, regex))
return s
}
// TextStartsWith Set the search criteria to match visible text in a widget that is
// prefixed by the text parameter.
//
// The matching is case-insensitive.
func (s UiSelectorHelper) TextStartsWith(text string) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.textStartsWith("%s")`, text))
return s
}
// TextContains Set the search criteria to match the visible text in a widget
// where the visible text must contain the string in your input argument.
//
// The matching is case-sensitive.
func (s UiSelectorHelper) TextContains(text string) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.textContains("%s")`, text))
return s
}
// ClassName Set the search criteria to match the class property
// for a widget (for example, "android.widget.Button").
func (s UiSelectorHelper) ClassName(className string) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.className("%s")`, className))
return s
}
// ClassNameMatches Set the search criteria to match the class property
// for a widget, using a regular expression.
func (s UiSelectorHelper) ClassNameMatches(regex string) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.classNameMatches("%s")`, regex))
return s
}
// Description Set the search criteria to match the content-description
// property for a widget.
//
// The content-description is typically used
// by the Android Accessibility framework to
// provide an audio prompt for the widget when
// the widget is selected. The content-description
// for the widget must match exactly
// with the string in your input argument.
//
// Matching is case-sensitive.
func (s UiSelectorHelper) Description(desc string) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.description("%s")`, desc))
return s
}
// DescriptionMatches Set the search criteria to match the content-description
// property for a widget.
//
// The content-description is typically used
// by the Android Accessibility framework to
// provide an audio prompt for the widget when
// the widget is selected. The content-description
// for the widget must match exactly
// with the string in your input argument.
func (s UiSelectorHelper) DescriptionMatches(regex string) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.descriptionMatches("%s")`, regex))
return s
}
// DescriptionStartsWith Set the search criteria to match the content-description
// property for a widget.
//
// The content-description is typically used
// by the Android Accessibility framework to
// provide an audio prompt for the widget when
// the widget is selected. The content-description
// for the widget must start
// with the string in your input argument.
//
// Matching is case-insensitive.
func (s UiSelectorHelper) DescriptionStartsWith(desc string) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.descriptionStartsWith("%s")`, desc))
return s
}
// DescriptionContains Set the search criteria to match the content-description
// property for a widget.
//
// The content-description is typically used
// by the Android Accessibility framework to
// provide an audio prompt for the widget when
// the widget is selected. The content-description
// for the widget must contain
// the string in your input argument.
//
// Matching is case-insensitive.
func (s UiSelectorHelper) DescriptionContains(desc string) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.descriptionContains("%s")`, desc))
return s
}
// ResourceId Set the search criteria to match the given resource ID.
func (s UiSelectorHelper) ResourceId(id string) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.resourceId("%s")`, id))
return s
}
// ResourceIdMatches Set the search criteria to match the resource ID
// of the widget, using a regular expression.
func (s UiSelectorHelper) ResourceIdMatches(regex string) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.resourceIdMatches("%s")`, regex))
return s
}
// Index Set the search criteria to match the widget by its node
// index in the layout hierarchy.
//
// The index value must be 0 or greater.
//
// Using the index can be unreliable and should only
// be used as a last resort for matching. Instead,
// consider using the `Instance(int)` method.
func (s UiSelectorHelper) Index(index int) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.index(%d)`, index))
return s
}
// Instance Set the search criteria to match the
// widget by its instance number.
//
// The instance value must be 0 or greater, where
// the first instance is 0.
//
// For example, to simulate a user click on
// the third image that is enabled in a UI screen, you
// could specify a search criteria where the instance is
// 2, the `className(String)` matches the image
// widget class, and `enabled(boolean)` is true.
// The code would look like this:
//
// `new UiSelector().className("android.widget.ImageView")
// .enabled(true).instance(2);`
func (s UiSelectorHelper) Instance(instance int) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.instance(%d)`, instance))
return s
}
// Enabled Set the search criteria to match widgets that are enabled.
//
// Typically, using this search criteria alone is not useful.
// You should also include additional criteria, such as text,
// content-description, or the class name for a widget.
//
// If no other search criteria is specified, and there is more
// than one matching widget, the first widget in the tree
// is selected.
func (s UiSelectorHelper) Enabled(b bool) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.enabled(%t)`, b))
return s
}
// Focused Set the search criteria to match widgets that have focus.
//
// Typically, using this search criteria alone is not useful.
// You should also include additional criteria, such as text,
// content-description, or the class name for a widget.
//
// If no other search criteria is specified, and there is more
// than one matching widget, the first widget in the tree
// is selected.
func (s UiSelectorHelper) Focused(b bool) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.focused(%t)`, b))
return s
}
// Focusable Set the search criteria to match widgets that are focusable.
//
// Typically, using this search criteria alone is not useful.
// You should also include additional criteria, such as text,
// content-description, or the class name for a widget.
//
// If no other search criteria is specified, and there is more
// than one matching widget, the first widget in the tree
// is selected.
func (s UiSelectorHelper) Focusable(b bool) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.focusable(%t)`, b))
return s
}
// Scrollable Set the search criteria to match widgets that are scrollable.
//
// Typically, using this search criteria alone is not useful.
// You should also include additional criteria, such as text,
// content-description, or the class name for a widget.
//
// If no other search criteria is specified, and there is more
// than one matching widget, the first widget in the tree
// is selected.
func (s UiSelectorHelper) Scrollable(b bool) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.scrollable(%t)`, b))
return s
}
// Selected Set the search criteria to match widgets that
// are currently selected.
//
// Typically, using this search criteria alone is not useful.
// You should also include additional criteria, such as text,
// content-description, or the class name for a widget.
//
// If no other search criteria is specified, and there is more
// than one matching widget, the first widget in the tree
// is selected.
func (s UiSelectorHelper) Selected(b bool) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.selected(%t)`, b))
return s
}
// Checked Set the search criteria to match widgets that
// are currently checked (usually for checkboxes).
//
// Typically, using this search criteria alone is not useful.
// You should also include additional criteria, such as text,
// content-description, or the class name for a widget.
//
// If no other search criteria is specified, and there is more
// than one matching widget, the first widget in the tree
// is selected.
func (s UiSelectorHelper) Checked(b bool) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.checked(%t)`, b))
return s
}
// Checkable Set the search criteria to match widgets that are checkable.
//
// Typically, using this search criteria alone is not useful.
// You should also include additional criteria, such as text,
// content-description, or the class name for a widget.
//
// If no other search criteria is specified, and there is more
// than one matching widget, the first widget in the tree
// is selected.
func (s UiSelectorHelper) Checkable(b bool) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.checkable(%t)`, b))
return s
}
// Clickable Set the search criteria to match widgets that are clickable.
//
// Typically, using this search criteria alone is not useful.
// You should also include additional criteria, such as text,
// content-description, or the class name for a widget.
//
// If no other search criteria is specified, and there is more
// than one matching widget, the first widget in the tree
// is selected.
func (s UiSelectorHelper) Clickable(b bool) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.clickable(%t)`, b))
return s
}
// LongClickable Set the search criteria to match widgets that are long-clickable.
//
// Typically, using this search criteria alone is not useful.
// You should also include additional criteria, such as text,
// content-description, or the class name for a widget.
//
// If no other search criteria is specified, and there is more
// than one matching widget, the first widget in the tree
// is selected.
func (s UiSelectorHelper) LongClickable(b bool) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.longClickable(%t)`, b))
return s
}
// packageName Set the search criteria to match the package name
// of the application that contains the widget.
func (s UiSelectorHelper) packageName(name string) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.packageName(%s)`, name))
return s
}
// PackageNameMatches Set the search criteria to match the package name
// of the application that contains the widget.
func (s UiSelectorHelper) PackageNameMatches(regex string) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.packageNameMatches(%s)`, regex))
return s
}
// ChildSelector Adds a child UiSelector criteria to this selector.
//
// Use this selector to narrow the search scope to
// child widgets under a specific parent widget.
func (s UiSelectorHelper) ChildSelector(selector UiSelectorHelper) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.childSelector(%s)`, selector.value.String()))
return s
}
func (s UiSelectorHelper) PatternSelector(selector UiSelectorHelper) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.patternSelector(%s)`, selector.value.String()))
return s
}
func (s UiSelectorHelper) ContainerSelector(selector UiSelectorHelper) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.containerSelector(%s)`, selector.value.String()))
return s
}
// FromParent Adds a child UiSelector criteria to this selector which is used to
// start search from the parent widget.
//
// Use this selector to narrow the search scope to
// sibling widgets as well all child widgets under a parent.
func (s UiSelectorHelper) FromParent(selector UiSelectorHelper) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.fromParent(%s)`, selector.value.String()))
return s
}