FindNode

FindNode 是基于核心 Selector 包的一个 Shell 程序,其目的是找到一个或多个预期的 UI 元素(或无障碍功能节点)并提取信息或对其执行操作。简而言之,AAI 用查询替代了坐标。

FindNode 是 AAI 项目(无障碍功能和自动化集成)的一部分,由以下组件组成:

  1. 查询语言:简单的单行语法语言,用于搜索预期的节点。
  2. 每个设备上的 FindNode(和 Selector 核心)。
  3. 一对多同步中的对象模式,向所有设备发送节点(或 UI 对象)的查询,而不是使用坐标。点击 "OK" 可以在不同分辨率的所有设备上运行,而不是点击(100,100)。
  4. UI 探测:用于获取节点信息和构建查询的辅助程序,非常适合学习查询语言。
  5. AAIS:一种在多个设备上执行自动化的简单语言。
  6. 作为现有 REST 和 JS API 的一部分(例如,devices.click("OK"))。
  7. FindNode + JavaScript 结合 "device" 和 "devices" 构造可以创建自动化脚本。

Selector 位于无障碍功能和 UI Automator 之上,每个 UI 元素或 UI 元素容器由节点或节点集合标识,每个节点都具有在当前屏幕上唯一的 ID。Selector 提供了各种方式来搜索一个或多个节点。一旦获取到节点,就可以进行许多有趣的操作,例如获取文本/图像或执行操作,如点击按钮或在文本字段中输入文本,所有这些都不需要使用坐标。节点的坐标在运行时获取。

例如:

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 进行通信:

  • 发送 "params" 对象。
  • 如果 FindNode 在一定时间内未返回,生成超时错误。
  • 对于某些需要比默认超时时间更长的命令,FindNode 将延长时间以避免超时错误。
  • 自动处理返回值(错误时为 null,有值时为非 null,使用 lastError() 获取错误消息)。
  • 对于多个设备 "devices",将创建线程,将相同的命令发送到设备线程。

据推测,您可以通过使用 "device.sendAai()" 和 "devices.sendAai()" 来完成大部分任务,FindNode 是 Appium 的 "handler",另一个 "invoke" handler 可以使用 Java 反射访问 UiDevice、UiObject2、AccessibilityNodeInfo 和 InteractionController 中的方法。

随着版本 9.0 (Update 50) 中引入 AWU Beta,AWU 允许每个设备在有或没有 Total Control 的情况下独立运行 FindNode 脚本。 AWU 也减少了之前的复杂设置,只需授予 AWU Accessibility 和文件管理器权限即可。

限制

FindNode 有几个限制:

  • 仅支持纵向模式,横向模式将在将来引入。
  • 此时不支持识别所有内容,取决于应用程序的设计,可能会错过一些节点。
  • 不支持水平滚动或弹出窗口(将获取较少或较多的节点)。
  • 不支持多线程,每个执行都必须互斥:MDCC(对象模式)、终端(或脚本)、UI 探测 和部分 REST API 使用 FindNode。
  • 不支持 WebView 对象中的 UI 元素,Facebook 和 Youtube 等使用 WebView 的应用程序不适合 FindNode。

查询 + 动作

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

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

>> device.sendAai({query:"TP:basic&&T:Android version&&OY:1", action:"getText"})
{retval: '13'}
  • 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> 文本(字符串)
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可以接受整数和字符串。字符串更加强大。

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



扩展查询 (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>,C:<class name>,R:<resource ID>

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



T:<text>,D:<text>,TD:<text>

两个最重要的键,带有信息的节点通常包含文本或描述,这比OCR更准确和更快速。"until"命令可以监视文本或描述是否发生了变化。经常使用带有特殊字符的T:<text>。"TD"匹配文本或描述,如果其中任何一个匹配成功,则匹配成功。

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

点击“确定”按钮。



边界范围匹配(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:如果节点已选择。
  • scrollable:如果节点是可滚动节点(容器),则该节点可以水平或垂直滚动。RecyclerView、ScrollView和ListView等类是可滚动的。

允许多个布尔属性,用逗号分隔,选择是否将其包含在引号中是可选的。

用法:

{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

对于只需要一个节点的动作(例如"click"),将选择ML中的第一个节点(ON、IX、OX和OY可以改变这一点)。动作中有许多命令,在新版本中将添加改进,可能会引入不兼容性,请使用action:"version"来确保版本支持所需的命令。某些命令需要附加参数(参数将被括在括号中)。目前支持以下类型的参数:

  • "String"或‘String’:用单引号或双引号括起来的字符串。
  • <no quote>:如果字符串不包含逗号或尾随空格,则不需要引号。
  • Boolean:true/false。
  • Integer:带或不带小数点的数字。
  • Double:带有小数点的数字。

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



单个动作

在单个动作中,动作中只有一个命令。在这种情况下,返回的结果在"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'}]}

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进行搜索。



动作参数中的特殊字符

操作参数中有几个对 FindNode 解析器有意义的字符:

  • 单引号或双引号用空格和逗号将文本括起来。 如果使用单引号或双引号,即使文本被引号包围,解析器也无法处理。 诸如“我是约翰”、“约翰的书”之类的文本将在解析器中生成错误。
  • 逗号:逗号用于分隔多个参数。 带逗号的文本需要用引号引起来。
  • 分号:分号用于分隔单个字符串中的多个操作。

从版本 16 开始,这 4 个字符可以通过“\\”转义:

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中的节点。如果操作需要一个节点(例如,click操作),将使用ML中的第一个节点;如果操作支持多个节点(例如,getNodes或getText操作),将使用整个ML。

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, list: [{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'}]}

看起来"*"和"+"是类似的,考虑下面的例子,"*"匹配来自"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','.','±','=']},{count: 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

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 [(<查询>)]

返回:

字符串属性数组。

>> 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"})
{boolProp: [['clickable','3622e'],['scrollable','105cd']], count: 2}


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


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,否则返回null。

示例:

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

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


获取ID(getIds)

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

用法:

getids [(<query>)]

返回:

计数和ID数组。

>> device.sendAai({})
{count: 26, ids: ['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, ids: ['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: 10, list: [{id: 'a773', resourceId: '.button_seven', text: '7'},{id: 'ab34', resourceId: '.button_eight', text: '8'},{id: 'aef5', resourceId: '.button_nine', text: '9'},{id: 'b677', resourceId: '.button_four', text: '4'},{id: 'ba38', resourceId: '.button_five', text: '5'},{id: 'bdf9', resourceId: '.button_six', text: '6'},{id: 'c57b', resourceId: '.button_one', text: '1'},{id: 'c93c', resourceId: '.button_two', text: '2'},{id: 'ccfd', resourceId: '.button_three', text: '3'},{id: 'd47f', resourceId: '.button_zero', text: '0'}]}
>> device.sendAai({query:"C:.SeekBar", action:"getNodes(RI)"})
{count: 1, list: [{id: 'b6f0', rangeInfo: {current: 0, max: 100, min: 0, type: 0, typeString: 'int'}}]}
>> device.sendAai({query:"C:.SeekBar", action:"getNodes(BP)"})
{count: 1, list: [{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)

返回当前正在运行的包名。

用法:

getPackageName

返回:

包名在"retval"中。

>> device.sendAai({action:"getPackageName"})
{retval: 'com.google.android.gm'}


获取进度(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"}).query)
)
T:Controls&&OX:-1
T:Controls
T:Controls&&OX:1

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

查询命令

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



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",如果堆栈中没有可弹出的内容,将生成错误。



intersectX/intersectY (O → M)

如果指定了查询字符串,则将使用查询字符串的第一个节点作为交叉节点;否则将使用ML的第一个节点。此命令将接收一个节点并生成更多节点,并存储在ML中。

用法:

intersect?[(<query>)]

返回:

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

如果指定了"post query",则将存储"post query"的输出(参见"addQuery")。如果未指定"post query",则交叉操作的输出将替换ML。

参见:"TX"和"TY"查询的更多解释

示例:

假设是计算器应用程序

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

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

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

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

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

这也可以起作用:

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


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


reduceNodes (M → M)

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

用法:

reduceNodes

返回:

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

另请参阅:查询"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"



newQuery/addQuery/waitQuery/exists

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

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

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

addQuery(<query>)

exists(<query>)

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

返回:

newQuery和addQuery将以"count"形式返回结果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: [{count: 1},{retval: true}]}

动作命令

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



click, nsClick, click2, longClick

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

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

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

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

用法:

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

返回:

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}


forEach (M)

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

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

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

用法:

forEach(<操作名称>)

forEach(<JSON名称>)

返回:

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

示例:

第一行运行了一次"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)"})
{getTicker: [[{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

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

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

用法:

创建新函数:

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

删除已创建的函数:

funcDelete(<函数名称>)

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

funcExists(<函数名称>)

重置 - 删除所有函数

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","="。

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

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

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

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

>> device.sendAai({action:"add(9)"})
{add: [{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"用于在文本字段中输入文本并点击发送按钮:

>> device.sendAai({action:"function(sendText(Hello))", sendText:[
    "newQuery(TP:textInput)", 
    "setText(%1)",
    "click(OX:2)"
]})
{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)"})
{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)"]});
{retval: true}
>> var info = device.sendAai({action:"getProduction"})
{getProduction: [{retval: true},{retval: true},{count: 1},{retval: '5.89 kW
Solar Power Now'},{retval: false},{retval: true}]}

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

function getProduction() {
    var p = device.sendAai({action:"getProduction"});
    if (p != null) {
        var prodText = p.getProduction[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


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


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”发送到屏幕,而不是特定节点,因此不涉及机器学习。 快捷键映射或键码/元在 AWU 设置中不起作用。

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

用法:

sendKey(<键码>)

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

sendKey(<快捷方式>)

参数:

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

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

appswitch 切换应用程序

back 发送返回键

backspace 发送退格键

delete 发送删除键

enter 发送回车键

home 发送主页键

menu 发送菜单键

search 发送搜索键

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

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>) → name: <name>, value:<value>

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

unsetConfig(<name>) → retval: true

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

Name Type Default Description
text:maxQueryLength Integer 30 替换为“*”的文本和描述的最大长度。
navi:maxPageScroll Integer 30 查找节点时的最大页面滚动次数。
navi:scrollVertSteps Integer 40 页面滑动的步数,较小的值快速但不准确,较大的值慢速但准确。
navi:slowScrollSteps Integer 100 使节点完全可见的滑动步数。
sigmaInput:backKeyAutoDismiss Boolean true Sigma输入法,发送额外的返回键以隐藏键盘。
selector:defaultTemplate String more 默认模板,如果在查询中未指定模板。
selector:allowBqAfterEq Boolean false 如果设置为true,将允许在扩展查询后定义的基本查询作为扩展查询的一部分按顺序执行(从左到右)。
selector:allowDelayedSearch Boolean true Enable dalayed search, search when it is needed
action:repeat:maxCount Integer 20 Increase the number of repeat count
action:enhancedParser Boolean true Enable parser to support escape characters

命令参考

FindNode 命令参考
版本 16
动作名称 OQ 参数 输入 输出 ML变化 属性
Get 命令
getBounds - 所有节点 ids:[<id>], bounds:[<bounds>] - -
getBoolProp - 第一个节点 retval:[<prop>] - -
getBoolPropInAncestors - 第一个节点 retval:[<prop>] - -
getChecked - 一个节点
所有节点
retval:<boolean>
retval:[boolean|N/A]
- -
getCount - 所有节点 count:<count> - -
getDescription - 一个节点
所有节点
retval:<text>
retval:[<text>]
- -
getHintText - 一个节点
所有节点
retval:<text>
retval:[<text>]
- -
getFocus - node:<ID> - -
getIds - 所有节点 ids:[<ID>] - -
getNodes [fieldList(S)] 所有节点 list:[<info>] - -
getPackageName - retval:<PackageName> - -
getProgress - 第一个节点 retval:<rangeInfo> - -
getQuery [ignoreText(B)]
[ignoreText(B)]
第一个节点 query:<query String> - -
getText - 一个节点
所有节点
retval:<text>
retval:[<text>]
- -
getUniqQuery [ignoreText(B)]
[IgnoreDesc(B)]
第一个节点 query:<query String> - -
查询命令
addQuery - 所有节点 count:<count> -
exists No <query> All Nodes retval:<boolean> - -
intersectX - 第一个节点 count:<count> TX
intersectY-第一个节点count:<count>TY
load/pop - retval:<boolean> -
newQuery<query>[,duration]count:<count>-
reduceNodes-所有节点 count:<count>RN
save/push - 所有节点 retval:<boolean> - -
sort<type(S)>所有节点retval:<boolean>ST
viewGroup [level(I)] 第一个节点 count:<count> VG
waitQuery<query>[duration(I)] retval:<boolean>--
动作命令
click-第一个节点retval:<boolean>--
click2-第一个节点retval:<boolean>--
forEach<action|JSON名称>所有节点retval:<name>:[[...]]--
function<名称 + 参数>retval:<boolean>--
funcExistsNo<name>Noneretval:<boolean>--
funcDeleteNo<name>Noneretval:<boolean>--
funcResetNo-Noneretval:<boolean>--
longClick-第一个节点retval:<boolean>--
nsClick-第一个节点retval:<boolean>--
repeat<次数, 动作|JSON>retval:<name>:[[...]]--
refresh-所有节点retval:<boolean>--
scrollIntoView[方向(S)]retval:<boolean>-
sendKey<代码(I)> [meta(I)]retval:<boolean>--
setChecked<boolean>所有节点changedCount:<count>--
setProgress<值(F/I)>第一个节点retval:<boolean>--
setText<文本(S)>第一个节点retval:<boolean>--
showOnScreen-第一个节点retval:<boolean>--
sleep<持续时间(毫秒)(I)>retval:<boolean>--
until<选项> <参数>retval:<boolean>--
其他命令
version-retval:<版本号>--
openApp<名称(S)>retval:<boolean>--
restartApp<名称(S)>retval:<boolean>--
closeApp[<名称(S)>]retval:<boolean>--
openAndroidSetting<设置(S)>retval:<boolean>--
listAndroidSettings-list:[<设置列表>]--
getConfig<名称(S)>name: <名称>, value: <值>--
setConfig<名称(S)> <值(*)>retval:<boolean>--
unsetConfig<名称(S)>retval:<boolean>--
(S) - 字符串, (I) - 整数, (B) - 布尔值, (F) - 浮点数, (*) - 所有类型
[可选参数] 或 <名称>:[<返回数组>]
device/devices.sendAai({query:<查询>, action:<命令>})
device/devices.sendAai({query:<查询>, actions:[<命令>]})

支持

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