FindNode

FindNode 是由 Sigma Resources & Technologies Inc. 开发的的创新 shell 程序,增强了核心的 Selector 软件包。它旨在智能识别 UI 元素(或无障碍功能节点),从而便于提取信息和交互,无需基于坐标的命令。这标志着 AAI (无障碍功能与自动化集成)项目的重大进步,旨在通过用直观的查询替换坐标来改变用户交互方式。

FindNode 的关键组件:

  1. 查询语言:一种简单而强大的语法,专为精确节点识别而设计。
  2. 核心技术:FindNode 与每个设备无缝集成,由 Selector 核心提供支持。
  3. 对象模式:增强跨多个设备的同步,无论屏幕分辨率如何,都可以进行统一的查询和操作。
  4. UI Explorer:学习查询语言的重要工具,提供对节点属性的洞察并协助查询制定。
  5. AAIS:一种用于 AAI 生态系统内多设备自动化的简单脚本语言。
  6. 集成:补充现有的 REST 和 JavaScript API,通过“设备”和“设备”构造实现基于脚本的自动化。

FindNode 利用位于 Accessibility 和 UI Automator 框架之上的 Selector 来唯一标识 UI 元素。 这种方法不需要基于坐标的交互,使脚本能够适应不同的设备分辨率和尺寸。每个 UI 元素或 UI 元素的容器由节点或节点集标识,每个节点都有其在当前屏幕上唯一的 ID。 选择器提供了不同的方式来搜索节点。一旦获取了节点,就可以完成许多自动化任务,例如获取文本/图像或执行单击按钮或在文本字段中输入文本等操作,所有这些都无需坐标。

例如:

JS API:使用 devices.click("OK") 而不是 devices.click(100, 200)。通过查找文本为 "OK" 的节点,获取坐标并点击它。所有操作在运行时完成。

MDCC:用户在主设备上点击一个按钮,该按钮的唯一查询(例如 "T:OK")将被发送到其他设备,以搜索并点击该节点。旨在提供各种定位节点的方式,而无需使用屏幕坐标,没有页面上/下的操作,但可以使用 "scrollToView" 定位节点,这样,相同的脚本可以在具有不同分辨率和屏幕大小的手机上运行。

FindNode 使用 Appium 的 JSON 结构执行 JSON 命令,JSON 格式为:

{"cmd": "action", "action": "findnode", "params": {…}}

每个 FindNode 命令都在 "params" 对象中执行。

返回值可以有两种类型之一,成功运行:

{"status":0, "value":"{…}"}

或失败:

{"status":13,"value":"{<错误消息>}"}

TC 提供了 “device.sendAai()” 或 “devices.sendAai()” 来与 FindNode 交互:

  • 发送命令。
  • 如果FindNode在特定时间段内没有返回,则生成超时错误。
  • 对于某些需要比默认超时时间更长的命令,FindNode 会延长时间以避免超时错误。
  • 自动处理返回值(错误时为 null,值时为非 null,lastError() 获取错误消息)。

想必您可以通过使用“device.sendAai()”和“devices.sendAai()”来完成大部分任务,FindNode 利用了 Appium 的“处理程序”。

随着版本 9.0(更新 50)及更高版本中引入 ATS Beta,ATS 不再需要设置设备驱动程序和开发人员选项。 它允许每个设备在有或没有 Total Control 的情况下独立运行 FindNode 脚本。 ATS也减少了之前的复杂设置,只需授予 ATS Accessibility 和文件管理器权限即可。

限制

FindNode 使用 Android 辅助功能服务来获取信息:

  • 仅支持竖屏模式;横屏模式将在未来的更新中引入。
  • 不支持横向滚动和弹出窗口,这可能会导致获取的节点数量有所增加或减少。
  • 不支持多线程。每次执行时,MDCC(对象模式)、终端(或脚本)、UI Explorer 以及使用 FindNode 的部分 REST API 必须互斥运行。
  • 无法获取 WebView 对象中的某些 UI 元素或某些应用程序实现的原生 UI 元素。因此,像 Facebook、YouTube 以及各种游戏等应用程序通常无法完全支持,可能会漏掉一些节点。使用 UI Explorer 来确定 FindNode 识别了哪些节点。

查询 + 动作

使用我们的查询语言在屏幕上查找节点并对找到的节点执行操作。在本手册中,我们将介绍查询和动作。

例如,要在关于手机屏幕上获取 Android 版本:

>> device.sendAai({query:"TP:basic&&T:Android version&&OY:1", action:"getText"})
{retval: '14'}
  • TP:basic 模板
  • T:Android version 基本查询
  • OY:1 扩展查询

要点击一个标签为 "OK" 的按钮:

{query:"T:OK", action:"click"}

要获取所有股票符号:

{query:"R:.ticker", action:"getText"}


查询

大多数 FindNode 的使用都涉及查询,这是 FindNode 的核心部分(也是 AAI 的一部分)。它允许用户通过对 Accessibility 节点进行不同类型的有用查询来定位所需的 UI 元素。整个屏幕由许多节点组成,一个节点可以是最小的 UI 元素或更小节点的容器,其中许多节点是不可见的。整个屏幕是从单个根节点开始的树状结构。将执行查询以获取满足条件的节点,目的是将大量节点减少到一个或少数几个目标节点,用户可以获取节点的信息或应用操作。

我们开发了一种小型查询语言,可以在设备和脚本之间进行传递,并且足够强大,可以定位大多数节点。其格式为 JSON:

{query:"<key>:<value>&&<key>:<value>&&<key>&&<key>…"}

我们接受 "||" 和 "&&" 作为相同的分隔符,将来可以更改。由于查询是 AND 关系,查询需要满足所有条件才能成为 "匹配节点",使用 "&&" 更为合理。FindNode 不提供 OR 关系,但底层的 Selector 包提供了该功能。"templates" 使用该结构。对于某些键,不需要值。

查询字符串分为 3 个部分:模板(template/TP)、基本查询(basic query/BQ)和扩展查询(extended query/EQ)。"模板"(TP)将生成初始节点,"基本查询"(BQ)将逐个从节点属性执行查询,"扩展查询"(EQ)将使用 TP 和 BQ 的输出,并在多个节点之间执行查询。

获取主存储空闲大小:

可以使用 "查询" + "动作":

>> device.sendAai({query:"T:Main storage&&OX:1", action:"getText"})
{retval: '402 GB free'}

也可以使用 "可选查询"(OQ):

>> device.sendAai({action:"getText(T:Main storage&&OX:1)"})
{retval: '402 GB free'}

类似地,要返回上一个屏幕(左箭头),可以使用 OQ:

>> device.sendAai({action:"click(T:Storage Analysis&&OX:-1)"})
{retval: true}

要获取音频大小,可以混合使用不同的查询,并仍然获得相同的结果:

>> device.sendAai({query:"TP:all&&T:Main storage&&VG:2&&XT:Audio&&OX:1", action:"getText"})
{retval: '77 MB'}

解析器将按以下顺序将查询分为 TP、BQ 和 EQ,并执行它们:

  • TP: TP:all
  • BQ: T:Main storage
  • EQ: VG:2&&XT:Audio&&OX:1


模板 (TP)

模板查询(因缺乏更好的名称而命名)是查询的必需键,它生成用于特定目的的节点。如果缺少模板,则默认为"TP:more",不允许使用多个模板,以下是不同的模板:

名称 描述
all 返回所有节点。
more "all" 选项,去除布局节点。如果未定义模板,则为默认选项。
basic “more” 选项,其子节点计数为零(叶节点)。
reduced 返回“有趣的”节点(参见 UI 探测 的“优化”模式)。
findText,<text> 返回具有特定文本或描述的节点。findText 需要一个额外的参数来指定要搜索的文本。
anyText[,min][,max] 返回具有文本内容的节点,可选择指定长度范围。
anyDescription[,min][,max] 返回具有描述内容的节点,可选择指定长度范围。
textInput[,<hint text>] 返回可“编辑”的节点,按从左上到右下的顺序排序。与 "IX" 一起使用以获取多个文本输入。可选的文本是可用的提示文本。
scrollable,[pos] 返回可滚动的节点,对于多个可滚动区域的屏幕,可以使用 "position" 来标识要匹配的可滚动区域。
line,<top|bottom>,<line number> 返回滚动节点的顶部或底部的节点。


基本查询 (BQ)

基本查询是纯粹基于单个节点信息的查询,BQ 获取从模板(或默认模板,如果没有指定)生成的节点并应用查询。 BQ 不是必需的,如果未定义 BQ,它将把所有节点从模板传递到 EQ。

以下是基本查询,用于匹配从节点本身获取的信息:

名称 描述
P:<package name> 包名(字符串)
C:<class name> 类名(字符串)
R:<resource ID> 资源ID(字符串)
D:<text> 内容描述(字符串)
T:<text> 文本(字符串)
T0:<text> 没有布尔属性为 true 的文本节点 (T:<text>&&BP:none)
T1:<text> 没有任何可编辑属性的文本节点 (T:<text>&&BP:!editable)
IT:<number> 文本输入类型(整数)
CC:<number> 子节点数量(整数)
ID:<hex string> 节点ID(字符串)
BI:[x, y] 返回包含点 (x,y) 的节点
BI:[x1, y1, x2, y2] 返回被矩形包围的节点,-1 表示忽略 x 或 y 坐标
BP:<prop name> 返回与布尔属性匹配的节点
TD:<text> 匹配文本或描述
HT:<text> 匹配提示文本(如果不支持,则匹配 getText())

由于查询始终在屏幕上的现有应用程序上执行,不应使用 "P"。"(S)"代表字符串,"I"代表整数,"[x, y]"表示位置,"[x1, y1, x2, y2]"表示矩形(或边界)。I/S可以接受整数和字符串。字符串更加强大。

键不区分大小写,我们使用大写字母来区分键和值。

(API 18) 一个屏幕可能同时包含多个应用程序(例如 systemui),默认使用正在运行的应用程序,可以使用“P:<package name>”来更改要查询的应用程序。操作“getAllPackageNames”可以列出同时运行的所有包名。



扩展查询 (EQ)

以下是扩展查询的键,通常适用于由BQ或模板返回的多个节点。扩展查询从左到右依次执行,同一个键可以多次应用。EQ是可选的。以下是EQ的键:

名称 描述
BQ:<basic query> 在BQ中对多个键执行基本查询。
IX:<number> 基于位置从匹配节点列表中返回一个节点。
OX:<number> 水平偏移到相邻节点(正数表示右侧,负数表示左侧)。
OY:<number> 垂直偏移到相邻节点(正数表示向下,负数表示向上)。
ON:<type> 从匹配节点列表中选择一个节点的不同方法。
RN 从匹配节点列表中返回优化的节点。
ST:<sort type> 根据节点在屏幕上的位置返回排序后的节点。
TX 返回与参考节点水平相交的节点。
TY 返回与参考节点垂直相交的节点。
V:<value> 设置setText、setChecked和setProgress的值
VG:[level number] 返回相同视图组中的节点。
"X"<BQ key>:<value> 在BQ中对特定键执行基本查询。

所有这些查询都是可选的,您可以选择任何字段,如果您正在对屏幕进行查询。"(S)"表示字符串,"I"表示整数,"[x, y]"表示数组。I/S可以接受整数和字符串。字符串更加强大。

键不区分大小写,我们使用大写字母来区分键和值。



匹配列表 (ML)

在执行查询后,匹配的节点将被存储在匹配列表 (ML) 中,ML 中的节点通过节点ID进行标识。许多操作使用ML中的节点,一些操作可以改变 ML。"save"和"load"以及较新的"push"和"pop"可用于保存和检索保存的 ML。使用 action:"getIds"来获取 ML 的 ID。TX/TY/VG 接受一个节点并生成多个节点。

另一种获取 ML 的方法是使用"elements"数组。

以前搜索总是在 action 开始之前执行,不设置“查询”,可以使用默认模板来构造 ML,如果 action 与节点无关(例如 openApp、function),则效果不佳。

从 16 版本开始,实现了延迟搜索,只有需要节点时才进行搜索。

示例:

以前,即使没有指定查询,它也会隐式地基于默认模板“TP:more”进行搜索,“getIds”将打印出 ML 中的节点 ID。

>> device.sendAai({action:"getIds"})
{count: 11, ids: ['14801','1714c',...]}
>> device.sendAai({query:"TP:more", action:"getIds"})
{count: 11, ids: ['14801','1714c',...]}

16 版本的“延迟搜索”,当调用“getIds”时才会进行搜索:

>> device.sendAai({action:"getIds"})
{count: 11, ids: ['14801','1714c',...]}

由于该操作不涉及 ML,因此不会在 openApp 上执行任何搜索:

>> device.sendAai({action:"openApp(Skype)"})
{retval: true}

下面将执行示例 2 搜索,一个是默认模板“TP:more”,另一个是“TP:reduced”。 版本16,将执行一次“TP:reduced”搜索:

>> device.sendAai({action:"newQuery(TP:reduced)"})
{count: 8}

使用以下命令返回到之前的行为:

device.sendAai({action:"setConfig(selector:allowDelayedSearch,false)"})


"elements" 属性

"elements"接受一个节点ID数组,节点ID必须是屏幕上的有效节点ID,否则会生成错误。当指定"elements"时,所有元素将存储在ML中,不会执行搜索。它可以用于按特定顺序对节点应用操作。每个带有"elements"的查询将遍历所有节点以定位相应的节点,尝试一次性将多个ID放入数组中,而不是逐个放入。相同的节点ID可以出现在elements中。"elements"是唯一可以直接组成"ML"的方法,它与"forEach"结合使用非常有用。

示例:

device.sendAai({elements:["1234a", "5678b"], action:"getNodes"})

这个查询数字按钮并构建elements数组,并使用"forEach"点击所有按钮并打印结果。

>> ids = device.sendAai({query:"T:/^[1-9]$/"}).ids
a71e,aadf,aea0,b622,b9e3,bda4,c526,c8e7,cca8
>> device.sendAai({elements:ids, action:"forEach(nsClick);getText(+R:.editText_result)"})
{count: 2, list: [{retval: [{retval: true},{retval: true},{retval: true},{retval: true},{retval: true},{retval: true},{retval: true},{retval: true},{retval: true}]},{retval: '789456123'}]}

要检索一个节点,可以使用"ID:<节点ID>"。

device.sendAai({query:"ID:1234a", action:"getNodes(all)"})


测试

通过查询找到节点并不是很困难,你需要使用"UI 探测"和脚本终端:

  • 节点ID非常重要,通常对于应用程序是唯一的。
  • 脚本终端:输入 device.sendAai({query:"..."}) 来查找是否匹配该节点。有许多搜索选项,可以有多种方法找到节点。
  • UI 探测:
    • 可以使用代码助手构建查询。
    • 尝试不同的查询来查看匹配的节点。
    • 无限制的撤销/重做以比较不同的查询。
    • 查找节点内的属性。
    • UI 探测中的"Code"是定位节点的最佳方法,有关更多信息,请参考 action:"getUniqQuery"。
  • 脚本终端:当有疑问时,使用UI 探测来查找节点ID,使用 device.sendAai({elements:["<ID>","<ID>"…], action:"getNodes"}),这将帮助您获取更多信息。
  • 从一个设备开始使用 "device.sendAai",通过 "devices.sendAai" 转换到多个设备,您可以使用 Device.searchObject() 来定位所需的设备(例如:所有设备、设备组中的设备、特定设备)。

模板(TP)详细介绍

模板(TP)是一个必需的字段,TP基于数值返回节点列表,它是BQ、EQ或操作的初始节点列表。TP是Selector的一个子类,将来可能允许用户定义新的模板。TP有几个规则:

  • 每个查询只能有一个TP,查询中有两个TP将导致错误。
  • 如果未定义TP,将使用默认模板。默认模板为"TP:more",可以使用 setConfig(selector:defaultTemplate, <template>) 来更改默认模板。默认模板不能包含带有必需参数的模板(例如:TP:findText)。
  • 如果TP返回null(findText找不到文本或没有任何文本输入字段的textInput),将生成错误。
  • 如果TP包含参数,使用逗号分隔参数。TP:<template>,<argument>。
  • TP生成的节点将被保存到ML中。


TP:all, more, basic, reduced

这些模板遍历整个屏幕上的节点:

  • all:返回包括根节点在内的所有节点。
  • more:"all"选项去除布局节点。如果查询中未定义TP,则默认为此选项。
  • basic:在"more"选项的基础上,只返回子节点数为零的节点。
  • reduced:在"more"选项的基础上,使用"reduceNodes"分析屏幕上的节点,并返回关键节点。

UI 探测有3个选择:

  • 默认:TP:more
  • 全部:TP:all
  • 优化:TP:reduced

例如,第一个不带参数的getCount将获取"ML"的节点数量,没有"query"时,默认为"TP:more",因此与"TP:more"的节点数量相同。"+TP:all"将修改ML,这就是为什么第三个"getCount"返回由"+TP:all"构建的ML中的节点数量。

>> device.sendAai({actions:[ 
    "getCount", 
    "getCount(+TP:all)",
    "getCount",
    "getCount(+TP:more)",
    "getCount(+TP:basic)",
    "getCount(+TP:reduced)"
]})
{count: 6, list: [{count: 237},{count: 242},{count: 242},{count: 237},{count: 76},{count: 58}]}


TP:anyText[,min[,max]], anyDescription[,min[,max]]

该模板将返回任何文本字段中具有非空内容的文本,或者返回任何描述字段中具有非空内容的描述。可选地,可以指定找到的文本的最小和最大长度(包括在内)。如果未指定最大长度,则不会强制执行最大长度。不允许使用零作为最小/最大值。

示例:

>> device.sendAai({query:"TP:anyText", action:"getText"})
{retval: ['Calculator','AC','÷','×','DEL','7','8','9','-' ,'4','5','6','+','1','2','3','%','0','.','±','=']}
>> device.sendAai({query:"TP:anyText,1,1", action:"getText"})
{retval: ['÷','×','7','8','9','-','4','5','6','+','1','2','3','%','0','.','±','=']}


TP:findText,<text>

返回具有与指定文本匹配的文本或描述的节点。文本参数是必需的,否则会生成错误。它接受与"BQ"中的"T:<…>"类似的参数。

示例:

>> device.sendAai({query:"TP:findText,/[0-9]/", action:"getText"})
{retval: ['7','8','9','4','5','6','1','2','3','0']}
>> device.sendAai({query:"TP:findText,/^[A-Z]{1,4}$/", action:"getText"})
{retval: ['TGT','INTC','WMT','T','AAPL','SBUX','TSLA']}
>> device.sendAai({query:"TP:findText,*T", action:"getText"})
{retval: ['TGT','WMT','T']}


TP:textInput[,<提示文本>]

此模板返回可编辑的节点,如果可用,则返回可选的提示文本(需要Android 8.0或更高版本)。如果找不到节点,将生成错误。或者,您可以使用"BP:editable"获得相同的结果。

示例:

获取所有文本字段:

>>  device.sendAai({query:"TP:textInput"})
{count: 6, ids: ['e135','e8b7','f3fa','fb7c','106bf','10e41']}
>> device.sendAai({query:"BP:editable"})
{count: 6, ids: ['e135','e8b7','f3fa','fb7c','106bf','10e41']}
// Show hint text as text value, use "getHintText" to obtain hint text
>> device.sendAai({query:"TP:textInput", action:"getHintText"})
{retval: ['text','text','Number','Email','text password','Person Name']}

通过索引或提示文本找到文本字段:

>> device.sendAai({query:"BP:editable&&IX:-1", action:"getHintText"})
{retval: 'Person Name'}
>> device.sendAai({query:"TP:textInput,text", action:"getText"})
{retval: ['text','text']}
Based on the hint of the text field: query:"TP:textInput&&T:name"

使用“setText”更改内容:

>> name = "john@noname"
>> device.sendAai({query:"TP:textInput,Person Name", action:`setText(${name})`})
{retval: true}
>> device.sendAai({query:"BP:editable&&T:Email", action:"setText(john@noname.com)"})
{retval: true}
>> device.sendAai({query:"TP:textInput&&IX:-1", action:"setText(John)"})
{retval: true}
>> device.sendAai({query:"TP:textInput&&T:text ", action:"setText(Hello)"})
{retval: true}


TP:line,<top|bottom>,<行号>

目前,此功能仅适用于具有可滚动容器的应用程序,否则TP:line将返回null。FindNode将定位滚动节点之外的UI元素,行模式将将UI元素分组为一系列行,每行可以有一个或多个节点。许多应用程序在屏幕顶部和底部都有固定的UI元素,特定的节点无法与BQ或EQ结合定位。例如,"TP:line,top,1&&IX:2"将返回第一行的第二个项目,或者"TP:line,bottom,-1&&T:Chats"将返回带有Chats文本的行的底部。

  • 对于像计算器这样的非滚动应用程序,将始终返回null。
  • 在状态栏底部和可滚动节点顶部之间被视为顶部区域,行号从1开始。负行号返回倒序的行,-1表示该区域的最后一行。
  • 在可滚动节点底部和导航栏顶部之间被视为底部区域。类似地,行号从1或负行号开始。"TP:line,bottom,-1"返回该区域的最后一行的节点(也很可能是应用程序的最后一行)。
  • 使用超出范围的行号将返回null(例如,底部有2行,则TP:line,bottom,3将返回null,如果不知道行数,请使用负数)。
  • 如果一个节点的高度与同一行上的两个垂直节点的高度相似,则较低的节点将放置在下一行。
  • 许多应用程序将顶行放在可滚动区域内,TP:line,top,<number>将返回null。
  • 不会干扰"IX",节点旁边的小红色计数器或红点将被删除(不在返回列表中)。
  • 未来将增强行模式,包括非滚动区域。

示例:

>> device.sendAai({query:"TP:line,top,1"})
{count: 6, ids: ['1902b0','191936','192fbc','1cf0bc','19373e','194a03']}
To go back click the first item "IX:0":
>> device.sendAai({query:"TP:line,top,1", action:"click(IX:0)"})
{retval: true}
>>  device.sendAai({query:"TP:line,bottom,-1"})
{count: 5, ids: ['2be5b8','2c0b42','2c1a46','2c30cc','2c3fd0']}
>> device.sendAai({query:"TP:line,bottom,-1", action:"setText(BP:editable, Hello)"})
{retval: true}


TP:scrollable[,<位置>]

此模板返回由"position"标识的可滚动项(叶节点)的屏幕上的项目。"position"是从0开始的数字,从顶部到底部。在可滚动节点中使用资源ID可以获得更好的结果。默认位置为零。如果在屏幕上找不到可滚动节点,则会引发错误。"TP:scrollable"返回一个可滚动节点上的叶节点,"BP:scrollable"将匹配屏幕上的所有可滚动节点。

对于可滚动的每个项(或行/列),可能包含多个值,可以使用资源ID与之配合以返回项的特定值。例如,查询:"TP:scrollable&&R:.tv_name"。

示例:

获取当前屏幕上的所有可滚动节点:

>> device.sendAai({query:"TP:scrollable"})
{count: 5, ids: ['1e434','5bbba','5bf7b','5d9c2','6f70e']}

"BP:scrollable" 返回屏幕上所有可滚动的节点:

>> device.sendAai({query:"BP:scrollable", action:"getNodes(B)"})
{count: 5, list: [{bounds: '[0,268][1440,2738]', id: '1e434'},{bounds: '[42,457][1440,1303]', id: '5bbba'},{bounds: '[42,457][1440,1303]', id: '5bf7b'},{bounds: '[0,1303][1440,1742]', id: '5d9c2'},{bounds: '[0,1958][1440,2738]', id: '6f70e'}]}

返回设置中的所有标题(在三星手机中):

>> device.sendAai({actions:["openAndroidSetting(Settings)", "newQuery(TP:scrollable&&R:android:id/title)", "getText"]})
{count: 3, list: [{retval: true},{count: 10},{retval: ['My Name','Connections','Connected devices','Modes and Routines','Sounds and vibration','Notifications','Display','Wallpaper and style','Themes','Home screen']}]}

基本查询(BQ)详细介绍

基本查询会使用来自TP的ML,并逐个节点进行节点级别匹配,创建一个较短的ML,丢弃不匹配的节点。BQ从可访问性中提取数据,它包含单个节点的信息和操作。扩展查询中的更多功能以实现更强大的匹配(特殊字符)并添加快捷方式以使查询更简洁。



查询中的特殊字符

对于数字,例如inputType或childCount,可以接受数字或字符串:

  • <number>、"<number>"或"=<number>":精确匹配该数字。
  • "> <number>":匹配大于<number>的数字。
  • "< <number>":匹配小于<number>的数字。

字符串可以是以下之一,"\n"匹配换行符:

  • "<string>":完全匹配文本,区分大小写。
  • "(?i)<string>":不区分大小写的正则表达式匹配。
  • "ci:<string>":在字符串中不区分大小写匹配。
  • "*<string>":匹配以字符串结尾,例如"*Group"。
  • "<string>*":匹配以字符串开头,例如"android.widget.*"。
  • "*<string>*":匹配任意子字符串"*widget*"。
  • "/<regex>/":匹配任意正则表达式,例如"/ImageView|TextView/"。

"!"用于字符串开头表示NOT,"!0"表示非零。"C:!/Layout/"表示非布局类。

为了简化示例,标准前缀{"cmd": "action","action": "findnode", "params":{…}}将被省略,我们将在"param"对象中列出示例,并使用JavaScript对象表示法以减少引号的使用。



快捷方式

有两个快捷方式可用于类名(C)和资源ID(R),使用"."作为常用前缀的替代:

  • 类名(C):使用"."作为"android.widget."的替代。例如,".Button" = "android.widget.Button"。
  • 资源ID(R):使用"."作为"<package name>:id/"的替代。例如,".text_connect" = "com.sigma_rt.totalcontrol:id/text_connect"。

"."符号只能用于普通匹配和"!"匹配,其他类型的匹配(例如正则表达式和通配符)需要使用完整形式。



P:<package name>

P:<package name>

屏幕上可能有多个应用程序在运行,默认情况下,在查询中省略“P”时,将使用活跃的应用程序(占据最大屏幕区域的应用程序)。你可能希望从另一个应用程序获取信息,比如 systemui。UI Explorer 可以选择应用程序,或者使用“getAllPackageNames”命令来查找屏幕上所有正在运行的包名。

示例:

要从系统栏获取当前电池百分比,由于 OQ 不支持“P”,使用“newQuery”命令的以下方法之一:


			>> device.sendAai({query:"P:com.android.systemui&&R:.battery_percentage_view", action:"getText"})
{retval: '61%'}
>> device.sendAai({query:"P:com.android.systemui", action:"getText(R:.battery_percentage_view)"})
{retval: '61%'}
>> device.sendAai({action:"newQuery(P:com.android.systemui); getText(R:.battery_percentage_view)"})
{count: 2, list: [{retval: 28},{retval: '61%'}]}

点击导航栏中的主屏按钮:

>> device.sendAai({query:"P:com.android.systemui", action:"click(R:.home)"})
{retval: true}
			

C:<class name>,R:<resource ID>

所有节点都包含包名和类名以及可选的资源ID。由于匹配是在运行应用程序的屏幕上进行的,"P"将默认为运行的应用程序,其他"P"没有意义。因此,"P"不是必需的。



T:<text>, D:<text>, TD:<text>, T0:<text>, T1:<text> (T0 & T1 in API 18)

两个最重要的键,带有信息的节点通常包含文本或描述,这比 OCR 更准确和更快速。"until" 命令可以监视文本或描述是否发生了变化。经常使用带有特殊字符的 T:<text>。"TD" 匹配文本或描述,如果其中任何一个匹配成功,则匹配成功。“T0” 是 “T&&BP ”,“T1” 是 “T&&BP:!editable”。T1 用于防止在文本字段中查找文本。T0 用于查找没有操作的标签(祖先节点可能有操作)。

示例:

device.sendAai({query:"T:OK", action:"click"})

点击“确定”按钮。

device.sendAai({query:"T1:Name&&OX:1", action:"getText"})

这将不会找到带有“Name”的文本字段。



边界范围匹配(BI):

边界范围匹配接受屏幕坐标或边界,并根据以下条件返回节点。主要用于屏幕操作,如UI探测或调试目的。FindNode的对象模式试图在不同分辨率和尺寸的设备上查找节点,而基于坐标的"BI"将无法在所有设备上工作。

  • [x, y]:将返回包含坐标(x,y)的边界的节点。
  • [x, -1]或[-1, y]其中一个为-1:如果x = -1,则忽略x并返回包含y的边界的节点,类似地,如果y = -1,则仅匹配x。就像在y和x位置上绘制一条水平/垂直线一样,与该线接触的节点将被返回。
  • [x1, y1, x2, y2]:将匹配边界在由(x1,y1)-(x2,y2)指定的矩形内的节点边界。

示例:

{query:"BI:[-1, 1000]&&IX:-1", action:"click"}
{query:"BI:[0, 0, 1000, 1000]", action:"getBounds"}


CC:<child count>, IT:<input type>

这些是节点中的属性,很少使用。CC 是子节点的数量,IT 通常是一个数字文本字段。



BP:<property>

布尔属性在节点中很重要,这些属性具有true或false的值。允许多个值,属性名称前面的"!"将匹配false。以下是这些属性:

  • checkable:如果节点包含复选框或开关,通常与"clickable"一起出现。
  • checked:如果节点包含复选框/开关,则为true表示选中/打开,false表示未选中/关闭。
  • clickable:如果节点可点击(通常是按钮或响应点击的UI元素,如复选框)。
  • editable:如果节点允许输入或修改文本。
  • enabled:如果节点已启用,禁用的节点不响应操作。通常以灰色显示。
  • focusable:如果节点可以获取焦点。
  • focused:如果节点已聚焦,通常屏幕上只有一个焦点,选择此属性将返回null或1个节点。
  • longClickable:如果节点可长按。
  • progressible:如果节点是"progress"节点,如滑块。
  • selected:如果节点已选择。
  • none: 没有一个布尔属性返回 true。(API 18)
  • !none: 任何一个布尔属性返回 true。(API 18)
  • scrollable:如果节点是可滚动节点(容器),则该节点可以水平或垂直滚动。RecyclerView、ScrollView和ListView等类是可滚动的。

允许多个布尔属性,用逗号分隔,选择是否将其包含在引号中是可选的。“none”和“!none”不能与其他属性组合,否则会产生错误。

用法:

{query:"BP:<prop name>, !<prop name>, ..."}

示例:

>> device.sendAai({query:"TP:textInput&&BP:clickable"})
{count: 1, ids: ['27ebc0']}
>> device.sendAai({query:"TP:textInput&&BP:'clickable,enabled'"})
{count: 1, ids: ['27ebc0']}
>> device.sendAai({query:"TP:textInput&&BP:clickable,enabled"})
{count: 1, ids: ['27ebc0']}

使用"getNodes(BP)"来输出所有布尔属性:

>> device.sendAai({query:"TP:textInput&&BP:clickable,enabled", action:"getNodes(BP)"})
{count: 1, list: [{booleanProperties: {checkable: false, checked: false, clickable: true, editable: true, enabled: true, focusable: true, focused: false, longClickable: true, multiLine: true, scrollable: false, selected: false, visibleToUser: true}, id: '27ebc0'}]}

从API 14开始,支持"not" (!),您不仅可以找到true值,还可以使用"!"找到false值。

假设通过循环获取值,直到没有进一步匹配(箭头显示为灰色):

>> device.sendAai({query:"R:.btn_next_period&&BP:!enabled", action:"getCount"})
{count: 1}

反过来不成立:

>> device.sendAai({query:"R:.btn_next_period&&BP:enabled", action:"getCount"})
null
>> lastError()

找不到匹配项

通常"checkable"与"clickable"相同,要从列表中删除"checkable":

>> device.sendAai({query:"BP:clickable", action:"getCount"})
{count: 26}
>> device.sendAai({query:"BP:checkable", action:"getCount"})
{count: 10}
>> device.sendAai({query:"BP:clickable,!checkable", action:"getCount"})
{count: 16}


基本查询(BQ)示例

device.sendAai({}) → device.sendAai({query:"TP:more", action:"getIds"})

版本 16,TP:more 将在调用“getIds”时执行。

>> device.sendAai({})
{count: 9, ids: ['95b0','b779','bb3a','9d32','a0f3','a4b4','a875','ac36','aff7']}
>> device.sendAai({query:"TP:more", action:"getIds"})
{count: 9, ids: ['95b0','b779','bb3a','9d32','a0f3','a4b4','a875','ac36','aff7']}
>> device.sendAai({query:"T:/./", action:"getCount"})
{count: 25}
>> device.sendAai({query:"D:/./", action:"getCount"})
{count: 44}
>> device.sendAai({query:"TD:/./", action:"getCount"})
{count: 69}

可以包含更复杂的查询,例如:

>> device.sendAai({query:"T:/^.$/&&C:.Button&&R:.button_*", action:"getText"})
{retval: ['÷','×','7','8','9','-','4','5','6','+','1','2','3','%','0','.','±','=']}

如果找不到匹配项,它将返回null,使用"lastError()"来显示错误消息:

>> device.sendAai({query:"T:X", action:"getText"})
null
>> lastError()
No match found
>> device.sendAai({query:"X:X", action:"getText"})
null
>> lastError()
Error:Cannot find the key for "X"

可以通过编程方式检查null。

扩展查询(EQ)详细介绍

剩下的查询不是 BQ 或 TP。 EQ 中的关键通常是涉及多个节点的计算和启发式方法。 EQ 从左到右执行键控功能,一路进行操作,希望最终 ML 包含用户想要的节点。 如果没有操作,ML 就毫无意义,生成的 ML 将传递给操作以应用操作或提取信息。 密钥可以多次应用“OX:1&&OX:1”。



索引 (IX) M → O

IX:<number>。根据从零开始的位置从ML节点返回一个节点(IX:0)。如果IX是负值,则位置按相反的顺序排列(例如,-1是最后一个节点)。

示例:

>> device.sendAai({})
{count: 9, ids: ['95b0','b779','bb3a','9d32','a0f3','a4b4','a875','ac36','aff7']}
>> device.sendAai({query:"IX:1"})
{count: 1, ids: ['b779']}
>> device.sendAai({query:"IX:-2"})
{count: 1, ids: ['ac36']}


单节点选择 (ON) M → O

ON:<option> 从 ML 中返回一个节点,目前有 3 个选项可用。目前只有有限的选项,将来会扩展更多选项。

这些是选项:

  • first: 从ML中取第一个节点(类似于IX:0)。
  • last: 从ML中取最后一个节点(类似于IX:-1)。
  • min: 从ML中选择最小的节点(按面积计算)。


偏移量 (OX & OY) O → O

OX:<整数> 和 OY:<整数>。使用偏移量基于一个参考节点查找相邻节点,OX 表示水平位置,正整数向右移动,负整数向左移动。对于 OY,正整数向下移动,负整数向上移动。

示例:

在关于手机中:

>> device.sendAai({query:"T:Model name&&OX:1", action:"getText"})
{retval: 'Galaxy S22 Ultra'}
>> device.sendAai({query:"T:Model number&&OX:1", action:"getText"})
{retval: 'SM-S908U1'}
>> device.sendAai({query:"T:Android version&&OY:1", action:"getText"})
{retval: '12'}

另一个示例:

>> device.sendAai({query:"T:5&&OX:1", action:"getText"})
{retval: '6'}
>> device.sendAai({query:"T:5&&OY:-1", action:"getText"})
{retval: '8'}

// OX & OY orders may get different results
>> device.sendAai({query:"T:0&&OY:-1&&OX:2", action:"getText"})
{retval: '×'}
>> device.sendAai({query:"T:0&&OX:2&&OY:-1", action:"getText"})
{retval: '−'}


ViewGroup (VG) O → M

ViewGroup: VG 或 VG:<level>。此命令可选地接受一个级别(level),如果未指定级别,则使用1作为级别。它接受一个节点(来自查询),会进行父级遍历,直到找到一个“组”为止,级别用于指定何时停止。一个“组”是指占据屏幕宽度85%的布局,或者在类中是“ViewGroup”。ML将包含组内的节点,其中第一个节点是ViewGroup/Layout节点本身,包含了所有的节点。这对于逻辑上将感兴趣的节点分组并使用“addQuery”或BQ在组内搜索节点非常有用。可以使用“setConfig(selector:viewGroupWidthRatio, )”来修改85%。

相关操作:“viewGroup”。

示例:

在充电之前选择正确的当前,第一级VG未达到预期节点:

>> device.sendAai({query:"T:Start Charging&&VG"})
{count: 2, ids: ['53b16','53ed7']}

要获取充电组,请使用级别2。第一个节点将是包围组中所有节点的节点(绿色矩形)。第一个节点对于“showOnScreen”非常有用,以确保整个组在屏幕上可见。

>> device.sendAai({query:"T:Start Charging&&VG:2"})
{count: 24, ids: ['4a8af','4ac70','4b031','4b3f2','4bb74','4bf35','4c2f6','4c6b7','4ca78','4ce39','4d1fa','4d5bb','4d97c','4dd3d','4e0fe','4ec41','4f002','4f3c3','53755','53b16','53ed7','54298','54659','54a1a']}

获取当前的安培数:

>> device.sendAai({query:"T:Start Charging&&VG:2&&XT:/^[0-9]+ A$/", action:"getText"})
{retval: '32 A'}

获取并减少 1 安培:

>> device.sendAai({query:"T:Start Charging&&VG:2&&XT:/^[0-9]+ A$/", actions:"getText;click(OX:-1);refresh;getText"})
{count: 4, list: [{retval: '32 A'},{retval: true},{retval: true},{retval: '31 A'}]}

使用 "repeat" 减少 5 安培:

>> device.sendAai({query:"T:Start Charging&&VG:2&&XT:/^[0-9]+ A$/", actions:"getText;repeat(5,nsClick(OX:-1));refresh;getText"})
{count: 4, list: [{retval: '32 A'},{retval: [{retval: true},{retval: true},{retval: true},{retval: true},{retval: true}]},{retval: true},{retval: '27 A'}]}


交叉 (TX & TY) O → M

交叉:TX 和 TY。这些命令以一个节点作为参考节点,并将 ML 更改为包含所有水平交叉(intersectX)或垂直交叉(intersectY)的节点。匹配器将利用参考节点的边界(intersectX 的上/下边界,intersectY 的左/右边界)搜索交叉的节点。

参考节点的大小(宽度或高度)非常重要,如果选择了一个宽的参考节点,TY 将匹配屏幕上的大多数节点。明智地选择参考节点。

该命令将以第一个节点为基础,在 ML 中生成多个节点,您可以使用 "addQuery" 对它们设置更多的约束条件。

参见:"intersectX" 和 "intersectY"

示例:

在上面的偏移计算器示例中:

>> device.sendAai({query:"T:2&&TX"})
{count: 6, ids: ['bb4b','bf0c','c2cd','e0d5','e496','111a2']}
>> device.sendAai({query:"T:2&&TY"})
{count: 6, ids: ['1d115','1d4d6','a886','b3c9','bf0c','ca4f']}

但是,如果您选择宽度滚动整个屏幕的结果作为参考节点,OX 将获取一个节点(即它本身),而 OY 将获取所有节点。

>> device.sendAai({query:"R:.result&&TX"})
{count: 1, ids: ['1d4d6']}
>> device.sendAai({query:"R:.result&&TY"})
{count: 23, ids: ['1d897','1d4d6','d592','f39a','a4c5','a886','ac47','dd14','1029e','b008','b3c9','b78a','e0d5','bb4b','bf0c','c2cd','111a2','e496','c68e','ca4f','ce10','120a6','e857']}


减少节点(RN) M → M

减少节点:RN。 这是FindNode的重要功能之一,该功能接受节点ID列表,意图减少可见节点的数量。 “TL”、“BL”、“OX/OY”和“intersect”都使用reduceNodes。 请注意,如果较大的节点完全包含较小的节点,则reduceNodes将选择较小的节点。 例如,如果较大的“.Button”节点包围较小的“.TextView”节点,则将选择“.TextView”节点而不是“.Button”节点,“.TextView”节点上的“单击”仍然有效。 请参阅 UI 资源管理器“优化”模式。

示例:

>> device.sendAai({}).count
242
>> device.sendAai({query:"RN"}).count
55

相关操作:"reduceNodes"。



排序(ST) M → M

排序:ST:X|Y|YX。根据节点的边界进行排序,通常按照从上到下的方式进行结构和搜索,很可能已经按照顺序排序。除非排序很重要,否则很少有理由进行排序。

X – 比较节点的左边界

Y – 比较节点的顶部边界

YX – 比较节点的顶部边界,然后比较左边界

示例:

>> device.sendAai({query:"C:.Button", action:"getText"})
{retval: ['AC','÷','×','DEL','7','8','9','-','4','5','6','+','1','2','3','%','0','.','±','=']}
>> device.sendAai({query:"C:.Button&&ST:Y", action:"getText"})
{retval: ['AC','÷','×','DEL','7','8','9','-','4','5','6','+','1','2','3','%','0','.','±','=']}
>> device.sendAai({query:"C:.Button&&ST:YX", action:"getText"})
{retval: ['AC','÷','×','DEL','7','8','9','-','4','5','6','+','1','2','3','%','0','.','±','=']}
>> device.sendAai({query:"C:.Button&&ST:X", action:"getText"})
{retval: ['AC','7','4','1','0','÷','8','5','2','.','×','9','6','3','±','DEL','-','+','%','=']}

相关操作:排序



基本查询(BQ) M → M

基本查询:BQ:'<基本查询>' 或 "<基本查询>"。BQ 是 EQ 中的命令。考虑以下查询(从 UI 探测自定义查询中获取):

示例:

查询:T:Overall Cpu Usage&&VG

在该查询中,使用了"VG"来获取组,如果我们想要获取"Cpu Cores"的数量,以下查询将不起作用:

T:Overall Cpu Usage&&VG&&T:Cpu Cores&&OX:1 → T:Cpu Cores&&OX:1&&VG

两个"T"命令都是基本命令,第二个"T"会覆盖第一个"T",因为 BQ 命令在 EQ 之前应用。如果我们需要在 EQ 中使用基本查询的功能,以便可以匹配文本,有以下三种方法:

  • 使用BQ:'...'来包含BQ键。BQ:'T:Cpu Cores'。
  • 在基本查询中添加"X"前缀。XT:Cpu Cores。
  • 将标志"selector:allowBqAfterEq"配置为true,任何在 EQ 之后的 BQ 命令将作为 EQ 的一部分按顺序执行。

以下两种方式都可以:

  • T:Overall Cpu Usage&&VG&&XT:Cpu Cores&&OX:1
  • T:Overall Cpu Usage&&VG&&BQ:'T:Cpu Cores'&&OX:1
>> device.sendAai({query:"T:Overall Cpu Usage&&VG&&XT:Cpu Cores&&OX:1", action:"getText"})
{retval: '8'}
>> device.sendAai({query:"T:Overall Cpu Usage&&VG&&BQ:'T:Cpu Cores'&&OX:1", action:"getText"})
{retval: '8'}

从版本14开始,有一个标志允许在EQ命令之后使用任何BQ命令,在本示例中,"T:Cpu Cores"被视为EQ的一部分

>> device.sendAai({action:"setConfig(selector:allowBqAfterEq, true)"})
{retval: true}
>> device.sendAai({query:"T:Overall Cpu Usage&&VG&&T:Cpu Cores&&OX:1", action:"getText"})
{retval: '8'}


Value (V) M → M

这不应该是查询的一部分,因为它执行“操作”来设置节点的值,但是使用关联的值构造查询很方便。 事实上,该键将导致执行“设置”操作来设置值。 这只适用于ML中的第一个节点,可以在“query”或“newQuery”操作中设置,对于查询,如果设置正确,则不会返回任何内容,错误的数据类型将导致错误。 支持 3 种集合类型:

节点道具 行动 类型 示例
Checkable setChecked boolean V:true, V:false
progressible setProgress float/integer V:95.37, V:100
Editable setText string V:I am John

示列

要在日历中添加新条目:

默认情况下,操作是“getIds”:

>> device.sendAai({query:"T:Title&&V:Online Gaming Tournament&&OY:1&&OX:1&&V:true"})
{count: 1, ids: ['e72e2']}
>> device.sendAai({action:"newQuery(T:Title&&V:Online Gaming Tournament&&OY:1&&OX:1&&V:true);getIds"})
{count: 2, list: [{count: 1},{count: 1, ids: ['1a2e06']}]}
>> device.sendAai({query:"T:Title&&V:Online Gaming Tournament&&OY:1&&OX:1&&V:111"})
null
>> lastError()
Wrong format, expect boolean value


动作(Actions)

FindNode提供了许多动作命令,使用"function"可以创建更多的动作。"action"可以接受单个字符串命令或包含多个命令的字符串数组。查询是可选的,如果没有指定查询,它将包含默认模板。如果指定了查询,查询的输出将保存到ML中,命令/动作所需的节点将通过查询或可选查询从底层的ML中获取,使用action:"getIds"来获取ML内容。如果未指定任何动作,将使用action:"getIds"。"action"和"actions"的使用方式是相同的。

使用"device.sendAai()"或"devices.sendAai()"与FindNode进行通信。对于多个设备,将为每个设备创建一个线程来执行查询和动作。

当sendAai遇到来自FindNode的错误或超时时,它将返回null,并包含错误消息的"lastError()"。

>> device.sendAai({query:"T:Not there", action:"getText"})
null
>> lastError()
No match found

对于仅需要一个节点的操作(例如“单击”),将选择 ML 中的第一个节点(ON、IX、OX 和 OY 可以更改该节点)。 actions中命令较多,新版本会进行改进,可能会带来不兼容的情况,请使用action:"version"来确保版本支持所需的命令。 某些命令需要附加参数(参数将括在括号中)。

"function"可以创建更多的动作,有关说明和示例,请参见"function"命令。





数据类型

FindNode 在参数中支持以下数据类型,版本 17 添加了函数、数组和对象数据类型:

字符串 - 使用单引号、双引号或不带特殊字符的未引用字符串作为参数。

整数 - 任何数字。

双精度 - 带有小数点的任何数字。

布尔值 - true 或 false。

函数 – 函数名与括号内的参数(版本 17)。

数组 – 括号内由逗号分隔的元素(版本 17)。“retval”中的数组还将包含“count”。

对象 – 括号内具有键值对的元素(版本 17)。

null – 特殊字符,用于比较。

示例:

>> device.sendAai({action:"echo(this is a string)"})
{retval: 'this is a string'}
>> device.sendAai({action:"echo('this is a string')"})
{retval: 'this is a string'}
>> device.sendAai({action:'echo("this is a string")'})
{retval: 'this is a string'}
>> device.sendAai({action:'echo(1234567)'})
{retval: 1234567}
>> device.sendAai({action:'echo(1234567.89)'})
{retval: 1234567.89}
>> device.sendAai({action:"echo(true)"})
{retval: true}
>> device.sendAai({action:"echo(false)"})
{retval: false}
>> device.sendAai({action:"echo(getText(T:/^\\d$/))"})
{count: 10, retval: ['7','8','9','4','5','6','1','2','3','0']}
>> device.sendAai({action:"echo([101,102,103,104,105])"})
{count: 5, retval: [101,102,103,104,105]}
>> device.sendAai({action:"echo({a:1, b:2, c:[1,2,3,4,5]})"})
{retval: {a: 1, b: 2, c: [1,2,3,4,5]}}
>> device.sendAai({action:"echo(null)"})
{retval:null}

单个动作

在单个动作中,动作中只有一个命令。在这种情况下,返回的结果在"retval"中(如果发生错误,则为null)。

device/devices.sendAai({query:"...", action:<action>})

示例:

>> device.sendAai({query:"T:Product name&&OX:1", action:"getText"})
{retval: 'Galaxy S22 Ultra'}
// Absence of query, the default query of "TP:more" will be applied
>> device.sendAai({action:"getText(T:Product name&&OX:1)"})
{retval: 'Galaxy S22 Ultra'}

Error message:
>> device.sendAai({action:"getText(T:Product name&&OX:2)"})
null
>> lastError()
Query Error: Node not found on offset


多个动作

在FindNode中允许多个动作,许多FindNode操作都是通过多个动作完成的,多个动作将逐个从左到右执行,结果是一个数组,其中存储了每个动作的输出,如果其中一个动作失败,它将返回null,并且lastError()将显示失败的动作的索引(从零开始)和错误消息。有两种定义多个动作的方法:

数组:

actions:["动作1", "动作2", …]

字符串中的分号(版本 14+):

actions:"<动作1>;<动作2>"

device/devices.sendAai({query:"...", actions:[<动作1>, <动作2>, …]})
device/devices.sendAai({query:"...", actions:"<动作1>;<动作2>, …"})

示例:

下面是执行多个动作的两种方法,count将显示命令的数量,list将显示动作的输出列表。List[0]是"setText"的输出,list[1]是refresh的输出,list[2]是gettext的输出。

>> device.sendAai({query:"TP:textInput", actions:["setText(Hello)", "refresh", "getText"]})
{count: 3, list: [{retval: true},{retval: true},{retval: 'Hello'}]}
>> device.sendAai({query:"TP:textInput", actions:"setText(Hello);refresh;getText"})
{count: 3, list: [{retval: true},{retval: true},{retval: 'Hello'}]}

Error message will show the position (zero-based) of the error:
>> device.sendAai({query:"TP:textInput", actions:["setText(Hello)", "refresh", "noAction", "getText"]})
null
>> lastError()
[2 - noAction] Invalid action: noAction


动作和查询中的变量

如果操作的参数是在运行时确定的,则有 3 种方法可以进行变量替换:

  • JavaScript 字符串操作函数。 适用于操作和查询。
  • JavaScript 模板文字:应用于操作和查询。
  • aaix 辅助函数:使用变量重建字符串的简单辅助函数。

如果想在动作命令中使用变量,可以使用"+"来连接多个项,在加上引号后可能会变得繁琐:

JavaScript字符串函数:


var input = "Hello"
>> "setText('" + input + "')"
setText('Hello')
>> 'setText("' + input + '")'
setText("Hello")
>> 'setText(' + input + ')'
setText(Hello)

>> device.sendAai({query:"TP:textInput", action:["setText('" + input + "')", "click(OX:2)"]});
{count: 2, list: [{retval: true},{retval: true}]}

JavaScript模板字面量

从版本 14 开始,变量替换(或表达式)可以轻松应用于带反引号的字符串中,因为这是 JavaScript 的基本结构,它可以同时应用于查询和操作。随着字符串中多个操作(用 ";" 分隔)的引入,替换变得更加简单,它还允许跨多行的多个操作。

下面是一个示例,在输入input包含空格的情况下,建议用引号括起来:

>> device.sendAai({query:"TP:textInput", action:`setText("${input}");click(OX:2)`})
{count: 2, list: [{retval: true},{retval: true}]}

获取计算器中的所有数字:

>> var text = "/^[0-9]$/"
>> device.sendAai({query:`T:${text}`, action:"getText"})
{retval: ['7','8','9','4','5','6','1','2','3','0']}

调用函数以获取类和描述:

>> var c=()=>".CheckBox"
>> var x="T,D"
T,D
>> var d=()=>"Sunday"
>> device.sendAai({query:`C:${c()}&&D:${d()}`,action:`getNodes("${x}")`})
{count: 1, list: [{description: 'Sunday', id: '509a7', text: 'S'}]}

>> var disp=()=>"getNodes('${x}')"
>> disp()
getNodes('T,D')
>> device.sendAai({query:`C:${c()}&&D:${d()}`,action:`${disp()}`})
{count: 1, list: [{description: 'Sunday', id: '509a7', text: 'S'}]}

Multiple actions (create a function called “enterText”):
>> device.sendAai({action:"function(enterText(Hello))", 
       enterText:`setText(BP:editable, %1);
                  if(getText(BP:editable) != %1, error(Not match));
                  click(BP:editable&&OX:2);
                  sendKey(Back)
`})

aaix:

aaix是在版本 14 之前的一个简单辅助函数,用于简化动作的编写。由于引入了JavaScript模板字面量,使用"aaix"没有必要。

>> aaix("setText", input)
setText("Hello")
>> aaix("sendKey", tcConst.keyCodes.KEYCODE_HOME, 0)
sendKey(3, 0)

>> device.sendAai({query:"TP:textInput", action:[aaix("setText", input), "click(OX:2)"]})
{count: 2, list: [{retval: true},{retval: true}]}
>> device.sendAai({action:aaix("sendKey", tcConst.keyCodes.KEYCODE_HOME, 0)})
{retval: true}

如果参数是字符串,以下三个示例是相同的:

action:["intersectX(OY:1, IX:2)", "getText"]
action:["intersectX('OY:1', 'IX:2')", "getText"]
action:[aaix("intersectX", "OY:1", "IX:2"), "getText"]

大多数操作的第一个参数都是可选的查询字符串,例如:

{query:"T:John", action:"click"}
{actions:["newQuery(T:John)", "click"]}	→ {action:"click(+T:John)"}
{actions:["addQuery(T:Mary)", "click"]}	→ {action:"click(T:Mary)"}

添加的查询字符串会使命令更加简洁。以"+"为前缀的"newQuery"将清除ML,进行搜索并重建ML。"addQuery"将基于现有的ML进行搜索。



动作参数中的特殊字符

Action 参数中有几个字符对 FindNode 解析器有意义,从版本 16 开始,这 4 个字符可以通过前缀“\\”进行转义,“\\”在Java中将被翻译为“\”。

  • 单引号或双引号用空格和逗号将文本括起来。 如果使用单引号或双引号,即使文本被引号包围,解析器也无法处理。 诸如“我是约翰”、“约翰的书”之类的文本将在解析器中生成错误。
  • 逗号:逗号用于分隔多个参数。 带逗号的文本需要用引号引起来。
  • 分号:分号用于分隔单个字符串中的多个操作。
  • “\\<char>”之后的任何其他字符都将保留其值(例如正则表达式)。

setText(I\\'m John)

setText(John\\'s Book)

由于 FindNode 解析器认为括号中的未知文本类型被视为文本,因此不需要保护括号内的文本,只需使用转义字符:

>> device.sendAai({query:"T:Field 7&&OX:1", action:"setText(I\\'m John\\, she is Mary\\;);getText"})
{count: 2, list: [{retval: true},{retval: 'I'm John, she is Mary;'}]}

Domino Pizza的应用程序是“Domino's”,之前的版本 16,它无法运行,现在它可以使用转义字符:

>> device.sendAai({action:"openApp(Domino\\'s)"})
{retval: true}


同步操作

为了确保操作是同步的,FindNode付出了很大的努力。当调用"click"时,不仅会确保点击操作是同步进行的,还会等待特定的"事件"来确保渲染"开始"发生,但无法确保渲染完成。FindNode提供了几种方法来执行这种检查:

  • 操作命令,如"click":大多数情况下应该可以正常工作。
  • "waitQuery":如果新窗口的渲染时间非常长,可以使用此命令检查新节点是否可用,在waitQuery之后添加操作命令,这将确保找到节点并准备好接受操作。


可选查询 (OQ)

需要节点的操作将从ML中获取节点,需要一个节点的操作将从ML中获取第一个节点。但是,用户可以通过在操作中添加可选查询 (OQ) 来更改这种行为。

例如:{query:"T:OK", action:"click"} → {action:"click(T:OK)"}

所有 OQ 都是可选的,如果未指定 OQ,则操作将从 ML 中获取节点。如果操作需要一个节点(例如点击),则将使用 ML 中的第一个节点;如果操作支持多个节点(例如 getNodes 或 getText),则将使用整个 ML。在 “TP” 中的能力受限,任何需要参数的 TP 都将生成错误。

OQ是普通查询语言,不同类型的查询由不同的前缀标识,OQ与其他查询类似,可以生成一个或多个节点,其中一些查询类型将修改ML。以下是支持的3个前缀:

  • 没有前缀:应用来自ML的查询。ML不会改变(类似于"addQuery"操作)。
  • "+":新查询,新ML(类似于"newQuery"操作)。
  • "*":与普通查询相同,结果将替换ML。

假设有一个计算器,分为3行,分别是数字1到9,分别是"7,8,9"、"4,5,6"和"1,2,3"(从上到下):

>> device.sendAai({query:"T:/[3-9]/", actions:["getText(T:5)", "getText"]})
{count: 2, retval: [{retval: '5'},{retval: ['7','8','9','4','5','6','3']}]}
>> device.sendAai({query:"T:/[3-9]/", actions:["getText(+T:5)", "getText"]})
{count: 2, list: [{retval: '5'},{retval: '5'}]}
>>  device.sendAai({query:"T:/[3-9]/", actions:["getText(*T:5)", "getText"]})
{count: 2, list: [{retval: '5'},{retval: '5'}]}

这是常规查询和"*"查询之间的区别:

>>  device.sendAai({query:"T:/[3-9]/", actions:["getText(OX:1)", "getText(OX:1)", "getText"]})
{count: 3, list: [{retval: '8'},{retval: '8'},{retval: ['7','8','9','4','5','6','3']}]}
>>  device.sendAai({query:"T:/[3-9]/", actions:["getText(*OX:1)", "getText(*OX:1)", "getText"]})
{count: 3, list: [{retval: '8'},{retval: '9'},{retval: '9'}]}

计算器,返回可打印字符和非数字:

>>  device.sendAai({action:"getCount(+TP:anyText&&T:/[^0-9]/);getText(T:/^.$/)"})
{count: 2, list: [{retval: 11},{retval: ['÷','×','-','+','%','.','±','=']}]}

看起来"*"和"+"是类似的,考虑下面的示例,"*"匹配来自"ML"的列表(T:2不在ML中),"+"搜索整个屏幕内容。

>>  device.sendAai({query:"T:/[3-9]/", actions:["getText(+T:1)", "getText"]})
{count: 2, list: [{retval: '1'},{retval: '1'}]}
>> device.sendAai({query:"T:/[3-9]/", actions:["getText(*T:1)", "getText"]})
null
>>  lastError()
[0 - getText] Query Error:No match found

由于常规查询(无前缀)和更新ML的查询("*"前缀)将使用ML,因此只允许在"newQuery"或"+"前缀中使用"TP":

>>  device.sendAai({query:"T:/[3-9]/", actions:["getText(TP:all)", "getCount"]})
null
>>  lastError()
[0 - getText] Query Error:Key "TP" is not permitted in this query
>>  device.sendAai({query:"T:/[3-9]/", actions:["getText(+TP:all)", "getCount"]})
{count: 2, list: [{retval: [{},{},{},{},{},{},{},'Calculator',{},{},{},{},'AC','÷','×','DEL','7','8','9','-','4','5','6','+','1','2','3','%','0','.','±','=']},{retval: 32}]}
>>  device.sendAai({query:"T:/[3-9]/", actions:["getText(*TP:all)", "getCount"]})
null
>>  lastError()
[0 - getText] Query Error:Key "TP" is not permitted in this query


函数内部表达式 (VFC)版本 17

对于“if”、“assert”、“echo”、“log”和“return”,函数可以包含函数内部参数,内部函数的返回值将传递给外部函数。 如果使用“比较表达式”,则会根据比较返回布尔结果。 如果动作/函数接受函数内的表达式,则用法中的参数将显示“”(值、函数或比较)。 以下是 3 种表达形式:

格式 1:function() // 返回值为“retval”的函数调用。

格式 2:function().<key> // 具有属性访问的函数调用,将返回键的值。

格式 3:function()[.<key>] <comp 运算符> <值> // 比较表达式,将返回 true 或 false

几点说明:

  • 前 2 种形式返回单个值。 第三种形式返回 true/false 的布尔值。
  • “null”可用于“== null”、“!= null”等比较,返回“{retval:null}”有效。
  • 没有键的“function()”将默认为“retval”键。
  • “value”(右参数)还可以包含函数,在这种情况下,函数的返回值将用于比较。
  • 版本 17 上的所有操作将始终有一个返回值“retval”。 "setConfig" 可以更改以实现向后兼容
  • “forEach”和“repeat”中的函数不需要“()”。 此表达式需要带有“()”的函数以避免歧义:echo(test) 与 echo(test())。

示例:

左侧参数中需要该函数,否则会产生错误:

>> device.sendAai({action:"if(1==1, echo(true), echo(false))"})
null
>> lastError()
Wrong format, expect comparison expression
>> device.sendAai({action:"if(echo(1)==1, echo(true), echo(false))"})
{retval: true}

Use "()" in parameter to identify string from function:
>> device.sendAai({action:"echo(getCount)"})
{retval: 'getCount'}
>> device.sendAai({action:"echo(getCount())"})
{retval: 59}

对于第三种形式:比较表达式对于“if”和“assert”非常有用。 如果没有比较运算符,则可以使用函数调用的值(和可选键)来确定布尔值(仅在“if”和“assert”中,其他函数将接受作为值)。

true/false 与 JavaScript 类似(数组除外):

以下是 false:

  • Boolean: false
  • Integer: zero
  • Double: zero
  • String: zero length or null
  • Array: length of zero
  • null

以下是 ture:

  • Boolean: true
  • Integer: non-zero
  • Double: non-zero
  • String: length > 0
  • Array: length > 0

示例:

>> device.sendAai({action:"function(clickExists)", clickExists:"if(exists(T:%1), click(T:%1), return(false))"})
{retval: true}
>> device.sendAai({action:"clickExists(Skype)"})
{retval: true}
>> device.sendAai({action:"clickExists(NotThere)"})
{retval: false}

比较运算符可以是以下之一:

== or = // 测试左参数是否等于右参数

> // 测试左参数是否大于右参数

< // 测试左边参数是否小于右边参数

>= // 测试左参数是否大于等于右参数

<= // 测试左参数是否大于或等于右参数

!= // 测试左边参数是否不等于右边参数

从函数返回的左侧参数将被转换为与右侧参数相同的格式。 与 JavaScript 类似,如果返回的文本前面包含数字,它将只解析数字,直到找到非数字。 因此“85%”将被转换为 85 的整数。

有一个“null”可以用于函数返回值或右侧参数,与null进行比较是有效的,例如“if(getText() != null, click)”。

示例:

左侧参数中需要该函数,否则会产生错误:

>> device.sendAai({action:"if(1==1, echo(true), echo(false))"})
null
>> lastError()

格式错误,需要比较表达式

>> device.sendAai({action:"if(echo(1)==1, echo(true), echo(false))"})
{retval: true}

如果 T:OK 可用,请单击

>> if(exists(T:OK), click(T:OK))
{retval: true}

或者编写一个函数:

>> device.sendAai({action:"function(clickText)",clickText:"if(exists(T:%1),
click(T:%1))"})
{retval: true}
>> device.sendAai({action:"clickText(OK)"})
{retval: true}

检查 Android 版本:

>> device.sendAai({action:"function(getAndroidVersion)", getAndroidVersion:"return(getText(T:Android version&&OY:1))"})
{retval: true}
>> device.sendAai({action:"getAndroidVersion"})
{retval: '14'}
>> device.sendAai({action:"function(isAndroid14OrLater)", isAndroid14OrLater:"if(getAndroidVersion() >= 14, return(true), return(false))"})
{retval: true}
>> device.sendAai({action:"isAndroid14OrLater"})
{retval: true}

许多应用程序有多个屏幕深度,要返回到应用程序的顶部,您需要检查是否找到顶部屏幕,否则使用返回按钮直到找到为止,您可以运行直到找到 true 为止:

>> device.sendAai({action:"function(goBack)", goBack:"if(exists(T:%1), return(true), sendKey(Back));return(exists(T:%1))"})
{retval: true}
>> device.sendAai({action:"goBack(WhatsApp)"})
{retval: false}
>> device.sendAai({action:"goBack(WhatsApp)"})
{retval: true}

显示 3 种不同格式的输出:

>> device.sendAai({action:"echo(listAndroidSettings())"})
{retval: ['SETTINGS','ACCESSIBILITY_SETTINGS', ..., 'ZEN_MODE_PRIORITY_SETTINGS']}
>> device.sendAai({action:"echo(listAndroidSettings().count)"})
{retval: 70}
>> device.sendAai({action:"echo(listAndroidSettings().count > 50)"})
{retval: true}
>> device.sendAai({action:"echo(listAndroidSettings().count > 100)"})
{retval: false}

数组和对象示例:

>> device.sendAai({action:"echo({})"})
{retval: {}}
>> device.sendAai({action:"echo({a:1, b:2})"})
{retval: {a: 1, b: 2}}
>> device.sendAai({action:"echo({a:1, b:2, c:[100,200,300]})"})
{retval: {a: 1, b: 2, c: [100,200,300]}}
>> device.sendAai({action:"echo([])"})
{retval: []}
// Array will put count automatically
>> device.sendAai({action:"echo([1,2,3])"})
{count: 3, retval: [1,2,3]}

数组比较:

>> device.sendAai({action:"if(echo([1,2,3]) == [1,2,3], echo(true), echo(false))"})
{retval: true}
>> device.sendAai({action:"if(echo([1,2,3]) == [1,2,5], echo(true), echo(false))"})
{retval: false}

对象比较,顺序并不重要:

>> device.sendAai({action:"if(echo({a:1, b:2}) == {b:2, a:1}, echo(true), echo(false))"})
{retval: true}
>> device.sendAai({action:"if(echo({a:1, b:2}) > {b:2, a:1}, echo(true), echo(false))"})
null
>> lastError()
This data type only allow == or !=

函数返回对象:

>> device.sendAai({action:"function(getObj)", getObj:"return({a:37,b:[100,200,300],c:{x:50, y:60}})"})
{retval: true}
>> device.sendAai({action:"if(getObj() == 37, echo(true), echo(false))"})
null
>> lastError()
getObj() == 37: Unmatched or unsupported data type: JSONObject vs Integer
>> device.sendAai({action:"if(getObj().a == 37, echo(true), echo(false))"})
{retval: true}
>> device.sendAai({action:"if(getObj().b == [100,200,300], echo(true), echo(false))"})
{retval: true}
>> device.sendAai({action:"if(getObj().b == [100,200,400], echo(true), echo(false))"})
{retval: false}
>> device.sendAai({action:"if(getObj().c == {y:60, x:50}, echo(true), echo(false))"})
{retval: true}
// Same function
>> device.sendAai({action:"function(dupObj)", dupObj:"getObj"})
{retval: true}
>> device.sendAai({action:"dupObj"})
{retval: {a: 37, b: [100,200,300], c: {x: 50, y: 60}}}
>> device.sendAai({action:"if(getObj() == dupObj(), echo(true), echo(false))"})
{retval: true}
>> device.sendAai({action:"if(getObj().c == dupObj().c, echo(true), echo(false))"})
{retval: true}
>> device.sendAai({action:"echo(getObj().b)"})
{count: 3, retval: [100,200,300]}

Get 命令



getBounds (M)

返回ML中节点的边界(格式:[左,上,右,下]),它返回两个数组,一个包含所有ID,另一个包含所有边界。

用法:

getBounds[(<查询>)]

返回:

边界数组,每个边界都是一个由4个元素构成的数组:[x1, y1, x2, y2]。

>> device.sendAai({query:"T:/[0-9]/", action:"getBounds"})
{bounds: [[18,1158,370,1521],[370,1158,722,1521],[722,1158,1074,1521],[18,1521,370,1884],[370,1521,722,1884],[722,1521,1074,1884],[18,1884,370,2247],[370,1884,722,2247],[722,1884,1074,2247],[18,2247,370,2610]], count: 10, ids: ['98d1','9c92','a053','a7d5','ab96','af57','b6d9','ba9a','be5b','c5dd']}


getBoolProp (O)

返回节点的布尔属性,它将返回"editable"、"progressible"、"clickable"、"checkable"、"scrollable"或"longClickable"。如果找不到属性,则返回null。

用法:

getBoolProp [(<查询>)]

返回:

“retval”中的字符串属性数组

>> device.sendAai({query:"T:/12:30\\sPM/&&VG", action:"getBoolProp(T:Every day&&OX:1)"})
{retval: ['checkable','clickable']}
>> device.sendAai({query:"T:/12:30\\sPM/&&VG&&PQ:'T:Every day&&OX:1'", action:"getBoolProp"})
{retval: ['checkable','clickable']}
>> device.sendAai({query:"T:ci:calculator", action:"getBoolProp"})
null
>> lastError()
No boolean property is found


getBoolPropInAncestors(O)

与getBoolProp类似,不同之处在于它遍历父节点直到达到顶级节点。

用法:

getBoolPropInAncestors [(<查询>)]

返回:

属性和节点ID的数组数组,如果未找到,则返回null。

>> device.sendAai({query:"T:John", action:"getBoolPropInAncestors"})
{count: 2, retval: [['clickable','1b86f'],['scrollable','16d5b']]}


getChecked (O/M)

该命令返回“checkable”节点的布尔值。 通常,切换控件、复选框或单选按钮是可选的。 例如,“.Switch”或“.Checkbox”的类名是可选的。

用法:

getChecked [(<查询>)]

返回:

如果输入的是一个节点,则在“retval”中返回true/false,如果输入的是多个节点,则retval将包含true/false数组,对于不可检查的节点,将显示为“N/A”。 如果没有找到可检查的节点,则返回 null。 “BP:checkable”查找可检查的节点,“BP:checked”查找已检查的节点。

另请参阅: setChecked

>> device.sendAai({action:"getChecked(D:Sunday&&TX)"})
{retval: [false,true,true,true,true,true,false]}

如果不想使用描述,可以使用类来进行搜索,没有其他UI元素的类名是“.CheckBox”:

>>  device.sendAai({action:"getChecked(C:.CheckBox&&IX:0&&TX)"})
{retval: [false,true,true,true,true,true,false]}

要找到所有选中的描述:

>> device.sendAai({action:"getDescription(D:Sunday&&R:.day_button_0&&TX&&XBP:checked)"})
{retval: ['Monday','Tuesday','Wednesday','Thursday','Friday']}


getCount (O/M)

该命令返回 ML 或 OQ 的长度。 如果使用 OQ,如果 OQ 中没有找到匹配,则返回零。

用法:

getCount [(<查询>)]

返回:

以“retval”形式返回 ML 或 OQ 的长度(版本 17 之前,以“count”形式返回)。

示例:

>> device.sendAai({action:"getCount"})
{retval: 26}
>> device.sendAai({action:"getCount(T:/\\d/)"})
{retval: 10}
>> device.sendAai({query:"T:Not there", action:"getCount"})
null
>> lastError()
No match found
>> device.sendAai({action:"getCount(T:Not there)"})
{retval: 0}


getDescription/getText/getHintText (O|M)

这些是“getNodes”命令的简化版本,更容易获取文本、描述和提示文本的内容。

这些命令会检测一个或多个节点。如果检测到一个节点,它将返回相应的值。否则,它将返回文本/描述或提示文本的数组。

用法:

getDescription [(<query>)]

getHintText [(<query>)]

getText [(<query>)]

返回值:

如果找到多个节点,它会返回包含文本/描述/提示文本的数组。如果找到一个节点,它会返回文本/描述。

>>  device.sendAai({query:"TP:line,bottom,-1", postAction:"getText"})
{retval: ['Chats','Calls','Contacts','Notifications']} 
// One value will not return an array
>> device.sendAai({query:"TP:line,bottom,-1&&IX:1", postAction:"getText"})
{retval: 'Calls'} 

>> device.sendAai({query:"BP:editable", action:"getText"})
{retval: ['text','text','Number','Email','text password','Person Name']}
>> device.sendAai({query:"BP:editable", action:"setText(T:Number, '100');setText(T:text password, secretWord)"})
{count: 2, list: [{retval: true},{retval: true}]}
>> device.sendAai({query:"BP:editable", action:"getText"})
{retval: ['text','text','100','Email','••••••••••','Person Name']}
// getHintText will maintain the value of the original hints, getText does not
>> device.sendAai({query:"BP:editable", action:"getHintText"})
{retval: ['text','text','Number','Email','text password','Person Name']}


获取焦点(getFocus)

返回当前获取焦点的节点(在getNodes(BP)中为"focusable")(通常是文本字段或滑块),如果没有节点获取焦点,则返回null。如果找到节点,将设置ML为找到的节点。

用法:

getFocus

返回:

找到节点 ID 时返回“retval”,否则返回 null 并出现错误。

示例:

输入下一行以从现有行输入:

>> device.sendAai({action:["getFocus", aaix("setText", username), "addQuery(OY:1)", aaix("setText", password)]})
{count: 4, list: [{retval: '67693'},{retval: true},{count: 1},{retval: true}]}


getFuncRetval 版本 17

在执行函数期间,每个命令的输出都会被存储。 要显示当前函数的输出,请使用 getFuncRetval。 建议使用 getFuncRetval 作为函数内的最终命令以获得最佳结果。 当不带任何参数调用 getFuncRetval 时,它将检索当前函数期间执行的所有命令的输出。 但是,如果提供了参数,getFuncRetval 将返回与指定命令相对应的输出。 此功能在包含大量命令的函数中特别有用,特别是当对特定命令(例如 getText)的输出感兴趣时。 需要注意的是,getFuncRetval 必须在函数上下文中执行,并且不会影响 forEach 和 Repeat 等操作。 使用搜索命令时,仅包含命令本身,不带任何参数。

用法:

getFuncRetval[(<搜索命令>)]

返回:

返回“retval”以及 JSON 对象格式的输出数组。 如果找不到搜索命令,将会产生错误。

示例:

>> device.sendAai({action:"function(testEcho)", testEcho:`
    echo(This is a test to echo in different types);
    echo(true);
    echo(1000);
    echo(3.14159);
    echo({a:1, b:{x:1, y:'string'}, c:[100, 200, 300]});
    getFuncRetval()`})
{retval: true}
>> device.sendAai({action:"testEcho"})
{retval: [{retval: 'This is a test to echo in different types'},{retval: true},{retval: 1000},{retval: 3.14159},{retval: {a: 1, b: {x: 1, y: 'string'}, c: [100,200,300]}}]}

获取特斯拉当前位置:

>> device.sendAai({action:"function(getAddress)", getAddress:`
    click(T:Location);
    newQuery(R:.map_header_text, 1000);
    getText;
    sendKey(Back);
    getFuncRetval(getText)`});
{retval: true}
>> device.sendAai({action:"getAddress"})
{count: 1, retval: [{retval: '9999 Ocean Drive'}]


获取ID(getIds)

如果未定义action,则默认使用此命令,该命令将返回ML的计数和ID数组。

用法:

getids [(<query>)]

返回:

计数和ID数组。

>> device.sendAai({})
{count: 26, retval: ['7708','d8a2','e7a6','ef28','824b','860c','89cd','8d8e','914f','9510','98d1','9c92','a053','a414','a7d5','ab96','af57','b318','b6d9','ba9a','be5b','c21c','c5dd','c99e','cd5f','d120']}
>> device.sendAai({action:"getIds(C:.Button)"})
{count: 4, retval: ['8dc5','9547','9908','9cc9']}


获取节点(getNodes)

此命令用于检索节点的信息。字段可以确定要显示的内容。"getNodes"将在检索信息之前刷新节点。

用法:

getNodes

getNodes([<query>] [,<fields>])

参数:

fields:可选的"fields"用于定义要返回的字段,多个字段使用逗号分隔。由于"field"中包含逗号,因此需要使用引号。字符串。有效的字段标识符包括:

  • P:包名(字符串)
  • C:类名(字符串)
  • R:资源ID(字符串)
  • D:描述(字符串)
  • T:文本(字符串)
  • IT:输入类型(整数)
  • CC:子节点数量(整数)
  • RI:RangeInfo,提供有关小部件类型、最小值、最大值、当前值的更多信息,例如SeekBar(滑块),使用"setProgress"命令更改值。
  • BP:节点中的所有布尔属性。
  • B:返回节点的边界[左 上][右 下](字符串)
  • All:所有字段

返回:

节点信息的计数和数组。

示例:

>> device.sendAai({action:"getNodes(T:/[0-9]/,'R,T')"})
{count: 12, retval: [{id: '9b0a', resourceId: '.editText_history', text: '1360+100'},{id: '9ecb', resourceId: '.editText_result', text: '=1460'},{id: 'b190', resourceId: '.button_seven', text: '7'},{id: 'b551', resourceId: '.button_eight', text: '8'},{id: 'b912', resourceId: '.button_nine', text: '9'},{id: 'c094', resourceId: '.button_four', text: '4'},{id: 'c455', resourceId: '.button_five', text: '5'},{id: 'c816', resourceId: '.button_six', text: '6'},{id: 'cf98', resourceId: '.button_one', text: '1'},{id: 'd359', resourceId: '.button_two', text: '2'},{id: 'd71a', resourceId: '.button_three', text: '3'},{id: 'de9c', resourceId: '.button_zero', text: '0'}]}

>> device.sendAai({query:"C:.SeekBar", action:"getNodes(RI)"})
{count: 1, retval: [{id: 'b6f0', rangeInfo: {current: 0, max: 100, min: 0, type: 0, typeString: 'int'}}]}
>> device.sendAai({query:"C:.SeekBar", action:"getNodes(BP)"})
{count: 1, retval: [{booleanProperties: {checkable: false, checked: false, clickable: false, editable: false, enabled: true, focusable: true, focused: false, longClickable: false, multiLine: false, scrollable: false, selected: false, visibleToUser: true}, id: 'b6f0'}]}


获取包名(getPackageName), 获取当前包名(getCurrentPackageName (API 18)), 获取所有包名(getAllPackageNames (API 18))

“getPackageName” 返回当前正在运行的包名,它与 “getCurrentPackageName” 相同,“getAllPackageNames” 将返回屏幕上所有当前正在运行的包名。

用法:

getPackageName

返回:

包名在"retval"中。

>> device.sendAai({action:"getPackageName"})
{retval: 'com.google.android.gm'}
>> device.sendAai({action:"getAllPackageNames"})
{count: 3, retval: ['com.android.systemui','com.sec.android.app.launcher','com.sigma_rt.sigmatestapp']}


获取进度(getProgress)

返回UI元素的进度类型的值,例如滑块("getNodes(RI)"的简写):

用法:

getProgress [query]

返回:

进度节点的RangeInfo在"retval"中,如果节点不是可进度节点,则返回null:

RangeInfo具有3种类型之一:"float" - 在最小值和最大值之间的浮点数,"int" - 在最小值和最大值之间的整数,"percent" - 在0和1之间。"current"包含当前值。

>> device.sendAai({action:"getProgress(C:.SeekBar)"})
{retval: {current: 41, max: 100, min: 0, type: 0, typeString: 'int'}}

另请参阅: setProgress



获取查询字符串(getQuery/getUniqQuery)

接受一个节点并尝试生成匹配该节点(或多个节点)的查询字符串。从"getQuery"返回的查询字符串可以匹配多个节点,有助于获取多个节点的文本或描述信息。从"getUniqQuery"返回的查询字符串只能匹配一个节点,获取到的查询字符串将被发送到其他设备以定位节点。目前,这还比较基础,我们将在将来开发更好的查询字符串:

  • 将匹配列表(查询或元素)的第一个节点作为参考节点。
  • 输出尚未完全优化,例如,它不使用模板。
  • 如果找到多个节点,"getUniqQuery"将添加"IX"来标识单个节点,这在不使用资源ID的应用程序中容易出错,IX可能是一个很大的数字,UI的任何更改都可能影响准确性。
  • UI 探测的"Code"按钮使用此命令来显示查询字符串。
  • 如果使用偏移量(OX|OY),它将搜索具有文本信息(例如T:<text>)的相邻节点。
  • 如果文本或描述信息是动态的,请使用ignoreText/ignoreDescription。
  • -对于文本和描述,如果长度超过30个字符,它将列出前30个字符并在末尾添加"*",使用setConfig(text:maxQueryLength, <length>)来更改。例如,如果节点的文本是"The quick brown fox jumps over the lazy dog",生成的查询将是:"T:The quick brown fox jumps over*"。

用法:

getUniqQuery/getQuery - 默认情况下,使用文本和描述作为查询的一部分。

getUniqQuery/getQuery(<true/false>) - 第一个参数为true时,忽略搜索查询中的文本。

getUniqQuery/getQuery(<true/false>, <true/false>) - 第二个参数为true时,忽略搜索查询中的描述。

返回:

返回查询字符串。

获取每个节点的查询字符串:

>> device.sendAai({query:"T:Controls&&TX"}).ids.forEach(
    x => print(device.sendAai({elements:[x],action:"getUniqQuery"}).retval)
)
T:Controls&&OX:-1
T:Controls
T:Controls&&OX:1

>> var query = device.sendAai({query:"T:AAPL", action:"getQuery(true)"}).retval
>> query
C:.TextView&&R:.ticker&&CC:0
>> device.sendAai({query:query, action:"getText"})
{retval: ['MSFT','INTC','TGT','T','AAPL','SBUX','TSLA','CSCO']}

查询命令

以下命令(除了waitQuery)将涉及到ML。



newQuery/addQuery/waitQuery/exists

所有都接受查询字符串,“newQuery”将启动新的查询并重新创建 ML(类似于 OQ 中的“+”)。 “addQuery” 在 ML 中执行搜索,将结果节点放入 ML 中(类似于 OQ 中的“*”)。 “waitQuery”不会更改 ML,它具有超时,会确保在超时到期之前完成查询,否则将返回 null。 由于 waitQuery 不会更改 ML,如果您想要像 "newQuery" 那样更改 ML,请使用带超时的“newQuery”。 “exists”将返回 true 或 false 来指示查询是否会找到匹配的节点,存在的“超时”仅适用于 OQ 中的新查询(“+”<查询>)。

例如:"VG"通常会跟随"addQuery"而不是"newQuery"。另一方面,点击以打开新窗口,需要使用"newQuery"。

newQuery(<query>[, <timeout>])

addQuery(<query>)

exists(<query>[, <timeout>])

waitQuey(<query>[, <timeout>]) // 默认超时设置为2000毫秒

返回:

newQuery 和 addQuery 在“retval”中返回结果 ML 的大小。 当超时到期时 waitQuery 将返回 null,否则“retval”将包含 true。

示例:

以下命令将获得相同的结果,但"newQuery"将更改 ML 并影响后续使用 ML 的操作:

>> device.sendAai({action:`waitQuery(T:${text},10000);click(T:${text})`})
{count: 2, list: [{retval: true},{retval: true}]}
>> device.sendAai({action:`newQuery(T:${text},10000);click`})
{count: 2, list: [{retval: 1},{retval: true}]}


intersectX/intersectY (O → M)

如果指定了查询,将使用查询的第一个节点进行交集运算,否则将使用 ML 的第一个节点。该命令将取一个节点,并生成更多节点并存储在 ML 中。

用法:

intersect?[(<query>)]

返回:

true/false:如果操作成功。如果操作成功,将返回"count"。

如果指定了“后置查询”,则将后置查询的输出(参见"addQuery")存储为 ML。如果未指定“后置查询”,则交集的输出将替换 ML。

另请参阅:有关更多解释,请参阅“TX”和“TY”查询

示例:

假设计算器应用

>> device.sendAai({actions:["intersectX(T:3)", "getText"]})
{count: 2, list: [{retval: 4},{retval: ['1','2','3','%']}]}

"intersect Y"可能包括其他不可见的节点,例如结果或视图组。

>>  device.sendAai({actions:["intersectY(T:3)", "getText"]})
{count: 2, list: [{retval: 8},{retval: [{},{},{},'×','9','6','3','±']}]}

添加一个正则表达式以确保至少有一个字符可见:

>>  device.sendAai({actions:["intersectY(T:3)", "addQuery(T:/.+/)", "getText"]})
{count: 3, list: [{retval: 8},{count: 5},{retval: ['×','9','6','3','±']}]}

这也可以起作用:

>> device.sendAai({actions:["intersectY(T:3)", "getText(T:/.+/)"]})
{count: 2, list: [{retval: 8},{retval: ['×','9','6','3','±']}]}


push/pop & load/save (M)

这些命令用于存储和检索 ML,仅在多个命令时有用。push/save 命令用于保存 ML 状态,newQuery + actions,pop/load 命令用于检索最后保存的 ML 状态并继续。save/load 命令是为了兼容性的原因,因为它们没有限制存储 ML 的数量。实际上,在实现中,"save"和"push"、"load"和"pop"是相同的。可以使用多个"save"和"load"。

用法:

push

pop

save

load

返回值:始终返回 true 的"retval",如果堆栈中没有可弹出的内容,将生成错误。



reduceNodes (M → M)

与"RN"查询完全相同,没有参数。将返回结果后的节点数量作为"count"。

用法:

reduceNodes

返回:

在"retval"中返回结果节点的数量。

另请参阅:查询"RN"

>> device.sendAai({query:"TP:reduced"}).count
54
>> device.sendAai({action:"reduceNodes"})
{count: 54}


Sort (M → M)

基于节点边界进行排序。与"ST"查询完全相同,具有相同的参数。

用法:

sort(X|Y|YX)

返回:

在"retval"中返回 true/false。

另请参阅:查询"ST"



viewGroup (O → M)

该命令类似于“VG”查询,将接受级别和查询。 查询是否已定义用于标识要搜索组的第一个节点。 与“VG”类似,它会改变 ML。 以前此命令称为“getViewGroup”。

用法:

viewGroup

viewGroup([<查询>,] <层级>)

返回:

如果找到ViewGroup,它将返回ML中的计数,如果未找到ViewGroup,它将返回null。

另请参阅:查询"VG"

>> device.sendAai({query:"T:Battery&&VG", action:"getText(T:/\\d%/)"})
{retval: '92% available'}
>> device.sendAai({actions:["viewGroup(T:Battery)","getText(T:/\\d%/)"]})
{count: 2, list: [{count: 6},{retval: '92% available'}]}

动作命令

此部分的命令对节点执行操作。



click, nsClick, click2, longClick

对节点执行不同类型的点击操作,这是同步点击,会确保在控制权返回时屏幕开始刷新。

  • click:在节点的中心进行点击。
  • longClick:长按并较长时间(500毫秒)进行点击以模拟长按。
  • click2:在节点的随机位置进行点击。

上述所有点击操作都会监视点击后屏幕变化的事件,如果屏幕没有变化,这些点击操作将失败并最终超时。如果点击后屏幕没有发生变化,可以使用"nsClick":

  • nsClick:执行点击操作,但不监视事件。

"nsClick" 也接受两个整数,如果使用硬编码的 x 和 y 坐标值,则会在新设备、分辨率或更新的应用上失败,脚本不具备可移植性。某些使用原生界面的应用程序,一些节点可以被找到(例如,WebView),可以使用找到的节点来计算缺失节点的位置并执行点击操作。使用"getBounds"或"getNodes(B)"来获取节点的尺寸(或边界)。

用法:

click/nsClick/click2/longClick [(<query>)]

nsClick(<x>, <y>) 版本 17

返回:

true 或 false,表示操作是否成功。

click, nsclick, click2, longClick

click(<query>)、nsclick(<query>)、click2(<query>)、longClick(<query>)

示例:

>> device.sendAai({query:"T:5", action:"click"})
{retval: true}
>> device.sendAai({action:"nsClick(T:5)"})
{retval: true}

计算器应用,使用按钮 4 的尺寸来计算按钮 5 的位置并执行点击:

>> device.sendAai({action:"getBounds(T:4)"})
{bounds: [[18,1585,370,1981]], count: 1, ids: ['a641']}
>> var dim = device.sendAai({action:"getBounds(T:4)"}).bounds[0]
>> var x = (dim[2]-dim[0])/2 + dim[2]
>> var y = (dim[3]-dim[1])/2 + dim[1]
>> print(x + "," + y)
546,1783
>> device.sendAai({action:`nsClick(${x}, ${y})`})
{retval: true}


echo,log 版本 17

echo 和 log 的用法完全相同,echo 在“retval”中显示输出,log 在 logcat 中显示输出。 他们接受以下类型:

  • 值:布尔值、整数、双精度值、字符串、函数、数组和对象。
  • 函数调用:带返回值的函数调用。
  • 比较表达式:如果使用比较运算符,将返回 true 或 false。
  • 对于字符串,如果字符串中包含有效的算术表达式,则执行并输出结果。

用法:

echo ()

log ()

返回:

  • echo 将在“retval”中显示返回值。
  • 日志将始终显示 retval: true。

示例:

>> device.sendAai({action:"echo(string); echo(100); echo(3.14159); echo(true); echo(false); echo(null)"})
{count: 6, list: [{retval: 'string'},{retval: 100},{retval: 3.14159},{retval: true},{retval: false},{retval: 'null'}]}

执行简单的算术,目前不太有用:

>> var s = "(123 + 456) * (789-456)"
>> device.sendAai({action:`setText(BP:editable,${s}); echo(getText(BP:editable))`})
{count: 2, list: [{retval: true},{retval: 192807}]}
>> device.sendAai({action:`setText(BP:editable,${s});log(getText(BP:editable))`})
{count: 2, list: [{retval: true},{retval: true}]}

Logcat 将显示“日志:192807”。



errorVersion 18

在调用 sendAai() 时将停止函数执行,并返回 null,同时 lastError() 会包含“User Exception: ”和错误信息。

用法:

error (<错误信息>)

返回:

将停止执行,不需要返回值。

示例:

>> device.sendAai({query:"P:com.android.systemui&&R:.battery_percentage_view",   
    action:`if(getText() < 10, 
                error(battery too low), 
                echo(battery level is adequate))
`})
null
>> lastError()
User Exception: battery too low
			


forEach (M)

此操作循环遍历ML中的每个节点,并逐个执行提供的操作。有几个规则适用:

  • 不能执行可能改变ML的操作,否则会生成错误。
  • 所有带有前缀("+"或"*")的可选查询(OQ)将导致错误。
  • 所有循环遍历的操作只会看到一个节点。
  • 返回值:
    • 对于参数中的单个操作,"retval"数组将用于存储每个节点的输出。
    • 对于多个操作,JSON名称将包含数组的数组,内部数组将是一个节点的执行结果,外部数组将包含所有节点的数组。

对于多个操作,"forEach"使用JSON对象的名称,其值定义了操作,可以是数组或以分号分隔的字符串。对于单个操作,可以直接将操作名称放在参数中。

用法:

forEach(<操作名称>)

forEach(<JSON名称>)

返回:

  • 对于参数中的单个操作,"retval"数组将用于存储每个节点的输出。
  • -对于多个动作,“retval”将包含数组的数组,内部数组将是一个节点的执行结果,外部数组将包含所有节点的数组。

示例:

第一行运行了一次"getText",第二行运行了4次"getText"。

>> device.sendAai({query:"T:1&&TX", action:"getText"})
{retval: ['1','2','3','%']}
>> device.sendAai({query:"T:1&&TX", action:"forEach(getText)"})
{retval: [{retval: '1'},{retval: '2'},{retval: '3'},{retval: '%'}]}

>> device.sendAai({query:"R:.ticker&&T:/TSLA|AAPL|INTC|CSCO/", actions:"forEach(getTicker)", getTicker:"refresh(OX:1);getDescription(OX:1)"})
{retval: [[{retval: true},{retval: '160.25'}],[{retval: true},{retval: '190.41'}],[{retval: true},{retval: '50.51'}],[{retval: true},{retval: '29.36'}]]}

>> device.sendAai({query:"R:/.day_button_[0-6]/", action:"getText"})
{retval: ['S','M','T','W','T','F','S']}
>> device.sendAai({query:"R:/.day_button_[0-6]/", action:"forEach(setChecked(true))"})
{retval: [{changedCount: 0},{changedCount: 0},{changedCount: 0},{changedCount: 1},{changedCount: 1},{changedCount: 0},{changedCount: 1}]}

在示例文档中,对于计算器,它填入所有的操作在"elements"中,并使用"forEach(nsClick)"来输入计算。



function/funcExists/funcDelete/funcReset/funcList

这个动作命令将创建一个由以下组成的新动作:

  • 创建函数后,可以通过接受函数或操作的操作/函数来调用该函数。 不要创建与现有命令相同的名称,否则永远不会调用它。
  • 它支持批处理文件风格的变量替换,%<number>对应于调用参数。 与普通操作参数一样,替换可以是字符串、浮点、布尔值(true/false)和整数。 当调用“function”创建动作时,参数不足将使用默认值。 任何%后面不带数字都不会受到干扰,如果需要“%1”作为参数的一部分(例如setText),则使用“%%”来代表“%”,因此“%%1”将变成“%” 1”,不进行替换。
  • 它支持默认值,在设置函数时,新的参数将成为默认值。如果动作参数少于默认参数数量,将使用默认值。
  • 不要使用递归(或函数调用导致循环),它可能会导致崩溃。
  • 在 17 版本之前,函数将返回函数中每个命令的返回值。 从版本 17 开始,函数将返回函数中最后一个命令的返回值。 “return”命令可以更改该行为(有关更多信息,请参阅“return”操作)。 “forEach”和“repeat”将保留以前的返回值。

用法:

创建新函数:

function(<JSON中的动作名称>(参数)) <JSON名称>:<动作>,用[]或";"或单个动作

删除已创建的函数:

funcDelete(<函数名称>)

返回函数是否存在(true/false):

funcExists(<函数名称>)

以数组和计数形式返回函数名称列表(版本 17):

funcList

重置 - 删除所有函数

funcReset

返回值:

如果发生错误,返回 null,否则在 retval 中返回 true。

示例:

假设我们想要使用计算器来添加两个个位数的值:

>> device.sendAai({action:"function(add(1,2))", add:"click(T:%1);click(T:+);click(T:%2);click(T:=);getText(R:.editText_result)"})
{retval: true}

这个命令接受两个替换参数%1和%2,%1的默认值是"1",%2的默认值是"2"。

以下命令将使用%1("3")替换第一个参数值,使用%2("4")替换第二个参数值,从而点击"3","+","4","="。

由于函数返回最后一个命令的返回值,即getText:

>> device.sendAai({action:"add(3,4)"})
{retval: '=7'}

要恢复旧行为,请使用“getFuncRetval()”:

>> device.sendAai({action:"function(add(1,2))", add:"click(T:%1);click(T:+);click(T:%2);click(T:=);getText(R:.editText_result);getFuncRetval()"})
{retval: true}
>> device.sendAai({action:"add(3,4)"})
{retval: [{retval: true},{retval: true},{retval: true},{retval: true},{retval: '=7'}]}

以下命令没有指定参数,将使用两个默认值,因此点击"1","+","2","="。

>> device.sendAai({action:"add"})
{retval: [{retval: true},{retval: true},{retval: true},{retval: true},{retval: '=3'}]}

指定了一个参数,%1将为"9",%2 将为默认值"2",点击"9","+","2","="。

>> device.sendAai({action:"add(9)"})
{retvak: [{retval: true},{retval: true},{retval: true},{retval: true},{retval: '=11'}]}

检查函数是否存在并删除一个函数。

>> device.sendAai({action:"funcExists(add)"})
{retval: true}
>> device.sendAai({action:"funcDelete(add)"})
{retval: true}
>> device.sendAai({action:"funcExists(add)"})
{retval: false}
>> device.sendAai({action:"add(9)"})
null
>> lastError()
Invalid action: add

另一个示例:这个函数“sendText”用于在 Skype 中发送文本,在文本字段中输入文本并单击发送按钮:

>> device.sendAai({action:"function(sendText(Hello))", sendText:[
    "newQuery(TP:textInput)", 
    "setText(%1)",
    "click(OX:2)",
    "getFuncRetval"
]})
{retval: true}
>> device.sendAai({action:"sendText(How are you?)"})
{sendText: [{count: 1},{retval: true},{retval: true}]}
>> var input = "Good morning"
>> device.sendAai({action:`sendText(${input})`})
{sendText: [{count: 1},{retval: true},{retval: true}]}

下面的函数将查找一个名称,点击进入聊天界面,使用刚刚创建的"sendText"函数发送文本,然后点击返回键回到主界面:

>> device.sendAai({action:"function(findAndSend)", findAndSend:"scrollIntoView(T:%1);click;sendText(%2);sendKey(Back);getFuncRetval"})
{retval: true}
>> device.sendAai({action:"findAndSend(John Doe, Nice to meet you)"})
{findAndSend: [{retval: true},{retval: true},{sendText: [{count: 1},{retval: true},{retval: true}]},{retval: true}]}

另一个示例:获取来自mySolarEdge应用程序的SolarEdge产量:

>> device.sendAai({actions:"function(getProduction)", getProduction:[
    "openApp(mySolarEdge)", 
    "waitQuery(T:Lifetime,20000)", 
    "newQuery(R:.pv_power_now)", 
    "getText",
    "sendKey(Back)", 
    "sendKey(Back),
    "getFuncRetval(getText)"]});
{retval: true}
>> var info = device.sendAai({action:"getProduction"})
>> info
{retval: [{retval: '5.89 kW
Solar Power Now'}]}

要获取产量,请使用info.getProduction[3].retval。SolarEdge将根据输出量返回W或kW,可以编写一个简单的函数来获取太阳能产量:

function getProduction() {
    var p = device.sendAai({action:"getProduction"});
    if (p != null) {
        var prodText = p.retval[3].retval;
        var matches = /^(\d+(\.?\d+)?) (W|kW)/.exec(prodText);
        if (matches != null) {
            return matches[3] == 'kW' ? Math.round(matches[1]*1000) : matches[1];
        }
    }
    return -1;
}

>> getProduction()
5890


If, assert版本 17

"if" 和 "assert" 命令可以使用条件来确定 true/false(请参阅操作部分中的 "函数内部表达式"),对于 "if" 命令,如果条件为 true,则调用 "then",否则调用 "else",对于 "assert",如果条件为 true,则返回 true。以下是几个规则:

  • 对于这两个命令,都需要 VFC 来确定 true/false。
  • 对于 "if",如果条件为 true,则调用 "then",如果条件为 false,则调用 "else",可以在 "then" 或 "else" 中指定 null 以不执行任何操作。如果调用 "null",返回值是 {retval: true}。
  • 对于 "assert",如果条件为 true,则返回 {retval: true},如果条件为 false,则会导致错误,它将传播断言错误以发送到 sendAai(),随后使用 RingoJS assert.fail() 生成 AssertionError。如果正在运行测试,AssertionError 将导致测试失败。
  • “if”可以包含“return”来退出函数。

用法:

if(<VFC>, <then>, <else>)

assert(<VFC>[, <message>])

返回:

返回值: "then" 或 "else" 的返回值。 Assert 将返回 true,或者将在 RingoJS assert.fail() 中引发 AssertionError。

示例:

在 Tesla 应用中,尝试找出电池剩余量的百分比,它可以显示在范围内(后缀为 "mi")或百分比(后缀为 "%"),点击它可以更改模式。 这是获取电池剩余量的函数:

>> device.sendAai({action:"function(getPctg)", getPctg:"if(exists(+T:/^\\d+ mi/), nsClick);getText(+T:/^\\d+%/)"})
{retval: true}
>> device.sendAai({action:"getPctg"})
{retval: '79%'}

创建一个返回需要充电的低电量百分比的函数:

>> device.sendAai({action:"function(getLowPctg)", getLowPctg:"echo(20)"})
{retval: true}

创建一个函数,返回是否需要充电的 true 或 false:

>> device.sendAai({action:"function(needCharge)", needCharge:"if(getPctg() <= getLowPctg(), return(true), return(false))"})
{retval: true}
>> device.sendAai({action:"needCharge"})
{retval: false}

或者,使用返回:

>> device.sendAai({action:"function(needCharge)", needCharge:"return(getPctg() <= getLowPctg())"})
{retval: true}
>> device.sendAai({action:"needCharge"})
{retval: false}
>> device.sendAai({action:"function(getLowPctg)", getLowPctg:"echo(100)"})
{retval: true}
>> device.sendAai({action:"needCharge"})
{retval: true}

此外,将“return”替换为“echo”也会达到相同的结果。您还可以使用对象符号:

您还可以使用该对象来存储值。 例如:

device.sendAai({action:"function(getNum)", getNum:"return({min:10, max:90})"})
device.sendAai({action:"if(getPctg() < getNum().min, chargeNow())"})
device.sendAai({action:"if(getPctg() > getNum().max, stopNow())"})

假设是计算器应用程序,.display 资源将显示结果,我们可以创建一个用于单个数字相加的函数如下:

>> device.sendAai({action:"function(add)", add:"click(T:%1);click(T:+);click(T:%2);click(T:=);getText(+R:.display)"})
{retval: true}
>> device.sendAai({action:"add(8,9)"})
{retval: '17'}

针对 echo 中的算术表达式的函数计算器应用程序:

>> device.sendAai({action:"function(equal)", equal:"if(add(%1,%2) == echo(%1+%2), return(true), return(false))"})
{retval: true}
>> device.sendAai({action:"equal(8,9)"})
{retval: true}

使用 "assert":

>> device.sendAai({action:"assert(calc(8,9))"})
{retval: true}
>> device.sendAai({action:"assert(add(8,9)==10)"})
[AssertionError 'Assertion error on "add(8,9)==10"']
>> device.sendAai({action:"assert(echo(true))"})
{retval: true}
>> device.sendAai({action:"assert(echo(false))"})
[AssertionError 'Assertion error on "echo(false)"']
>> device.sendAai({action:"assert(echo(false), 'This should not happen')"})
[AssertionError 'This should not happen']


refresh (M)

无障碍功能节点信息被缓存,如果屏幕已更新,缓存信息可能不准确,使用"refresh"强制重新读取节点信息。

用法:

refresh [(<查询>)]

返回值:

返回计数和true(始终返回true)

示例:

>> device.sendAai({query:"T:Overall Cpu Usage&&OX:1", actions:["getText", "sleep(2000)", "refresh", "getText"]})
{count: 4, list: [{retval: '29'},{retval: true},{retval: true},{retval: '78'}]}


repeat (M)

此命令对于重复操作非常有用,它共享操作输入(JSON 或单个操作)和与“forEach”类似的返回值,它将在给定的运行次数中重复执行一个/多个操作。 最大重复次数为 20。使用“action:repeat:maxCount”上的“setConfig”来增加最大重复次数。

用法:

repeat (<重复次数>, <操作名称>)

repeat (<重复次数>, <JSON名称>)

返回值:

  • 对于参数中的单个操作,"retval"数组将用于存储每次运行的输出。
  • 对于多个操作,JSON名称将包含数组的数组,内部数组将是一次运行的执行结果,外部数组将包含所有运行。

示例:

该命令在计算器中点击5次"1":

>> device.sendAai({action:"repeat(5,click(T:1))"})
{retval: [{retval: true},{retval: true},{retval: true},{retval: true},{retval: true}]}

设置电动汽车的充电电流:

减少1安培:

>> device.sendAai({action:"click(T:/[0-9]+ A/&&OX:-1)"})
{retval: true}

减少5安培:

>> device.sendAai({action:"repeat(5, click(T:/[0-9]+ A/&&OX:-1))"})
{retval: [{retval: true},{retval: true},{retval: true},{retval: true},{retval: true}]}

减少并验证:

>> device.sendAai({query:"T:/[0-9]+ A/", action:"getText;repeat(5,click(OX:-1));refresh;getText"})
{count: 4, list: [{retval: '27 A'},{retval: [{retval: true},{retval: true},{retval: true},{retval: true},{retval: true}]},{retval: true},{retval: '22 A'}]}


return 版本 17

“return”是一个重要的函数,具有两个作用:

  • 停止当前函数的运行。
  • 返回指定的值

return 只能在函数内部有效,在函数外部,其行为类似于“echo”。 默认情况下,该函数将返回函数中最后一个命令的返回值,“return”和“getFuncRetval”可以改变这一点。 return 接受一个参数,如果没有指定参数,则返回 true。 允许“返回(空)”。 在 17 之前的版本中,函数执行将返回函数中运行的每个命令的返回值。 要恢复相同的功能,请使用“return(getFuncRetval())”。

用法:

return [<query>] // 如果未指定参数,则返回 true。

示例:

>> device.sendAai({action:"if(exists(+BP:editable), return(getText), return(null))"})
{retval: 'Hello'}

return 对主要操作没有影响,只对函数内部有效:

>> device.sendAai({action:"echo(1);echo(2);return;echo(3)"})
{count: 4, list: [{retval: 1},{retval: 2},{retval: true},{retval: 3}]}

演示它可以在函数执行中间停止:

>> device.sendAai({action:"function(testecho)", testecho:"echo(1);echo(2);return(getFuncRetval());echo(3);return(getFuncRetval())"})
{retval: true} 
>> device.sendAai({action:"testecho"})
{retval: [{retval: 1},{retval: 2}]}

示例:

getFuncRetval



scrollIntoView → (M)

接受查询字符串和滚动方向,将尝试根据提供的方向向上/向下滚动以匹配查询,当找到一个或多个节点时,将返回结果,如果找不到匹配项,则将返回空并附带错误。滚动将使用页面向上/向下,最多 30 页(由 setConfig(navi:maxPageScroll, ) 控制),滚动可能需要一些时间,此命令将自动延长 "sendAai" 中的超时直到完成。为了使脚本能够在任何分辨率和大小下运行,使用 scrollIntoView 比使用 Page Up/Down 更好。此操作还将使匹配节点中的第一个节点完全可见,并修改 ML,ML 可能包含多个节点。

用法:

scrollIntoView(<查询>) // 默认向下滚动

scrollIntoView(<查询>, <方向>)

参数:

<方向> 可以是以下之一,如果未指定,则默认为 "down"。

"down": 从当前位置开始向下滚动搜索。

"up": 从当前位置开始向上滚动搜索。

"top": 快速滚动到页面顶部,然后向下滚动搜索。

"bottom": 快速滚动到页面底部,然后向上滚动搜索。

返回值:

如果无法找到查询结果,则返回null,否则返回true,ML将设置为第一个匹配节点的ID。

示例:

将当前应用程序滚动直到找到 "John",然后点击找到的节点。

>> device.sendAai({actions:["scrollIntoView(T:John)", "click"]})
{count: 2, list: [{retval: true},{retval: true}]}


sendKey

sendKey 接受按键代码或元状态并将按键发送到屏幕(不是节点),具有焦点的 UI 元素将接收按键代码和元,例如文本字段或屏幕键盘。 sendKey 还提供快捷方式、一些常用键码的文本字符串。 并非所有按键代码都会生成字符,一些特殊的按键代码会带入新窗口,例如返回、应用程序切换或转到主屏幕。 按键代码和元状态显示在 Android KeyEvent 类中。 “sendKey”发送到屏幕,而不是特定节点,因此不涉及机器学习。 快捷键映射或键码/元在 ATS 设置中不起作用。

有几个快捷键用作“全局操作键”,这些键提供不同的功能集,它将适用于所有环境。

用法:

sendKey(<键码>)

sendKey(<键码>, <元状态>)

sendKey(<快捷方式>)

参数:

键码和元状态:为整数,“快捷方式”为单个字符串。 关于后退键,如果在Sigma输入法输入模式下,单个“后退”键将向窗口发送2个“后退”键,第一个后退键关闭输入法,第二个后退键关闭窗口。 使用“setConfig(sigmaInput:backKeyAutoDismiss, false)”来更改行为。 两个后退键不适用于全局操作键。

以下是映射到键码(元为零)的快捷键(其中一些可能无法使用,具体取决于 Android 版本),这些快捷键是映射到键码。 它不适用于 ATS:

appswitch 切换应用程序

back 发送返回键

backspace 发送退格键

delete 发送删除键

enter 发送回车键

home 发送主页键

menu 发送菜单键

search 发送搜索键

以下是全局操作键,两个表中都存在“home”和“back”,如果启用了 ATS,将使用全局操作:

allApps 显示所有应用程序的应用程序抽屉

back 发回密钥

home 发送主页键

lockScreen 锁定设备屏幕

notifications 显示通知屏幕

powerDialog 显示电源对话框屏幕(关闭电源、重新启动、紧急呼叫)

quickSettings 显示快速设置屏幕

recentApps 显示最近使用的应用程序屏幕

takeScreenshot 截图并存储在照片库中

返回值:

返回操作的状态,为true/false。

示例:

tcConst包含键码和元状态信息。例如,以下3个示例将得到相同的结果:

sendKey('back')
sendKey(4)
sendKey(tcConst.keyCodes.BACK)

这将输入"A"键

>> device.sendAai({preAction:aaix("sendKey", tcConst.keyCodes.A, tcConst.keyCodes.META_SHIFT_ON)})
{retval: true}


setChecked (M)

该命令适用于具有“可检查”节点的节点。 通常,切换控件是可检查的、复选框或单选按钮,类的示例可以是“.Switch”或“.Checkbox”。 对于单选按钮,setChecked(false) 将不起作用,因为 FindNode 不知道单击哪个单选按钮来禁用现有的单选按钮。

用法:

setChecked(true|false)

setChecked([查询], true|false)

参数:

"setChecked(true|false)",由于没有权限设置选中值,如果需要更改值,它将单击节点以切换值。它接受多个节点,将忽略非可选中的节点。

返回值:

setChecked 返回“changedCount”,即已更改的可检查节点数。 从版本 16 开始,为了与其他命令保持一致,它将返回 true/false 来指示至少 1 个节点已更改(未找到)。



setProgress (O)

用于设置滑块的值,可以通过"getNodes('RI’)"获取最小值、最大值、类型和当前值。

用法:

setProgress(<数字>)

setProgress(<查询>,<数字>)

参数:

<数字>:整数或小数,根据"type"、"min"和"max"确定要设置的值

返回值:

true/false:如果成功设置了值,则返回true;如果设置为相同的值或超出范围,则返回false。如果节点不是进度节点,则返回null。

参见:getProgress

示例:

将显示亮度设置为50%:

>> device.sendAai({query:"T:Brightness&&VG&&PQ:'C:.SeekBar'", action:"getProgress"})
{retval: {current: 85983232, max: 267386880, min: 0, type: 0, typeString: 'int'}}
>> var max = device.sendAai({query:"T:Brightness&&VG&&PQ:'C:.SeekBar'", action:"getProgress"}).retval.max
267386880
>> device.sendAai({query:"T:Brightness&&VG&&PQ:'C:.SeekBar'", action:aaix("setProgress", max/2)})
{retval: true}

在声音设置中,将所有4个声音音量设置为50%:

>> device.sendAai({query:"C:.SeekBar"}).ids.forEach(
    x => { 
        var max = device.sendAai({elements:[x], action:"getProgress"}).retval.max;
        device.sendAai({elements:[x], action:`setProgress(${max/2})`});
    }
)


setText (O)

如果节点是文本字段,则将"输入字符串"的值以编程方式输入到输入文本字段中,不涉及Sigma输入法。

对于Android 11或更高版本,如果消息的后缀为"\n"或"\r\n":

  • 通常,"搜索"字段不包含执行搜索的按钮。在输入文本后,将发送"搜索"操作。
  • 对于具有"Enter作为发送"功能的应用程序,将输入并发送消息,无需点击发送按钮。

用法:

setText([查询], <输入字符串>)

setText([查询], +<输入字符串>)

参数:

输入字符串:默认情况下,在输入字符串之前,文本字段将被清除,如果第一个字母为"+",则将其添加到现有字符串中。对于多行文本字段,请使用"\n"跳到下一行。

返回值:

true或false,表示操作是否成功。

示例:

>> device.sendAai({query:"BP:editable", action:"getText"})
{retval: ['text','text','Number','Email','text password','Person Name']}
>> device.sendAai({query:"BP:editable", action:"setText(T:Number, '100');setText(T:text password, secretWord)"})
{count: 2, list: [{retval: true},{retval: true}]}
>> device.sendAai({query:"BP:editable", action:"getText"})
{retval: ['text','text','100','Email','••••••••••','Person Name']}
>> device.sendAai({query:"BP:editable", action:"getHintText"})
{retval: ['text','text','Number','Email','text password','Person Name']}

>> device.sendAai({query:"TP:textInput&&IX:1", action:"setText('Hello')"})
{retval: true}
>> device.sendAai({query:"TP:textInput&&IX:0", action: "setText('+ World')"})
{retval: true}
>> device.sendAai({query:"TP:textInput", action: "setText('Hello\n')"})
{retval: true}

通过String.fromCodePoint输入表情符号:

>> device.sendAai({query:"TP:textInput",action:aaix("setText", String.fromCodePoint(0x1f60a))})
{retval: true}

或与普通消息混合使用:

>> device.sendAai({query:"TP:textInput", action:aaix("setText", 
"hello" + String.fromCodePoint(0x1f643, 0x1f644) + "world")})
{retval: true}


showOnScreen (O)

这将确保第一个节点完全显示在屏幕上,如果关联的UI元素部分显示,则会滚动直到UI元素完全可见在屏幕上。如果节点太大而无法适应可滚动区域,则会生成错误。

用法:

showOnScreen [(<query>)]

返回值:

true表示屏幕已滚动,false表示节点已完全显示在屏幕上,不需要进行任何操作。如果节点太大而无法适应可滚动区域,则返回null。



sleep

此命令将暂停执行指定的毫秒数,用于在执行操作之前进行时间相关的等待。

用法:

sleep(<time>)

参数:

time:指定等待的时间,单位为毫秒。

返回值:

返回true



until

until会根据指定的选项等待特定的条件,如果条件满足,它将返回true,如果超时,则返回带有错误的null。目前支持两种类型:

until([query], gone, <timeout>)

"gone"将等待指定的节点(ML或OQ中的第一个元素)消失。

示例:

以下示例将等待重置按钮消失,支持两种格式:

>> device.sendAai({query:"R:.btn_img_reset_time", action:"until(gone, 20000)"})
{retval: true}
>> device.sendAai({action:"until(R:.btn_img_reset_time, gone, 20000)"})
{retval: true}

until([query], changed, T|D, <timeout>)

将检查直到超时或文本(T)或描述(D)内容发生变化。

示例:

以下示例检查纳斯达克指数是否发生变化:

>> device.sendAai({query:"T:Nasdaq*&&OY:1", actions:["getDescription", "until(changed,D,30000)","getDescription"]})
{count: 3, list: [{retval: '10,989.25'},{retval: true},{retval: '10,988.50'}]}
>> device.sendAai({actions:["until(T:Nasdaq*&&OY:1, changed, D, 30000)","getDescription"]})
{count: 2, list: [{retval: true},{retval: '10,988.50'}]}

其他命令



openApp/restartApp/closeApp 版本 11

指定的应用程序名称可以是包名或应用程序在启动器上显示的名称。如果存在多个匹配的应用程序名称,将使用第一个匹配的应用程序。不允许部分匹配,匹配不区分大小写。不带参数的 "closeApp" 将关闭当前应用程序。"openApp" 和 "restartApp" 可以接受可选的查询和超时参数,在启动应用程序后,它将进入 "waitQuery" 状态,等待查询匹配,然后将控制权返回给调用者。超时参数以毫秒为单位。

已经进行了工作,以确保 openApp 或 restartApp 在应用程序处于稳定状态时返回控制权(渲染完成,等待用户输入)。在终端中尝试 "restartApp" 和 "openApp",如果打开/重启需要很长时间,请使用关联的查询和超时参数。

"restartApp" = "closeApp" + "openApp"

"restartApp" 在自动化脚本中非常有用,因为它总是以已知状态启动应用程序。

"openApp" 将运行应用程序,如果应用程序已经在内存中,它将将应用程序显示在前台;如果应用程序不在内存中,它将重新启动应用程序。"closeApp" 将关闭应用程序(强制关闭)。

用法:

openApp(<name>[,<query>[,<timeout>]])

restartApp(<name>[,<query>[,<timeout>]])

closeApp(<name>)

closeApp

返回值:

retval:成功返回 true,未找到应用程序、无法启动应用程序或查询不匹配时返回 null 并附带 lastError()。

示例:

打开时间测量(以毫秒为单位)

>> device.sendAai({action:"openApp(Skype)"})
{retval: true, timeFindNode: 3040}
>> device.sendAai({action:"openApp(Skype)"})
{retval: true, timeFindNode: 672}
>> device.sendAai({action:"restartApp(Skype)"})
{retval: true, timeFindNode: 2859}

"mySolarEdge" 打开应用程序始终需要很长时间,使用查询和超时参数:

>> device.sendAai({action:"openApp(mySolarEdge,T:Lifetime,15000)"})
{retval: true, timeFindNode: 13098}


openAndroidSetting

将根据指定的设置名称打开 Android 系统设置窗口,设置名称将转换为大写字母,空格将转换为“_”,如果名称尚未添加“_SETTINGS”后缀,则会添加该后缀:

https://developer.android.com/reference/android/provider/Settings 上提供部分设置,使用 "listAndroidSetting" 列出此命令中可用的设置。此命令将打开设置窗口。如果找不到该设置,则返回 null。

用法:

openAndroidSetting(<设置名称>)

例如:以下命令打开无线设置:

device.sendAai({action:"openAndroidSetting(wireless)"})

以下命令打开应用程序设置:

>> device.sendAai({action:"openAndroidSetting(manage applications)"})
{retval: true}

"MANAGE_APPLICATIONS_SETTINGS"、"manage applications setting "、"Manage Application" 都是相同的设置。



listAndroidSettings

此命令将以数组形式返回设置列表。

用法:

listAndroidSettings

返回值:

设置的有效列表。

示例:

>> device.sendAai({action:"listAndroidSetting"})
{list: ['SETTINGS','ACCESSIBILITY_SETTINGS','ACTION_CONDITION_PROVIDER_SETTINGS', ..., 'ZEN_MODE_PRIORITY_SETTINGS']}
>> device.sendAai({action:"listAndroidSetting"}).list.length
70


setConfig, getConfig, unsetConfig

更改FindNode(或Selector)的值,各种值通过名称来标识,使用“getConfig”获取值,“setConfig”更改值,“unsetConfig”恢复原始值。

getConfig(<name>) → retval:<value>

setConfig(<name>, <value>) → retval: true

unsetConfig(<name>) → retval: true

如果找不到名称,将返回 null。

名称 类型 默认值 描述
text:maxQueryLength 整数 30 替换为"*"的文本和描述的最大长度。
navi:maxPageScroll 整数 30 查找节点时的最大页面滑动次数。
navi:scrollVertSteps 整数 40 页面滑动的步数,较小的值快速但不准确,较大的值慢速但准确。
navi:slowScrollSteps 整数 100 使节点完全可见的滑动步数。
sigmaInput:backKeyAutoDismiss 布尔值 true Sigma输入法,发送额外的返回键以隐藏键盘。
selector:defaultTemplate 字符串 more 默认模板,如果在查询中未指定模板。
selector:allowBqAfterEq 布尔值 false 如果设置为true,将允许在扩展查询后定义的基本查询作为扩展查询的一部分按顺序执行(从左到右)。
selector:allowDelayedSearch 布尔值 true 启用延迟搜索,需要时进行搜索
action:repeat:maxCount 整数 20 增加重复计数的数量
action:enhancedParser 布尔值 true 启用解析器以支持转义字符

示例:

>> device.sendAai({action:"getCount"})
{retval: 58}
>> device.sendAai({action:"setConfig(selector:defaultTemplate, reduced)"})
{retval: true}
>> device.sendAai({action:"getCount"})
{retval: 46}
>> device.sendAai({action:"unsetConfig(selector:defaultTemplate)"})
{retval: true}
>> device.sendAai({action:"getCount"})
{retval: 58}

命令参考

FindNode 命令参考
版本 17
动作名称 可选查询 参数 输入 输出 ML 变化 属性
Get 命令
getBounds - 所有节点 ids:[<id>], bounds:[<bounds>] - -
getBoolProp - 第一个节点 retval:[<prop>] - -
getBoolPropInAncestors - 第一个节点 retval:[<prop>] - -
getChecked - 一个节点
所有节点
retval:<boolean>
retval:[boolean|N/A]
- -
getCount - 所有节点 retval:<count> - -
getDescription - 一个节点
所有节点
retval:<text>
retval:[<text>]
- -
getHintText - 一个节点
所有节点
retval:<text>
retval:[<text>]
- -
getFocus - retval:<ID> - -
getFuncRetval - retval:[] or retval: {} - -
getIds - 所有节点 retval:[<ID>] - -
getNodes [fieldList(S)] 所有节点 retval:[<info>];
count:
- -
getPackageName - retval:<PackageName> - -
getProgress - 第一个节点 retval:<rangeInfo> - -
getQuery [ignoreText(B)]
[IgnoreDesc(B)]
第一个节点 retval:<query String> - -
getText - 一个节点
所有节点
retval:<text>
retval:[<text>]
- -
getUniqQuery [ignoreText(B)]
[IgnoreDesc(B)]
第一个节点 retval:<query String> - -
查询命令
addQuery <query> 所有节点 retval:<count> -
exists <query>[, <timeout>] 所有节点 retval:<boolean> - -
intersectX - 第一个节点 retval:<count> TX
intersectY-第一个节点retval:<count>TY
load/pop - retval:<boolean> -
newQuery<query>[,<timeout>]retval:<count>-
reduceNodes-所有节点 retval:<count>RN
save/push - 所有节点 retval:<boolean> - -
sort<type(S)>所有节点retval:<boolean>ST
viewGroup [level(I)] 第一个节点 retval:<count> VG
waitQuery<query>[, <timeout>] retval:<boolean>--
动作命令
assert<VFC>retval:<boolean>--
click-第一个节点retval:<boolean>--
click2-第一个节点retval:<boolean>--
echo<VFC>retval:<value>--
forEach<action|JSON name>所有节点retval:<name>:[[...]]--
function<name + arg>retval:<boolean>--
funcExists<name>retval:<boolean>--
funcDelete<name>retval:<boolean>--
funcList-retval:[<names>];count:<count> --
funcReset-retval:<boolean>--
if<VFC>,<then>,<else>retval:[<value>]--
log<VFC>retval:[<boolean>]--
longClick-第一个节点retval:<boolean>--
nsClick-第一个节点retval:<boolean>--
repeat<count, action|JSON>retval:<name>:[[...]]--
refresh-所有节点 retval:<boolean>--
return<VFC>retval:<value>--
scrollIntoView<query>[direction(S)]retval:<boolean>-
sendKey<code(I)> [meta(I)]retval:<boolean>--
setChecked<boolean>所有节点 retval:<boolean>--
setProgress<value(F/I)>第一个节点retval:<boolean>--
setText<text(S)>第一个节点retval:<boolean>--
showOnScreen-第一个节点retval:<boolean>--
sleep<duration in ms(I)>retval:<boolean>--
until<option> <arg>retval:<boolean>--
其他命令
version-retval:<version number>--
openApp<name>(S)>retval:<boolean>--
restartApp<name>(S)>retval:<boolean>--
closeApp[<name>(S)>]retval:<boolean>--
openAndroidSetting<setting(S)>retval:<boolean>--
listAndroidSettings-retval:[<settings>]; count:<count>--
getConfig<name(S)>retval:<value>--
setConfig<name(S)> <value(*)>retval:<boolean>--
unsetConfig<name(S)>retval:<boolean>--
(S) - String, (I) - Integer, (B) - Boolean, (F) - Float, (*) - All types
<VFC> - Value/Function/Comparison (see Expressions Inside Functions)
[Optional Arguments] or <name>:[<return array>]
device/devices.sendAai({query:<query>, action:<command>})
device/devices.sendAai({query:<query>, actions:[<command>]})

支持

请发送电子邮件至 support@sigma-rt.com 寻求支持和提供反馈意见。