FindNode 是由 Sigma Resources & Technologies Inc 开发的一款创新型 shell 程序,增强了核心 Selector 软件包。它旨在智能识别 UI 元素(或无障碍节点),从而实现信息提取和交互,而无需依赖基于坐标的命令。这标志着 AAI(无障碍与自动化集成)项目的一项重大进步,旨在通过直观的查询取代坐标,从而彻底改变用户交互方式。
FindNode 的关键组件:
- 查询语言:一种简单而强大的语法,专为精确识别节点而设计。
- 核心技术:FindNode 与每台设备无缝集成,由 Selector 核心驱动。
- 对象模式:增强多设备间的同步性,使查询和操作能够在不同屏幕分辨率下统一进行。
- UI Explorer:学习查询语言的必备工具,提供节点属性的深入解析,并帮助用户构建查询。
- AAIS:一种简单易用的脚本语言,适用于 AAI 生态系统中的多设备自动化。
- 集成:可与现有的 REST 和 JavaScript API 互补,通过 "device" 和 "devices" 结构实现基于脚本的自动化。
FindNode 利用 Selector 运行在 Accessibility 和 UI Automator 框架之上,以独特的方式识别 UI 元素。这种方法无需依赖坐标进行交互,使脚本能够适应不同设备的分辨率和尺寸。每个 UI 元素或 UI 元素的容器都由一个或一组节点标识,每个节点在当前屏幕上都有唯一的 ID。Selector 提供了多种方式来搜索节点。一旦获取到节点,便可执行许多自动化任务,例如获取文本/图片,或执行点击按钮、输入文本等操作,而无需使用坐标。
例如:
JS API: 使用 devices.click("OK")
代替 devices.click(100, 200)
。FindNode 通过查找包含文本 "OK" 的节点,获取坐标并执行点击操作,整个过程在运行时完成。
MDCC: 用户在主设备上点击一个按钮,该按钮的唯一查询标识(例如 "T:OK")将被发送到其他设备,以便搜索对应的节点并执行点击操作。FindNode 旨在提供多种方式定位节点,而不依赖屏幕坐标。它没有翻页(page up/down)功能,而是通过 scrollToView
定位节点,使同一脚本可在不同分辨率和屏幕尺寸的手机上运行。
FindNode 采用 Appium 的 JSON 结构来执行 JSON 命令,其 JSON 格式如下:
{"cmd": "action", "action": "findnode", "params": {…}}
所有 FindNode 命令都在 "params" 对象中执行。
返回值有两种类型,成功执行时:
{"status":0, "value":"{…}"}
或失败时:
{"status":13,"value":"{<error message>}"}
TC 提供了 device.sendAai()
或 devices.sendAai()
来与 FindNode 交互:
- 发送命令。
- 如果 FindNode 在指定时间内未返回结果,则生成超时错误。
- 对于某些执行时间较长的命令,FindNode 会自动延长超时时间,以避免超时错误。
- 自动处理返回值(错误时返回 null,成功时返回非 null,使用
lastError()
获取错误消息)。
一般情况下,您可以使用 device.sendAai()
和 devices.sendAai()
来完成大部分任务,FindNode 利用了 Appium 的 "handler" 机制。
在 9.0 版本(更新 50)及更高版本中引入的 ATS Beta 消除了设备驱动和开发者选项的设置需求。ATS 允许每台设备独立运行 FindNode 脚本,无论是否使用 Total Control。同时,ATS 简化了以往复杂的设置,只需授予 ATS 无障碍权限和文件管理权限即可。
限制
FindNode 依赖 Android 无障碍服务来获取信息:
- 目前仅支持竖屏模式,横屏模式将在未来的更新中引入。
- 不支持水平滚动和弹出窗口,这可能导致获取的节点数量有所增减。
- 不支持多线程操作。在 MDCC(对象模式)、终端(或脚本)、UI Explorer 以及部分使用 FindNode 的 REST API 之间,每次执行必须是互斥的。
- 无法获取某些 WebView 对象内的 UI 元素,或某些应用实现的原生 UI 元素。因此,像 Facebook、YouTube 及部分游戏等应用通常无法完全支持,可能会缺失部分节点。请使用 UI Explorer 检查 FindNode 识别的节点。
查询 (Query) + 操作 (Actions)
使用我们的查询语言在屏幕上查找节点,并对找到的节点执行操作。本手册将介绍查询和操作的使用方法。
例如,在“关于手机”页面获取 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"}
查询 (Query)
FindNode 的主要用途是查询,这是其核心功能(也是 AAI 的一部分)。它允许用户通过多种查询方式在无障碍节点上定位所需的 UI 元素。整个屏幕由许多节点组成,一个节点可以是最小的 UI 元素,也可以是包含多个小节点的容器,其中许多节点是不可见的。屏幕的结构是从单个根节点开始的树形结构(某些旧设备可能有多个根节点)。查询的目标是获取符合条件的节点,并将庞大的节点集合缩小至一个或少数几个目标节点,以便用户获取信息或对这些节点执行操作。
我们开发了一种小型查询语言,它可以在不同设备和脚本间通用,并且足够强大,可以定位大部分节点。其格式为 JSON:
{query:"<key>:<value>&&<key>:<value>&&<key>&&<key>…"}
我们接受 "||" 和 "&&" 作为相同的分隔符,未来可能会进行调整。由于查询是 AND 关系,查询条件必须全部满足才能匹配节点,因此使用 "&&" 更加合理。FindNode 本身不支持 OR 关系,但其底层的 Selector 包提供了该功能,"templates" 就是使用这种构造的。对于某些键,可以不指定值。
查询字符串分为三个部分:模板查询(template)、基本查询(basic query)和扩展查询(extended query)。"模板查询"(TP)用于生成初始节点,"基本查询"(BQ)用于逐个查询节点属性,"扩展查询"(EQ)会利用 TP 和 BQ 的输出,在多个节点间进行查询。
获取主存储的可用空间
可以使用 "query" + "action":
>> 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 Explorer 的 "Optimized" 模式)。 |
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> |
包名(S) |
C:<class name> |
类名(S) |
R:<resource ID> |
资源 ID(S) |
D:<text> |
内容描述(S) |
T:<text> |
文本(S) |
T0:<text> |
无布尔属性的文本节点(T:<text>&&BP:none 为 true) |
T1:<text> |
无可编辑属性的文本节点(T:<text>&&BP:!editable) |
IT:<number> |
文本输入类型(I) |
CC:<number> |
子节点数量(I) |
ID:<hex string> |
节点 ID(S) |
BI:[x, y] |
返回包含指定 (x, y) 坐标的节点 |
BI:[x1, y1, x2, y2] |
返回被矩形包围的节点,x 或 y 为 -1 时将被忽略 |
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)中,每个节点通过节点ID标识。许多操作会作用于ML中的节点,同时一些操作可以修改它。ML可以通过“save”(保存)和“load”(加载)进行存储和恢复,而“push”(推入)和“pop”(弹出)提供了额外的方式来存储和检索已保存的ML状态。“filter”(过滤)命令允许根据特定条件限制ML,移除不符合给定标准的节点。要获取ML中的节点ID,可以使用操作:"getIds"。此外,ML条目可以动态生成——TP、TX、TY 和 VG 可以产生多个节点,根据需要扩展ML。另一种填充ML的方法是使用“elements”数组或文本字符串。
过去,在执行操作前总是会先进行一次搜索,如果没有提供“query”(查询),则使用默认模板来构建ML。然而,对于不涉及节点的操作(如“openApp”或“function”),这种方式效率较低。从版本16开始,引入了延迟搜索机制,即仅在实际需要节点时才执行搜索,从而提高了效率。
示例:
以前,即使未指定查询,默认情况下也会基于默认模板 "TP:more" 进行搜索,"getIds" 将输出 ML 中的节点 ID。
>> device.sendAai({action:"getIds"})
{count: 11, retval: ['14801','1714c',...]}
>> device.sendAai({query:"TP:more", action:"getIds"})
{count: 11, retval: ['14801','1714c',...]}
版本 16 的 "延迟搜索" 机制,搜索将在调用 "getIds" 时执行:
>> device.sendAai({action:"getIds"})
{count: 11, retval: ['14801','1714c',...]}
由于 openApp 操作与 ML 无关,因此不会执行搜索:
>> device.sendAai({action:"openApp(Skype)"})
{retval: true}
以下示例中,会执行两次搜索,一次基于默认模板 "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 必须是屏幕上存在的有效标识符,否则会产生错误。
当指定 "elements" 时,所有匹配的节点都会存储到 ML 中,并跳过 "query" 操作。此功能适用于缓存节点 ID 以供后续使用。同一个节点 ID 可以多次出现在 "elements" 列表中,而不会引发问题。
示例:
device.sendAai({elements:["1234a", "5678b", "3a4a5b"], action:"getNodes"})
device.sendAai({elements:"1234a;5678b;3a4a5b"], action:"getNodes"})
此查询获取数字按钮,构建 "elements" 数组,并使用 "forEach" 依次点击所有按钮并打印结果。
>> ids = device.sendAai({query:"T:/^[1-9]$/"}).retval
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:<node ID>"。
device.sendAai({query:"ID:1234a", action:"getNodes(all)"})
测试
通过查询查找节点并不困难,您需要使用 "UI Explorer" 和终端:
- 节点 ID 很重要,它通常是应用程序内唯一的。
- 终端:输入 device.sendAai({query:"…"}) 以检查是否匹配节点。有许多搜索选项,可以使用多种方式找到节点。
- UI Explorer:
- 可以使用查询助手构建查询。
- 尝试不同的查询以查看匹配的节点。
- 支持无限次撤销/重做,以便比较不同的查询。
- 查看节点内的属性。
- UI Explorer 中的 "Code" 采用尽力而为的方式定位节点,详情请参考操作:"getUniqQuery"。
- 终端:如果不确定,可使用 UI Explorer 查找节点 ID,并使用 device.sendAai({elements:["<ID>","<ID>"…], action:"getNodes"}) 获取更多信息。
- 先在一个设备上使用 "device.sendAai",然后切换到多个设备使用 "devices.sendAai"。您可以使用 Device.searchObject() 搜索所需的设备(例如所有设备、设备组中的设备、特定设备)。
模板详细说明
模板(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:default" 将返回默认模板的节点。
TP:all, more, basic, reduced
这些模板会遍历整个屏幕上的节点:
- all 返回所有节点,包括根节点。
- more 在 "all" 选项的基础上去除布局节点。如果查询中未定义 TP,则此选项为默认值。
- basic 在 "more" 选项的基础上,仅保留子节点数为零的节点。
- reduced 在 "more" 选项的基础上,使用 "reduceNodes" 分析屏幕上的节点并返回关键节点。
UI Explorer 提供 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]]
此模板将返回具有非空内容的 anyText(任意文本)或 anyDescription(任意描述)。可以选择指定文本的最小和最大长度(包含边界)。如果未指定最大长度,则不会限制最大长度。最小值或最大值不能为零。
示例:
>> 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:<…>" 类似的参数。
示例:
计算器从上到下排列,因此7、8、9排在最前面:
>> 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[,<hint text>]
此模板返回所有 "可编辑" 的节点,可选地返回提示文本(需要 Android 8.0 或更高版本)。如果未找到任何节点,则会产生错误。或者,也可以使用 "BP:editable" 来获得相同的结果。
示例:
获取所有文本字段:
>> device.sendAai({query:"TP:textInput"})
{count: 6, retval: ['e135','e8b7','f3fa','fb7c','106bf','10e41']}
>> device.sendAai({query:"BP:editable"})
{count: 6, retval: ['e135','e8b7','f3fa','fb7c','106bf','10e41']}
// 显示提示文本作为文本值,使用 "getHintText" 获取提示文本
>> 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']}
基于文本字段的提示:
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>,<line number>
目前,此功能仅适用于具有可滚动容器的应用程序,否则 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, retval: ['1902b0','191936','192fbc','1cf0bc','19373e','194a03']}
要返回,请点击左箭头,它是第一个项目“IX:0”:
>> device.sendAai({query:"TP:line,top,1", action:"click(IX:0)"})
{retval: true}
>> device.sendAai({query:"TP:line,bottom,-1"})
{count: 5, retval: ['2be5b8','2c0b42','2c1a46','2c30cc','2c3fd0']}
>> device.sendAai({query:"TP:line,bottom,-1", action:"setText(BP:editable, Hello)"})
{retval: true}
TP:scrollable[,<position>]
此模板返回屏幕上可滚动区域的项目(叶子节点),由 "position" 标识。"position" 从 0 开始编号,从上到下排列。结合资源 ID 在可滚动节点中使用可获得更准确的结果。默认位置为 0。如果屏幕上找不到可滚动节点,将引发错误。"TP:scrollable" 返回单个可滚动节点的叶子节点,而 "BP:scrollable" 会匹配屏幕上的所有可滚动节点。
对于可滚动区域的每个项目(行或列),其中可能包含多个值,建议结合资源 ID 以返回特定值。例如:query:"TP:scrollable&&R:.tv_name"。
示例:
获取当前屏幕上的所有可滚动节点:
>> device.sendAai({query:"TP:scrollable"})
{count: 5, retval: ['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},{retval: 6},{count: 6, retval: ['John James','Sign in quickly and safely','Connections','Connected devices','Galaxy AI','Modes and Routines']}]}
基本查询详细说明
基本查询(BQ)基于 TP 生成的匹配列表(ML),然后逐个节点进行匹配,创建一个更短的 ML,并丢弃不匹配的节点。BQ 从无障碍(Accessibility)提取数据,包含单节点信息和操作。通过扩展查询功能(特殊字符)和添加快捷方式,使查询更强大和简洁。
查询中的特殊字符
对于数字(如 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:<包名>
在屏幕上,可能会有多个应用程序同时运行,默认情况下,如果查询中省略 "P",则使用当前活动应用(即占据最大屏幕区域的应用)。如果需要获取其他应用(如 systemui)中的信息,可以使用 UI Explorer 选择应用,或者使用 "getAllPackageNames" 命令查找所有正在运行的应用包名。
示例:
要从系统状态栏获取当前电池百分比,可以使用以下方法之一。由于 OQ 不支持 “P”,需要使用 “newQuery” 命令:
>> device.sendAai({query:"P:com.android.systemui&&R:.clock", action:"getText"})
{retval:'3:22'}
>> device.sendAai({query:"P:com.android.systemui", action:"getText(R:.clock)"})
{retval:'3:22'}
>> device.sendAai({action:"newQuery(P:com.android.systemui); getText(R:.clock)"})
{count:2,list:[{retval:29},{retval:'3:22'}]}
要点击导航栏中的主页按钮:
>> device.sendAai({query:"P:com.android.systemui", action:"click(R:.home)"})
{retval: true}
C:<类名>, R:<资源 ID>
所有节点都包含包名(package name)和类名(class name),并可能包含资源 ID(resource ID)。由于匹配是在当前运行的应用程序上执行的,因此 "P" 默认指向当前应用,其他 "P" 值无效,因此不需要指定 "P"。
T:<文本>, D:<文本>, TD:<文本>, T0:<文本>, T1:<文本>(T0 和 T1 适用于 API 18)
两个最重要的键,包含信息的节点通常携带文本或描述,这比 OCR 更准确且更快。"until" 命令可用于监控文本或描述是否发生了变化。T:<text> 常用于包含特殊字符的匹配。"TD" 用于匹配文本或描述,只要其中之一匹配即认为成功。“T0” 相当于 “T&&BP:none”,“T1” 相当于 “T&&BP:!editable”。T1 用于避免在文本字段中查找文本,而 T0 用于查找无操作的标签(其祖先节点可能具有操作)。
示例:
device.sendAai({query:"T:OK", action:"click"})
点击 "OK" 按钮。
device.sendAai({query:"T1:Name&&OX:1", action:"getText"})
此操作不会找到带有 "Name" 的文本字段。
Bound In(BI):
BI 使用屏幕坐标或边界(bounds)来返回符合以下条件的节点。主要用于屏幕操作,如 UI Explorer 或调试目的。FindNode 的对象模式尝试在不同设备上查找节点,不依赖分辨率和屏幕尺寸,而 "BI" 是基于坐标的,因此可能无法适用于所有设备。
- [x, y]:返回包含点 (x, y) 的节点。
- [x, -1] 或 [-1, y](其中一个值为 -1):如果 x = -1,则忽略 x,仅返回包含 y 的节点;如果 y = -1,则仅匹配 x。相当于在 x 或 y 位置画一条水平/垂直线,返回所有触及该线的节点。
- [x1, y1, x2, y2]:匹配边界在矩形 (x1, y1) - (x2, y2) 内的节点。
示例:
>> device.sendAai({query:"BI:[-1,645]&&IX:-1", action:"click"})
{retval:true}
>> device.sendAai({query:"BI:[0, 0, 1000, 1000]", action:"getBounds"})
{bounds:[[0,113,196,309],[252,164,529,257],[70,572,702,695],[702,572,737,576],[70,765,480,888],[480,765,515,769],[515,765,925,888],[925,765,960,769]],count:8,ids:["1b21e","1ae5d","17d90","18151","18c94","19055","19416","197d7"]}
CC:<子节点数量>, IT:<输入类型>
这些是节点中的属性,较少使用。CC 表示子节点的数量,IT(输入类型)通常用于数字输入字段。
BP:<属性>
布尔(Boolean)属性在节点中非常重要,这些属性的值为 true 或 false。允许使用多个值,"!" 置于属性名称前表示 false。以下是可用的属性:
- checkable:如果节点包含复选框(checkbox)或开关(on/off switch),通常也具有 "clickable" 属性。
- checked:如果节点包含复选框/开关,true 表示已选中/打开,false 表示未选中/关闭。
- clickable:如果节点可点击(通常是按钮或响应点击的 UI 元素,如复选框)。
- editable:如果节点允许输入或修改文本。
- enabled:如果节点处于启用状态,禁用状态的节点不会响应操作,通常显示为灰色。
- focusable:如果节点可以被聚焦。
- focused:如果节点当前被聚焦,通常每个屏幕上只有一个聚焦的节点,选择此属性可能返回 null 或 1 个节点。
- longClickable:如果节点支持长按操作。
- progressible:如果节点是 "进度条" 类型,如滑动条(slider)。
- selected:如果节点已被选中。
- scrollable:如果节点是可滚动的(例如容器),该节点可以水平或垂直滚动。例如 RecyclerView、ScrollView 和 ListView 都是可滚动的。
- none:没有任何布尔属性返回 true。(API 18)
- !none:任意布尔属性返回 true。(API 18)
多个布尔属性可以一起使用,使用逗号分隔,是否包含在引号内是可选的。"none" 和 "!none" 不能与其他属性组合,否则会产生错误。
用法:
{query:"BP:<prop name>, !<prop name>, ..."}
示例:
>> device.sendAai({query:"TP:textInput&&BP:clickable"})
{count: 1, retval: ['27ebc0']}
>> device.sendAai({query:"TP:textInput&&BP:'clickable,enabled'"})
{count: 1, retval: ['27ebc0']}
>> device.sendAai({query:"TP:textInput&&BP:clickable,enabled"})
{count: 1, retval: ['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"(!),可以使用 "!" 查找 false 值,而不仅仅是 true 值。
假设循环遍历此项以获取值,直到没有更多值(箭头显示为灰色):
>> 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()
未找到匹配项
>> device.sendAai({query:"T:Day&&TX", action:"getText"})
{count: 5, retval: ['Day','Week','Month','Year','Billing']}
"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}
基本查询示例
device.sendAai({}) → device.sendAai({query:"TP:more", action:"getIds"})
"getIds" 将输出匹配列表(ML)的ID。
>> device.sendAai({})
{count: 9, retval: ['95b0','b779','bb3a','9d32','a0f3','a4b4','a875','ac36','aff7']}
>> device.sendAai({query:"TP:more", action:"getIds"})
{count: 9, retval: ['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"
扩展查询详细说明
扩展查询(EQ)包含除 BQ 和 TP 之外的查询。EQ 中的键通常涉及多个节点的计算和启发式处理。EQ 按从左到右的顺序执行键入的函数,在执行过程中操作匹配列表(ML),最终希望 ML 仅包含用户想要的节点。ML 本身并无实际意义,最终结果将传递给操作(actions),以执行操作或提取信息。同一个键可以多次应用,例如 "OX:1&&OX:1" 是允许的。
索引(IX) M → O
IX:<number>。根据 ML(匹配列表)中的位置返回一个节点,索引从零开始(IX:0)。IX 也可以使用负值,表示从列表末尾反向计算(例如,-1 表示最后一个节点)。
示例:
>> device.sendAai({})
{count: 9, retval: ['95b0','b779','bb3a','9d32','a0f3','a4b4','a875','ac36','aff7']}
>> device.sendAai({query:"IX:1"})
{count: 1, retval: ['b779']}
>> device.sendAai({query:"IX:-2"})
{count: 1, retval: ['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>。该命令接受可选的级别参数,如果未指定级别,则默认使用 1 级。它接受一个节点(来自查询),然后执行父节点遍历,直到找到“组”为止。级别用于指定何时停止遍历。“组”是指占据屏幕宽度 85% 以上的布局,或类名中包含“ViewGroup”的元素。ML 将包含该组内的所有节点,第一个节点是 ViewGroup/Layout 本身,该节点包含所有子节点。这对于逻辑分组感兴趣的节点非常有用,并且可以使用 "addQuery" 或 BQ 在组内搜索节点。可以使用 "setConfig(selector:viewGroupWidthRatio, <percentage>)" 来修改默认 85% 的比例。
相关操作:"viewGroup"。
示例:
选择正确的当前值(Ampere)进行充电,VG 的第一层无法找到目标节点:
>> device.sendAai({query:"T:Start Charging&&VG"})
{count: 2, retval: ['53b16','53ed7']}
为了获取充电相关的组,使用 level 2。第一个节点是包围所有相关节点的父节点(绿色矩形)。该节点对于 "showOnScreen" 操作很有用,以确保整个组可见。
>> device.sendAai({query:"T:Start Charging&&VG:2"})
{count: 24, retval: ['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'}
获取当前值并减少 1A:
>> 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" 使电流减少 5A:
>> 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, retval: ['bb4b','bf0c','c2cd','e0d5','e496','111a2']}
>> device.sendAai({query:"T:2&&TY"})
{count: 6, retval: ['1d115','1d4d6','a886','b3c9','bf0c','ca4f']}
但如果选择结果作为参考节点,并在整个屏幕上进行滚动,OX 可能只返回一个节点(它自身),而 OY 可能会返回所有节点。
>> device.sendAai({query:"R:.result&&TX"})
{count: 1, retval: ['1d4d6']}
>> device.sendAai({query:"R:.result&&TY"})
{count: 23, retval: ['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(Reduce Nodes):这是 FindNode 的重要功能之一,此功能接受一个节点 ID 列表,旨在减少可见节点的数量。"TL"、"BL"、"OX/OY" 和 "intersect" 都使用了 reduceNodes 机制。需要注意的是,reduceNodes 选择较小的节点,如果较大的节点完全包含较小的节点,则较小的节点会被选中。例如,如果较大的 ".Button" 节点包围了较小的 ".TextView" 节点,系统会选择 ".TextView" 节点,而在该节点上执行 "click" 操作仍然有效。可在 UI Explorer 的 "Optimize" 模式下查看效果。
示例:
>> 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','-','+','%','=']}
相关操作:sort
基本查询(BQ) M → M
基本查询(BQ):BQ:'<basic query>' 或 "<basic query>"。BQ 是 EQ(扩展查询)中的一条命令。请参考以下查询示例(从 UI Explorer 自定义查询中获取):
示例:
查询:"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" 命令都是基本查询(BQ)命令,第二个 "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'}
值(V) M → M
严格来说,这不应属于查询的一部分,因为它执行的是 "actions"(操作)来设置节点的值。然而,在构建带有相关值的查询时,这样做非常方便。实际上,此键会触发 "set" 操作以设置值。这仅适用于 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, retval: ['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, retval: ['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" 参数可以接受一个字符串命令,或一个包含多个命令的字符串数组。
"query" 参数是可选的。如果未指定,将使用延迟查询机制,这意味着仅当操作需要时才会查询节点。如果明确指定了查询,其输出将存储在匹配列表(ML)中。需要节点的命令或操作将从 ML 中检索它们,无论是通过查询还是可选查询。要检查 ML 的内容,可以使用操作:"getIds"。如果未指定操作,则默认使用操作:"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 参数可以修改此选择。
FindNode 操作在新版本中不断改进,可能会引入不兼容性。要确保所需命令受支持,请使用操作:"version"。
可以使用“function”命令创建自定义操作。有关更多详细信息和示例,请参阅“function”文档。
单一操作
单个操作命令意味着在 "action" 参数中仅指定一个命令。在这种情况下,结果将返回在 "retval" 中,如果发生错误,则返回 null。
device/devices.sendAai({query:"…", action:<action>})
错误处理
如果查询未找到预期的节点,命令将返回 null,并且 lastError() 将提供原因,例如:
>> device.sendAai({action:"getCurrentPackageNameX"});
null
>> lastError()
Invalid action: getCurrentPackageNameX
多重操作
FindNode 允许顺序执行多个操作。许多 FindNode 操作涉及多个操作,这些操作会从左到右依次执行。结果是一个数组,其中存储了每个操作的输出。
如果任何操作失败,sendAai 将返回 null,并且 lastError() 将指示失败操作的零基索引及错误消息。
定义多个操作有两种方式:
1. 使用数组:
device/devices.sendAai({query:"…", actions:[<action 1>, <action 2>, …]})
2. 使用分号分隔的字符串:
device/devices.sendAai({query:"…", actions:"<action 1>;<action 2>, …"})
多个操作的返回格式
对于多个操作,返回值将是一个包含每个操作输出的数组,例如:
>> device.sendAai({action:"version;getCurrentPackageName;getCount"})
{count: 3, list: [{retval: 19},{retval: 'com.solaredge.homeowner'},{retval: 59}]}
处理多个操作中的错误
如果发生错误,lastError() 将指示失败命令的 0 基索引及错误消息,例如:
>> device.sendAai({action:"version;getCurrentPackageNameX;getCount"});
null
>> lastError()
[1 - getCurrentPackageNameX] Invalid action: getCurrentPackageNameX
数据类型
FindNode 在参数中支持以下数据类型。在版本 17 中,新增了对函数(function)、数组(array)和对象(object)数据类型的支持。数据类型遵循 JavaScript 规范,并在必要时执行类型转换(type coercion)。
支持的数据类型:
- 字符串(String) – 可以使用单引号、双引号括起,或者在不包含特殊字符时可省略引号。
- 整数(Integer) – 任何整数值。
- 双精度数(Double) – 任何带有小数点的数字。
- 布尔值(Boolean) – true 或 false。
- 函数(Function) – 函数名加括号括起的参数(版本 17 新增)。
- 数组(Array) – 逗号分隔的元素,放在方括号 [] 内(版本 17 新增)。"retval" 中的数组还包含一个 "count" 属性。
- 对象(Object) – 由键值对组成,使用大括号 {} 括起(版本 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([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}
node.X
node.X
表示法用于获取 ML(匹配列表)中第一个节点的属性。在 "filter" 命令中,node.X
逐个节点进行评估,使过滤过程可以遍历 ML 并仅保留符合条件的节点。对于其他命令(如 "if"、"assert" 和 "echo"),node.X
仅检索 ML 中的第一个节点。
通用属性(General Properties)
属性 |
描述 |
class |
节点的类名 |
resourceId |
资源 ID |
text |
节点的文本内容 |
description |
内容描述 |
bounds |
返回 'Rect(left, top - right, bottom)' |
bounds.left |
左侧坐标 |
bounds.top |
顶部坐标 |
bounds.right |
右侧坐标 |
bounds.bottom |
底部坐标 |
布尔属性(Boolean Properties,带 "is" 前缀)
属性 |
描述 |
isCheckable |
节点是否可选中 |
isChecked |
节点是否已选中 |
isClickable |
节点是否可点击 |
isEditable |
节点是否可编辑 |
isEnabled |
节点是否启用 |
isFocusable |
节点是否可聚焦 |
isFocused |
节点是否被聚焦 |
isLongClickable |
节点是否支持长按 |
isMultiLine |
节点是否支持多行文本 |
isSelected |
节点是否被选中 |
isScrollable |
节点是否可滚动 |
isVisibleToUser |
节点是否对用户可见 |
与文本相关的布尔属性(Text-Related Boolean Properties)
属性 |
描述 |
isNumber |
如果 node.text 是有效数字,则返回 true |
isInteger |
如果 node.text 是有效整数,则返回 true |
以下示例将移除 ML(匹配列表)中所有非 ".Button" 类的节点:
>> device.sendAai({action:"getCount;filter(node.class == '.Button');getCount"})
{count: 3, list: [{retval: 26},{retval: true},{retval: 20}]}
对于这个计算器应用,只有按钮是可点击的,因此检查 isClickable 也同样有效:
>> device.sendAai({action:"getCount;filter(node.isClickable);getCount"})
{count: 3, list: [{retval: 26},{retval: true},{retval: 20}]}
>> device.sendAai({action:"echo(node.class);echo(node.bounds)"})
{count: 2, list: [{retval: 'android.view.ViewGroup'},{retval: 'Rect(0, 113 - 1440, 2952)'}]}
操作和查询中的变量
如果操作(action)的参数在运行时确定,有 3 种方法可以进行变量替换:
- JavaScript 字符串操作函数:适用于 action 和 query。
- JavaScript 模板字符串(template literals):适用于 action 和 query。
- aaix 辅助函数:一个简单的辅助函数,用于重新构造带变量的字符串。
如果需要在 action 命令中使用变量,可以使用 "+"
连接多个字符串,但在处理带引号的字符串时可能会显得繁琐:
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"})
{count: 10, 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'}]}
多个操作(创建一个名为 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:
在版本 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}
如果参数是字符串,以下 3 个示例是等效的:
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 个字符可以使用 "\\" 进行转义,而 "\\" 本身会在 Java 代码中被解析为 "\"。
- 单引号(')或双引号("):用于包含带有空格或逗号的文本。如果只使用一个单引号或双引号,即使文本被包裹在引号中,解析器仍然无法正确处理。例如:"I'm John"、"John's book" 可能会导致解析错误。
- 逗号(,):用于分隔多个参数。带有逗号的文本需要用引号包裹。
- 分号(;):用于在单个字符串内分隔多个操作。
- 任何其他字符:在 "\\<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's Pizza 应用的名称是 "Domino's",在 16 版之前无法正确运行,现在可以使用转义字符:
>> device.sendAai({action:"openApp(Domino\\'s)"})
{retval: true}
使用 echo
处理特殊字符的转义:
>> device.sendAai({action:"echo(John\\'s ball)"});
{retval: 'John's ball'}
>> device.sendAai({action:'echo(a\\, b\\, c)'});
{retval: 'a, b, c'}
>> device.sendAai({action:'echo(he said \\"it was me\\")'});
{retval: 'he said "it was me"'}
同步操作
FindNode 经过优化,确保所有操作都是同步执行的。例如,当调用 "click" 时,不仅确保点击是同步进行的,还会等待某些 "事件" 发生,以确保渲染 开始,但无法确保渲染完全完成。FindNode 提供了多种方法来进行检查:
- 操作命令(如 "click"): 大多数情况下可正常工作。
- "waitQuery": 如果新窗口需要较长时间才能完成渲染,可以使用此命令检查新节点是否可用,并在其后添加操作命令,以确保节点已准备好接受操作。
可选查询(OQ)
需要节点的操作将从 ML(匹配列表)中获取节点。如果操作需要一个节点,它将从 ML 中获取第一个节点。但是,用户可以通过在操作中添加可选查询(OQ)来更改此行为。
例如:{query:"T:OK", action:"click"} → {action:"click(T:OK)"}
所有 OQ 都是可选的,如果未指定 OQ,操作将从 ML 中获取节点。如果操作需要单个节点(例如 click),则使用 ML 中的第一个节点;如果操作支持多个节点(例如 getNodes 或 getText),则使用整个 ML。在 "TP" 中功能有限,任何需要参数的 TP 都会产生错误。
OQ 是常规查询语言,支持通过不同的前缀标识不同的类型。OQ 与其他查询类似,可以生成一个或多个节点,其中某些查询类型会修改 ML。以下是支持的 3 种前缀:
- 无前缀:从 ML 中应用查询,ML 不会更改(类似于 "addQuery" 操作)。
- "+": 新查询,生成新的 ML(类似于 "newQuery" 操作)。
- "*": 与常规查询相同,结果将替换 ML。
假设计算器上有数字 1 到 9,排列在 3 行:"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] No match found
条件表达式(CondExp)
条件表达式(CondExp)用于 if
、assert
和 filter
等命令,以决定执行流程。FindNode 支持三种类型的条件表达式:
- 布尔表达式 – 直接求值为 true 或 false。
- 比较表达式(Comparison Expression) – 使用比较运算符评估两个值。
- 真值/假值评估(Truthy/Falsy Evaluation) – 基于 JavaScript 规则,将值转换为 true 或 false。
所有参数都可以包含函数,因为函数的返回值是一个对象。如果函数未指定键,则默认使用 "retval" 作为键。以下示例中的 "CondExp" 代表条件表达式,函数名称必须包含括号 ()
以避免歧义。
布尔表达式(Boolean Expression)
可以是函数调用的返回值,如果未指定键,则默认使用 "retval"。
>> device.sendAai({action:"if(true, echo(OK))"});
{retval: 'OK'}
>> device.sendAai({action:"if(exists(T:/^[0-9]$/), echo(OK), echo(Not))"});
{retval: 'OK'}
>> var query = "T:Username&&OX:1";
>> device.sendAai({action:`if(exists(${query}), echo(Found), error(Username not found))`});
{retval: 'Found'}
>> device.sendAai({action:`if(exists(${query}), echo(Found), error(Username not found))`});
null
>> lastError()
User Exception: Username not found
>> device.sendAai({query:query, action:`if(node.editable, setText(${username}), error(Username text field not found))`})
{retval: true}
真值/假值(Truthy/Falsy)评估
字面值的评估遵循类似 JavaScript 的真值规则。例如,if (0) { ... } 不会执行,因为 0 被视为假值(falsy);而 if (1000) { ... } 会执行,因为 1000 被视为真值(truthy)。
评估为假(falsy)的值:
- 布尔值:false
- 整数:0
- 浮点数:0.0
- 字符串:空字符串("" 或 '')
- null
评估为真(truthy)的值:
- 布尔值:true
- 整数:任何非零数值
- 浮点数:任何非零数值
- 字符串:任何长度大于 0 的字符串
- 数组:始终为真
- 对象:始终为真
>> device.sendAai({action:"function(isTrue)", isTrue:"if(%1, echo(is true), echo(is false))"})
{retval: true}
>> device.sendAai({action:"isTrue(0)"});
{retval: 'is false'}
>> device.sendAai({action:"isTrue(1)"});
{retval: 'is true'}
>> device.sendAai({action:"isTrue(Hello)"});
{retval: 'is true'}
>> device.sendAai({action:"isTrue('')"});
{retval: 'is false'}
>> device.sendAai({action:"isTrue(false)"});
{retval: 'is false'}
>> device.sendAai({action:"isTrue(null)"});
{retval: 'is false'}
>> device.sendAai({action:"isTrue([])"});
{retval: 'is true'}
>> device.sendAai({action:"isTrue({})"});
{retval: 'is true'}
>> device.sendAai({action:"isTrue([true,false][0])"});
{retval: 'is true'}
>> device.sendAai({action:"isTrue([true,false][1])"});
{retval: 'is false'}
>> device.sendAai({action:"isTrue(echo(100-100))"});
{retval: 'is false'}
>> device.sendAai({action:"isTrue(getText(BP:editable&&IX:0))"});
{retval: 'is true'}
比较表达式
比较表达式由左参数、比较运算符和右参数组成。每个参数可以是一个值或函数。如果参数是函数,则函数会在比较执行之前先运行。
比较运算符
FindNode 支持以下比较运算符:
运算符 |
描述 |
= 或 == |
检查左参数是否等于右参数 |
> |
检查左参数是否大于右参数 |
< |
检查左参数是否小于右参数 |
>= |
检查左参数是否大于或等于右参数 |
<= |
检查左参数是否小于或等于右参数 |
!= |
检查左参数是否不等于右参数 |
注意事项:
- null 可以用于比较,例如
== null
或 != null
,"{retval:null}" 也是有效的返回值。这在比较操作中非常有用,例如 getText() != null
。
- 右参数不支持算术表达式或
node.X
,可以使用 echo()
获取值,该限制将在下一个版本中修复。
示例:
可以使用 echo
测试比较表达式:
>> device.sendAai({action:"echo(40/2 == 20)"});
{retval: true}
>> device.sendAai({action:"echo(40/2 == 2*10)"});
null
>> lastError()
Right param has invalid value
>> device.sendAai({action:"echo(40/2 == echo(2*10))"});
{retval: true}
>> device.sendAai({action:"if(getText(T:1&&TX)[-1] == '%', echo(matched), error(% not matched))"})
{retval: 'matched'}
>> device.sendAai({action:"if(saveImageInfo().format != 'png', saveImageSettings({format:'png'}))"})
{retval: true}
>> device.sendAai({action:"function(clickExists)", clickExists:"if(exists(T:%1), click(T:%1), return(false))"})
{retval: true}
数组和对象比较
>> device.sendAai({action:"if([1,2,3] == [1,2,3],echo(true),echo(false))"})
{retval: true}
>> device.sendAai({action:"if([1,2,3] == [2,1,3],echo(true),echo(false))"})
{retval: false}
>> device.sendAai({action:"if({a:1, b:2} == {b:2, a:1},echo(true),echo(false))"})
{retval: true}
>> device.sendAai({action:"if({a:1, b:2} == {b:2, a:1, c:3},echo(true),echo(false))"})
{retval: false}
>> device.sendAai({action:"if([1,2,3][1] == 2,echo(true),echo(false))"})
{retval: true}
>> device.sendAai({action:"if({a:1, b:2}.a == 1,echo(true),echo(false))"})
{retval: true}
>> device.sendAai({action:"if({a:1, b:2}.b == 1,echo(true),echo(false))"})
{retval: false}
>> device.sendAai({action:"if([1,2,[4,5]][2] == [4,5],echo(true),echo(false))"})
{retval: true}
检查 Android 版本是否为 14 或更高:
>> device.sendAai({action:"function(getAndroidVersion)", getAndroidVersion:`
openAndroidSetting(DEVICE_INFO_SETTINGS);
newQuery(T:Software*);
click;
newQuery(T:Android version&&OY:1);
getText;
`})
{retval: true}
>> device.sendAai({action:"getAndroidVersion"});
{retval: '15'}
>> device.sendAai({action:"function(ensureAndroidVersion(14))", ensureAndroidVersion:"if(getAndroidVersion() >= %1, return(true), return(false))"})
{retval: true}
>> device.sendAai({action:"ensureAndroidVersion"})
{retval: true}
>> device.sendAai({action:"ensureAndroidVersion(16)"})
{retval: false}
get 命令
getBounds (M)
返回 ML(匹配列表)中节点的边界(格式:[left, top, right, bottom])。返回两个数组:一个包含所有节点 ID,另一个包含所有边界信息。
用法:
getBounds[(<query>)]
返回值:
一个包含边界信息的数组,每个边界由 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 [(<query>)]
返回值:
一个包含布尔属性的字符串数组,存储在 "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&&XT:Every day&&OX:1", action:"getBoolProp"})
{count: 2, retval: ['checkable','clickable']}
>> device.sendAai({query:"T:ci:calculator", action:"getBoolProp"})
null
>> lastError()
No boolean property is found
getBoolPropInAncestors (O)
与 getBoolProp
类似,但它会遍历父节点,直到到达顶级节点。
用法:
getBoolPropInAncestors [(<query>)]
返回值:
一个包含属性和节点 ID 的数组,如果未找到匹配项,则返回 null。
>> device.sendAai({query:"T:John", action:"getBoolPropInAncestors"})
{count: 2, retval: [['clickable','1b86f'],['scrollable','16d5b']]}
getChecked (M)
此命令返回“可选(checkable)”节点的布尔值。通常,切换控件(toggle controls)、复选框(checkboxes)或单选按钮(radio buttons)是可选的。例如,类名为“.Switch”或“.Checkbox”的元素是可选的。
用法:
getChecked [(<查询>)]
返回值:
如果输入的是单个节点,它将在“retval”中返回 true/false;如果是多个节点,“retval”将包含一个 true/false 的数组,对于非可选节点,将显示为“N/A”。如果未找到任何可选节点,则返回 null。使用“BP:checkable”查找可选节点,使用“BP:checked”查找已选中的节点。
另见: setChecked
>> device.sendAai({action:"getChecked(D:Sunday&&TX)"})
{count: 7, retval: [false,true,true,true,true,true,false]}
如果不想使用描述进行搜索,可以使用类名进行搜索,因为没有其他 UI 元素的类名为 .CheckBox
:
>> device.sendAai({action:"getChecked(C:.CheckBox&&IX:0&&TX)"})
{count: 7, retval: [false,true,true,true,true,true,false]}
查找所有已选中的描述:
>> device.sendAai({action:"getDescription(D:Sunday&&R:.day_button_0&&TX&&XBP:checked)"})
{count: 5, retval: ['Monday','Tuesday','Wednesday','Thursday','Friday']}
getCount (M)
此命令返回 ML(匹配列表)或 OQ(可选查询)的长度。如果使用了 OQ,而 OQ 中未找到匹配项,则返回 0。
用法:
getCount [(<query>)]
返回值:
在 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}
getDeviceName
此命令返回设备名称。
用法:
getDeviceName
返回值:
返回设备名称。
示例:
>> device.sendAai({action:"getDeviceName"})
{retval: 'samsung-SM-S938U1'}
getFocus
返回当前获得焦点的节点(在 getNodes(BP)
中为 "focusable"),通常是文本输入框或滑块。如果没有节点获得焦点,则返回 null
。如果找到节点,ML(匹配列表)将设置为该节点。
用法:
getFocus
返回值:
如果找到节点,retval
返回节点 ID;否则返回 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
在函数执行过程中,每个命令的输出都会被存储。要检索这些输出,可以使用 getFuncRetval。为了获得最佳效果,建议将 getFuncRetval 作为函数中的最后一个命令。当 getFuncRetval 无参数调用时,它将返回当前函数执行过程中所有命令的输出。然而,如果提供了参数,则仅返回指定命令的输出。
此功能在包含多个命令的函数中特别有用,可以精确检索特定输出,例如 getText 的结果。需要注意的是,getFuncRetval 必须在函数的上下文中执行,并且不适用于 forEach 或 repeat 等操作。当从 search 命令检索结果时,仅应指定命令名称,而不包含任何参数。
用法:
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(testFR)", testFR:`
echo(10*20);
getDeviceName();
getCount();
getCurrentPackageName();
getFuncRetval(%1)`})
{retval: true}
>> device.sendAai({action:"testFR(echo)"})
{count: 1, retval: [{retval: 200}]}
>> device.sendAai({action:"testFR(getDeviceName)"})
{count: 1, retval: [{retval: 'samsung-SM-S938U1'}]}
>> device.sendAai({action:"testFR(getCount)"})
{count: 1, retval: [{retval: 16}]}
获取特斯拉的当前位置:
>> 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'}]
getIds (M)
如果未定义 action
,此命令将作为默认操作执行。它返回匹配列表(ML)中的 ID 数组及其计数。
此前,该命令返回的 ID 存储在 ids
字段中,为了保持一致性,现已改为存储在 retval
字段中。
用法:
getIds [(<query>)]
返回值:
返回 count
(匹配的节点数量)以及 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 (M)
该命令用于检索节点信息,可通过指定字段来确定返回的数据内容。在获取信息之前,getNodes
会先刷新节点。
用法:
getNodes
getNodes([<query>] [,<fields>])
参数:
fields(可选):用于定义要返回的字段,多个字段以逗号分隔。由于 "fields" 内包含逗号,因此需要使用引号括起来。可用的字段标识符如下:
- P: 包名(String)
- C: 类名(String)
- R: 资源 ID(String)
- D: 描述(String)
- T: 文本内容(String)
- IT: 输入类型(Integer)
- CC: 子节点数(Integer)
- RI: 范围信息(RangeInfo),提供有关小部件(如 SeekBar/滑块)的类型、最小值、最大值和当前值,可使用
setProgress
命令更改值。
- BP: 返回节点中的所有布尔属性。
- B: 返回节点的边界(格式:[左 上][右 下])(String)。
- All: 返回所有信息。
返回值:
返回匹配节点的数量(count)以及节点信息数组。
示例:
>> device.sendAai({action:"getNodes(T:/^[0-9]$/,'R,T')"})
{count: 10, retval: [{id: 'a7d3', resourceId: '.button_seven', text: '7'},{id: 'ab94', resourceId: '.button_eight', text: '8'},{id: 'af55', resourceId: '.button_nine', text: '9'},{id: 'b6d7', resourceId: '.button_four', text: '4'},{id: 'ba98', resourceId: '.button_five', text: '5'},{id: 'be59', resourceId: '.button_six', text: '6'},{id: 'c5db', resourceId: '.button_one', text: '1'},{id: 'c99c', resourceId: '.button_two', text: '2'},{id: 'cd5d', resourceId: '.button_three', text: '3'},{id: 'd4df', 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 (O)
返回 UI 元素(如滑块)的进度类型值("getNodes(RI)" 的简写):
用法:
getProgress [query]
返回:
进度节点的 RangeInfo,存储在 "retval" 中。如果节点不是可调整进度的节点,则返回 null。
RangeInfo 具有以下三种类型之一:"float" – 在 min 和 max 之间的浮点数,"int" – 在 min 和 max 之间的整数,"percent" – 在 0 和 1 之间的百分比值,"current" 包含当前的进度值。
>> device.sendAai({action:"getProgress(C:.SeekBar)"})
{retval: {current: 41, max: 100, min: 0, type: 0, typeString: 'int'}}
另见: setProgress
getQuery / getUniqQuery (O)
接受一个节点,并尝试生成一个查询以匹配该节点(或多个节点)。从 "getQuery" 返回的查询可以匹配多个节点,适用于获取多个节点的文本或描述信息。而 "getUniqQuery" 返回的查询仅匹配一个节点,该查询可以用于在其他设备上定位相应的节点。目前该功能较为基础,未来将开发更优化的查询字符串:
- 将使用匹配列表(query 或 element)中的第一个节点作为参考节点。
- 输出结果尚未完全优化,例如,不会使用模板。
- 如果找到多个节点,"getUniqQuery" 将添加 "IX" 以标识单个节点。但这对于不使用资源 ID 的应用程序可能会有误差,IX 可能是一个较大的数字,任何 UI 变化可能都会影响查询的准确性。
- UI Explorer 的 "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"}).retval.forEach(
x => print(device.sendAai({elements:[x],action:"getUniqQuery"}).retval)
)
T:Controls&&OX:-1
T:Controls
T:Controls&&OX:1
getText / getDescription / getHintText (O|M)
这是 getNodes
的简化版本,更方便地获取文本内容、描述信息和提示文本。
如果查询到一个节点,返回该节点的值;如果查询到多个节点,则返回包含所有文本/描述/提示文本的数组。
用法:
getText [(<query>)]
getDescription [(<query>)]
getHintText [(<query>)]
返回值:
- 如果查询到多个节点,
retval
返回文本/描述/提示文本的数组。
- 如果查询到单个节点,
retval
返回该节点的文本/描述。
示例:
>> device.sendAai({query:"TP:line,bottom,-1", action:"getText"})
{retval: ['Chats','Calls','Contacts','Notifications']}
// One value will not return an array
>> device.sendAai({query:"TP:line,bottom,-1&&IX:1", action:"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']}
查询命令
以下命令(除 waitQuery
外)都涉及 ML(匹配列表)。
newQuery/addQuery/waitQuery/exists(M)
所有命令都接受查询字符串。
- newQuery 启动一个新的查询并重新创建匹配列表(ML),类似于在 OQ 中使用 "+"。
- addQuery 在现有 ML 内执行搜索,并将匹配到的节点放入 ML,类似于在 OQ 中使用 "*"。
- waitQuery 不会修改 ML,但会确保查询在超时前完成。如果条件未在超时内满足,则返回 null。由于 waitQuery 不会更改 ML,如果需要像 newQuery 那样修改它,请使用带超时的 newQuery。
- exists 返回 true 或 false,指示查询是否找到匹配的节点。使用 exists 时,如果设置超时,则仅适用于 OQ 中的 "+"(新查询),否则会报错。"exists" 是唯一在未找到节点时返回 false 的命令,大多数命令在未找到时返回 null。
包含超时的命令在超时结束前不能切换到不同的包,否则会抛出错误。这些命令在执行期间只会在相同的包名(或应用)内进行搜索。
示例:
- "VG" 通常应跟随 addQuery 而不是 newQuery,以优化现有的 ML。
- 当执行会打开新窗口的操作时,需要使用 newQuery(<query>) 来“重新读取”新窗口中的节点。如果您打算使用 OQ 并且没有其他条件,请使用 newQuery(TP:default)。
用法:
newQuery(<query>[, <timeout>])
addQuery(<query>)
exists(<optional query>[, <timeout>])
waitQuery(<query>[, <timeout>]) // 默认超时 = 2000ms
返回值:
"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}]}
>>device.sendAai({action:`exists(+T:${text},10000);click`})
{count: 2, list: [{retval: 1},{retval: true}]}
intersectX / intersectY (O → M)
如果指定了查询(query),则使用查询结果的第一个节点作为参考节点,否则使用 ML(匹配列表)的第一个节点作为参考。该命令会基于一个节点生成多个匹配节点,并存储到 ML 中。
用法:
intersect?[(<query>)]
返回值:
true / false: 如果操作成功。 如果操作成功,它将返回 "count"
如果指定了 "post query",则其输出(参见 "addQuery")将存储为 ML。 如果未指定 "post query",则 intersect 的输出将替换 ML。
另见: "TX" 和 "TY" 查询的详细解释。
示例:
假设在计算器应用中:
>> device.sendAai({actions:["intersectX(T:3)", "getText"]})
{count: 2, list: [{retval: 4},{retval: ['1','2','3','%']}]}
intersectY
可能会包含一些不可见的节点,例如计算结果或视图容器:
>> 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},{retval: 5},{count: 5, retval: ['×','9','6','3','±']}]}
这样也可以:
>> device.sendAai({actions:["intersectY(T:3)", "getText(T:/.+/)"]})
{count: 2, list: [{retval: 8},{count: 5, retval: ['×','9','6','3','±']}]}
或者使用过滤器移除非文本节点:
>> device.sendAai({actions:["intersectY(T:3)", "filter(node.text != null)", "getText"]})
{count: 3, list: [{retval: 8},{retval: true},{count: 5, retval: ['×','9','6','3','±']}]}
push/pop & load/save (M)
这些命令用于存储和恢复 ML(匹配列表),主要用于多步骤命令。例如,可以 push
或 save
现有的 ML,执行 newQuery
和其他操作后,再使用 pop
或 load
恢复之前的 ML 继续操作。
save/load
主要用于兼容性,因为它们没有存储 ML 的数量限制。实际上,在实现中,save
和 push
作用相同,load
和 pop
作用相同。
用法:
返回值:
始终返回 retval
为 true
,如果栈为空且尝试 pop
,则会生成错误。
reduceNodes (M → M)
等同于 RN
查询,无需参数。执行后,返回结果中的节点数量。
用法:
reduceNodes
返回值:
返回操作后的节点数量,存储在 retval
中。
另见: 查询 "RN"
>> device.sendAai({query:"TP:reduced"}).count
54
>> device.sendAai({action:"reduceNodes"})
{retval: 54}
sort (M)
根据节点边界进行排序,与 ST
查询相同,可使用相同的参数。
用法:
sort(X|Y|YX)
返回值:
返回 retval
为 true
或 false
。
另见: 查询 "ST"
viewGroup (O → M)
该命令类似于 VG
查询,可接受层级(level)和查询参数。如果提供查询,它用于识别第一个节点。与 VG
一样,执行该命令后,ML 将被修改。此命令的早期版本称为 getViewGroup
。
用法:
viewGroup
viewGroup([<query>,] <level>)
返回值:
如果找到 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: [{retval: 6},{retval: '92% available'}]}
节点上的操作命令
本节中的命令用于对节点执行操作。
click, nsClick, click2, longClick (O)
不同类型的点击操作,这些是同步点击,确保在返回控制时屏幕已经开始刷新。
- click: 点击节点中心。
- longClick: 长按(500ms),模拟长按点击。
- click2: 在节点的随机位置点击。
以上点击操作会监听屏幕事件,确保点击后屏幕发生变化。如果点击后屏幕未发生变化,这些点击可能会失败并最终超时。对于此类情况,可以使用 nsClick
:
"nsClick" 还接受两个整数,如果使用硬编码的 x 和 y 值,它将在 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, retval: ['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 (O)
echo
和 log
的用法完全相同,echo
在 retval
中显示输出,而 log
将输出记录在 logcat 中。它们接受以下类型:
- 值:布尔值、整数、浮点数、字符串、函数、数组和对象。
- 函数调用:调用函数并返回其值。
- 比较表达式:如果使用比较运算符,则返回
true
或 false
。
- 对于字符串,如果字符串包含有效的算术表达式,则会执行计算并输出结果。
node.X
:返回 ML(匹配列表)中第一个节点的信息。
用法:
echo (<CondExp>)
log (<CondExp>)
返回值:
echo
会在 retval
中返回结果。
log
始终返回 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'}]}
>> device.sendAai({action:"echo(listAndroidSettings()[0])"});
{retval: 'ACCESSIBILITY_DETAILS_SETTINGS'}
>> device.sendAai({action:"echo(listAndroidSettings()[-1])"});
{retval: 'ZEN_MODE_PRIORITY_SETTINGS'}
执行简单的整数运算,支持的运算符包括:+、-、*、/、% 和 ()。
>> 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 将显示 "Log: 192807"。
filter (M)
filter
命令允许用户根据指定的条件表达式操作 ML(匹配列表)。它会遍历所有节点,并移除不匹配的节点。如果 ML 为空,则会生成错误。条件表达式通常包含 node.X
。
用法:
filter(<CondExp>)
返回值:
成功时返回 true
,如果 ML 为空,则返回 null
。
示例:
如果整个屏幕上只有一个输入框,可以使用以下命令:
>> device.sendAai({action:`getCount;push;filter(node.isEditable);getCount; setText(${input});pop;getCount`})
{count: 7, list: [{retval: 227},{retval: true},{retval: true},{retval: 1},{retval: true},{retval: true},{retval: 227}]}
>> device.sendAai({action:"filter(node.text!=null); getText"})
{count: 2, list: [{retval: true},{count: 21, retval: ['Calculator','AC','÷','×','DEL','7','8','9','-','4','5','6','+','1','2','3','%', '0','.','±','=']}]}
>> device.sendAai({action:"filter(node.text != null);filter(node.isNumber);getText"})
{count: 3, list: [{retval: true},{retval: true},{count: 10, retval: ['7','8','9','4','5','6','1','2','3','0']}]}
forEach (M)
该操作会遍历 ML(匹配列表)中的每个节点,并逐个执行提供的操作。以下规则适用:
- 不能执行会改变 ML 的操作,否则会生成错误。
- 所有带有前缀 ("+" 或 "*") 的可选查询(OQ)都会导致错误。
- 所有循环操作都会逐个处理单个节点。
- 返回值:
- 如果参数中只有单个操作,"retval" 数组会存储每个节点的输出结果。
- 如果包含多个操作,JSON 结果会包含一个数组的数组,内部数组存储单个节点的执行结果,外部数组存储所有节点的执行结果。
对于多个操作,forEach
以 JSON 对象名称作为参数,值可以是数组或用分号分隔的字符串。如果只执行单个操作,可以直接在参数中指定操作名称。
用法:
forEach(<action name>)
forEach(<JSON name>)
返回值:
- 如果参数中只有一个操作,"retval" 数组会存储每个节点的输出。
- 如果有多个操作,"retval" 将包含一个嵌套数组,内部数组存储单个节点的执行结果,外部数组存储所有节点的执行结果。
示例:
第一行执行 getText
一次,第二行执行 getText
四次。
>> device.sendAai({query:"T:1&&TX", action:"getText"})
{count: 4, retval: ['1','2','3','%']}
>> device.sendAai({query:"T:1&&TX", action:"forEach(getText)"})
{count: 4, retval: [{retval: '1'},{retval: '2'},{retval: '3'},{retval: '%'}]}
>> device.sendAai({query:"R:/.day_button_[0-6]/", action:"getText"})
{count: 7, retval: ['S','M','T','W','T','F','S']}
>> device.sendAai({query:"R:/.day_button_[0-6]/", action:"forEach(setChecked(true))"})
{count: 7, retval: [{retval: true},{retval: false},{retval: false},{retval: false},{retval: false},{retval: false},{retval: true}]}
在示例文档中,对于计算器应用,它会将所有操作填充到 "elements" 中,并使用 forEach(nsClick)
来输入计算。
if, assert (O)
if
和 assert
命令可以使用条件表达式来判断 true
或 false
(请参阅 Action 部分中的 "Conditional Expression")。
- 对于这两个命令,都需要条件表达式来判断
true
或 false
。请参考 ACTIONS 部分中的 CondExp 章节。
- 对于
if
,如果条件为 true
,则执行 then
,如果为 false
,则执行 else
。如果 then
或 else
设为 null
,则不会执行任何操作。如果 else
未指定或设为 null
,返回值为 {retval: true}
。
- 如果 ML(匹配列表)中有多个节点,
if
和 assert
仅使用 ML 中的第一个节点。
- 对于
assert
,如果条件为 true
,返回 {retval: true}
,如果为 false
,会引发错误,并将错误传播到 sendAai()
,随后使用 RingoJS 的 assert.fail()
生成 AssertionError
。在测试环境中,AssertionError
会导致测试失败。
if
语句可以包含 return
以退出函数。
用法:
if(<CondExp>, <then>, <else>)
assert(<CondExp>[, <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}
或者,可以使用 return
:
>> 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())"})
假设是计算器应用,.formula 资源将显示计算结果,我们可以创建一个用于单数字加法的函数,如下所示:
>> device.sendAai({action:"function(add)", add:"click(T:%1);click(T:+);click(T:%2);click(T:=);getText(+R:.formula)"})
{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)
无障碍(Accessibility)的节点信息是缓存的,如果屏幕发生了更新,缓存的信息可能会不准确,使用 refresh
以强制重新读取节点信息。
用法:
refresh [(<query>)]
返回值:
返回节点数量,并始终返回 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)
该命令用于执行重复操作,它与 forEach
共享相同的输入(JSON 或单个操作),并且返回值类似。它会根据指定的运行次数重复执行 action/actions。最大重复次数为 20,可使用 setConfig
在 action:repeat:maxCount
上调整最大重复次数。
用法:
repeat (<count>, <action name>)
repeat (<count>, <JSON name>)
返回值:
- 如果参数中只有一个操作,"retval" 数组将存储每次运行的输出。
- 如果有多个操作,JSON 结果将包含一个嵌套数组,内部数组存储单个运行的执行结果,外部数组存储所有运行的执行结果。
示例:
此命令在计算器中点击 5 次 "1":
>> device.sendAai({action:"repeat(5,click(T:1))"})
{retval: [{retval: true},{retval: true},{retval: true},{retval: true},{retval: true}]}
设置电动车(EV)充电电流:
减少 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'}]}
saveImage, saveImageInfo, saveImageSettings (M)
saveImage
命令会将 ML(匹配列表)中的所有节点截图并保存到设备上。该操作可以使用 saveImageSettings
进行配置,并可以使用 saveImageInfo
获取当前配置。
图片可以保存为 JPEG 或 PNG 格式,并可以存放在共享图片文件夹或应用的私有文件夹中。可配置的属性如下:
- format: jpg 或 png。指定图片格式,默认值为 jpg。
- folder: share 或 private。"share" 将图片保存在公共图片文件夹,而 "private" 将其保存在 Total Control 私有目录
/storage/emulated/0/Android/data/com.sigma_rt.totalcontrol/files/Pictures
中。默认值为 "share"。
- quality: 30 – 100。指定 JPEG 图片质量,仅适用于 JPEG 格式。默认值为 80。
- resolution: 720p, 1080p, 2k, 4k 或 native。定义保存后的目标分辨率。例如,2K 屏幕的图片可以保存为 720p 以减少文件大小。"native" 选项会以原始分辨率保存。默认值为 1080p。
默认情况下,saveImage
会尝试截取 ML 中所有可见节点的区域,并保存为图片。它还支持可选查询(OQ),例如 "IX:0"
只保存 ML 中的第一个节点。如果操作成功,将返回保存的图片文件的绝对路径。
可以使用 saveImageSettings
以 JSON 格式设置参数。例如:
saveImageSettings({format: 'png', folder: 'private', resolution: '720p'})
要获取当前设置,使用 saveImageInfo
,它会返回当前的配置信息(JSON 格式)。
用法:
saveImage([query])
saveImageSettings({…})
saveImageInfo()
返回值:
saveImage
返回图片文件的绝对路径,错误时返回 null
。
saveImageSettings
设置成功返回 true
,失败返回 null
。
saveImageInfo
返回当前的配置信息(JSON 格式)。
示例:
以计算器应用为例:
>> device.sendAai({query:"T:/^[0-9]$/", action:"saveImage"})
{retval: '/storage/emulated/0/Pictures/Samsung-SM-S908-2024-12-01-03-51-48.jpg'}
>> device.sendAai({action:"saveImageSettings({format:'png',folder:'private',
resolution:'720p'})"})
{retval: true}
>> device.sendAai({query:"T:/^[0-9]$/", action:"saveImage"})
{retval: '/storage/emulated/0/Android/data/com.sigma_rt.totalcontrol/files/Pictures/Samsung-SM-S908-2024-12-01-03-56-18.png'}
对于 T:0 到 T:9,截图如下所示,可以使用 adb pull 命令获取返回的图片文件。
如果只想保存一个节点的图片,可以使用 "IX" 选项:
>> device.sendAai({action:"saveImageSettings({format:'jpg',quality:50, folder:'private',resolution:'1080p'})"})
{retval: true}
>> device.sendAai({action:"saveImageInfo"})
{retval: {folder: 'private', format: 'jpeg', quality: 50, resolution: '1080p'}}
>> device.sendAai({query:"T:/^[0-9]$/", action:"saveImage(IX:0)"})
{retval: '/storage/emulated/0/Android/data/com.sigma_rt.totalcontrol/files/Pictures/Samsung-SM-S908-2024-12-01-04-04-45.jpg'}
第一个节点是 7(位于左上角):
如果将 IX:0
更改为 IX:-1
,最后一个节点将显示为 0。
以下命令用于捕获 Skype 的导航图标:
>> device.sendAai({query:"T:Calls&&VG", action:"saveImage"})
{retval: '/storage/emulated/0/Android/data/com.sigma_rt.totalcontrol/files/Pictures/Samsung-SM-S908-2024-12-01-04-14-47.jpg'}
使用 "VG" 查询,最大的 ViewGroup 会被放置在第一个节点,query:"T:Calls&&VG&&IX:0"
将获得相同的截图。在 SigmaTestApp 中,如果只想保存 CheckBox 的截图:
>> device.sendAai({query:"C:.CheckBox", action:"saveImage"})
{retval: '/storage/emulated/0/Android/data/com.sigma_rt.totalcontrol/files/Pictures/Samsung-SM-S908-2024-12-01-04-30-37.jpg'}
scrollIntoView → (M)
接受查询字符串和滚动方向,根据提供的方向向上/向下滚动以匹配查询。当找到一个或多个匹配的节点时,将返回结果;如果找不到匹配项,将返回 null 并抛出错误。滚动操作会使用页面上下翻动(最多 30 页,由 setConfig(navi:maxPageScroll, <number>)
控制),可能需要一定时间,因此此命令会自动延长 sendAai
的超时时间。为了使脚本能适用于不同分辨率和尺寸的设备,建议使用 scrollIntoView
而不是直接翻页操作。该操作还会确保匹配到的第一个节点完全可见,并修改 ML(匹配列表),其中可能包含多个节点。
用法:
scrollIntoView(<query>)
// 默认向下滚动
scrollIntoView(<query>, <direction>)
参数:
<direction>
可以是以下之一,默认值为 "down"(向下滚动):
- "down": 从当前位置开始,向下滚动查找。
- "up": 从当前位置开始,向上滚动查找。
- "top": 快速滚动到页面顶部,然后向下查找。
- "bottom": 快速滚动到页面底部,然后向上查找。
返回值:
如果找不到匹配项,将返回 null
;如果找到匹配项,将返回 true
,同时 ML 将被设置为找到的第一个节点 ID。
示例:
滚动当前应用界面,直到找到 "John",然后点击匹配到的节点:
>> device.sendAai({actions:["scrollIntoView(T:John)", "click"]})
{count: 2, list: [{retval: true},{retval: true}]}
setChecked (M)
该命令适用于带有 "checkable"(可选)属性的节点。通常,开关控件、复选框或单选按钮都是可选的,例如 .Switch
或 .Checkbox
。对于单选按钮,setChecked(false)
将无法禁用当前选中的按钮,因为 FindNode 无法确定应该点击哪个按钮来取消选中状态。
用法:
setChecked(true|false)
setChecked([query], true|false)
参数:
setChecked(true|false)
:由于没有权限直接更改选中状态,它会点击节点来切换值(如果需要更改)。它可以应用于多个节点,并会忽略非可选节点。
返回值:
返回 true/false 以指示是否至少有一个节点被更改。
setProgress (O)
该命令用于设置滑动条(进度条)的值。可以使用 getNodes('RI')
获取滑动条的最小值(min)、最大值(max)、类型(type)和当前值(value)。
用法:
setProgress(<number>)
setProgress(<query>,<number>)
参数:
<number>
:整数或小数,用于设置进度值,范围由 "type"、"min" 和 "max" 决定。
返回值:
true/false
:如果值成功设置,则返回 true
;如果值未更改或超出范围,则返回 false
。如果节点不是进度条节点,则返回 null
。
参见: getProgress
示例:
将显示亮度设置为 50%:
>> device.sendAai({query:"T:Brightness&&VG&&XC:.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"}).retval.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”结尾:
- 通常,“搜索”字段没有执行搜索的按钮。在文本输入后,这将发送“搜索”操作。
- 对于启用了“回车即发送”功能的应用程序,它将在输入文本后自动发送消息,而无需单独点击发送按钮。
- 默认情况下,setText 会重置文本,用新值替换现有值。然而,如果新文本以 + 开头,它将被追加到当前文本的末尾,而不是替换它。
用法:
setText([query], <input string>)
setText([query], +<input string>)
参数:
输入字符串:默认情况下,在输入字符串之前会先清空文本字段。如果输入字符串的第一个字符是“+”,则它将在现有字符串的基础上追加输入内容。对于多行文本字段,使用“\n”跳转到下一行。
返回:
返回 true 或 false,以指示操作是否成功。
示例:
>> device.sendAai({query:"BP:editable", action:"getText"})
{count: 5, retval: ['Text','Number','Email','Text password','Text']}
>> device.sendAai({query:"BP:editable", action:"setText(T:Number, '100');setText(T:Text password, secretWord)"})
{count: 2, list: [{retval: true},{retval: true}]}
无法两次运行相同的命令,因为数字文本字段的值已更改为 100。使用 "HT" ,它不会随值的变化而更改。
>> device.sendAai({query:"BP:editable", action:"setText(HT:Number, '100');setText(HT:Text password, secretWord)"})
{count: 2, list: [{retval: true},{retval: true}]}
密码被一系列点掩盖:
>> device.sendAai({query:"BP:editable", action:"getText"})
{count: 5, retval: ['Text','100','Email','••••••••••','Text']}
使用 "+" 进行拼接:
>> device.sendAai({query:"TP:textInput&&IX:4", action:"setText('Hello');setText('+ World'); getText"})
{count: 3, list: [{retval: true},{retval: true},{retval: 'Hello World'}]}
要将 Enter 作为发送,添加 \n:
>> device.sendAai({query:"TP:textInput", action: "setText('Hello\n')"})
{retval: true}
通过 String.fromCodePoint 输入 Emoji:
>> 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。
swipeAcross (O)
swipeAcross 将以 ML 中的第一个节点为基准,按照指定方向从一端滑动到另一端,可选的步数参数决定滑动速度,步数越少速度越快。该命令适用于通过向左或向右滑动来关闭消息。
用法:
swipeAcross [query,] <left|right|up|down> [,<步数>]
返回值:
true 或 false,表示滑动是否成功。
示例:
>> device.sendAai({action:"swipeAcross(T:Alarm deleted, right)"})
{retval: true}
until (M)
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'}]}
操作命令
这些命令不涉及节点,但对于创建脚本非常有用。
error
该命令会停止函数执行,并在 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
exec
“exec” 命令使用 "sh" 执行指定的命令,并返回输出。如果发生错误,该命令将返回 null,并且可以使用 "lastError()" 获取错误消息。请注意,并非所有命令都被允许执行;如果执行失败,请检查 "lastError()" 是否包含 "Permission denied" 消息。
此外,命令不能包含 ">",因为它被保留用于捕获输出和错误消息。
用法:
exec (<命令行>)
返回值:
返回 "retval" 中的输出,如果发生错误,则返回 null。
示例:
>> device.sendAai({action:"exec(dumpsys activity activities | grep ResumedActivity | tail -1)"})
{retval: ' ResumedActivity: ActivityRecord{57c186 u0 com.veronicaapps.veronica.simplecalculator/.MainActivity t1027}'}
>> device.sendAai({action:"exec(ls /data/local)"})
null
>> lastError()
ls: /data/local: Permission denied
>> device.sendAai({action:"exec((cd /etc;ls|head -5)&&echo ----&&ls |head -5)"})
{retval: 'ADP.xml
ASKSB.xml
ASKSC.xml
ASKSHB.xml
ASKSP.xml
----
acct
apex
audit_filter_table
bin
bootstrap-apex'}
function/funcExists/funcDelete/funcReset/funcList
此操作命令用于创建新的组合操作:
- 一旦创建了函数,该函数可以被接受函数或操作的其他操作调用。请勿创建与现有命令相同的名称,否则它将无法被调用。
- 它支持类似批处理文件的变量替换,%<数字> 对应于调用参数。与普通操作参数一样,替换可以是字符串、浮点数、布尔值(true/false)和整数。如果参数数量不足,则使用 "function" 创建操作时定义的默认值。任何 "%” 后面没有数字的情况不会被干扰。如果需要 "%1" 作为参数(例如用于 setText),请使用 "%%" 代表 "%",例如 "%%1" 将解析为 "%1",不会进行替换。
- 它支持默认值,在创建函数时,可以设置新参数的默认值。如果调用函数时参数数量少于默认值,则使用默认值。
- 请勿使用递归调用(或导致循环调用的函数),否则可能会导致程序崩溃。
- 在版本 17 之前,函数会返回函数内每个命令的返回值。从版本 17 开始,函数将返回函数内最后一个命令的返回值。"return" 命令可以改变此行为(参见 "return" 操作获取更多信息)。"forEach" 和 "repeat" 仍然会保留之前的返回值。
用法:
创建新函数:
function(<JSON 格式的操作名称>(参数)),<JSON 名称>: [<操作>] 或使用 ";" 分隔多个操作,或使用单个操作
删除创建的函数:
funcDelete(<函数名称>)
返回 true/false,指示函数是否存在:
funcExists(<函数名称>)
返回函数名称列表(数组)及数量(版本 17):
funcList
重置 - 删除所有函数:
funcReset
返回值:
如果发生错误,则返回 null,否则返回 true(在 retval 中)。
示例:
假设我们希望使用计算器来计算两个单数字的加法:
>> 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" 和 "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)"})
{retval: [{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
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
openApp/restartApp/closeApp
指定的应用名称可以是包名,也可以是应用在启动器上显示的名称(应用名称)。如果匹配到多个相同名称的应用,则使用第一个匹配的应用。不允许部分匹配,匹配不区分大小写。"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" 都表示相同的设置。
return
"return" 是一个重要的函数,具有以下两个作用:
"return" 仅在函数内部生效,在函数外部,它的行为类似于 "echo"。默认情况下,函数将返回其最后一条命令的返回值,但可以使用 "return" 和 "getFuncRetval" 修改该行为。"return" 接受一个参数,如果未指定参数,则返回 true。"return(null)" 也是允许的。版本 17 之前,函数的执行结果是函数内每条命令的返回值数组。若要恢复此行为,请使用 "return(getFuncRetval())"。
用法:
return[<条件表达式>] // 如果未指定参数,则返回 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}]}
演示 Return 可在函数执行过程中停止执行:
>> 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
sendKey
sendKey 接受按键代码或元状态,并将键发送到屏幕(而不是特定节点)。具有焦点的 UI 元素(例如文本字段或屏幕键盘)将接收按键代码和元状态。sendKey 还提供了一些快捷方式,这些快捷方式对应于某些常见按键代码。并非所有按键代码都会生成字符,某些特殊按键代码会触发新窗口,例如返回键、应用切换或回到主屏幕。按键代码和元状态可在 Android KeyEvent 类中查看。"sendKey" 直接发送到屏幕,不涉及 ML。因此,在 ATS 设置中,快捷方式映射或按键代码/元状态可能无法使用。
某些快捷方式被用作“全局操作键”,这些键提供了一组不同的功能,并适用于所有环境。
用法:
sendKey(<按键代码>)
sendKey(<按键代码>, <元状态>)
sendKey(<快捷方式>)
参数:
按键代码和元状态:均为整数,而 "快捷方式" 是一个字符串。关于返回键(back key),如果处于 Sigma 输入法模式,单独的“back”按键会发送两个“back”按键,第一个用于关闭输入法,第二个用于关闭窗口。使用 "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 包含按键代码和元状态信息。例如,以下三种方式的结果相同:
sendKey('back')
sendKey(4)
sendKey(tcConst.keyCodes.BACK)
此命令将输入 "A":
>> device.sendAai({action:aaix("sendKey", tcConst.keyCodes.A, tcConst.keyCodes.META_SHIFT_ON)})
{retval: true}
setClipData, getClipData, getClipText 版本 18
这些命令用于获取或设置 Android 剪贴板,数据称为 ClipData。ClipData 支持 4 种主要数据类型:纯文本(Text)、HTML、URI 和 Intent。getClipData 将显示数据及其类型,getClipText 将返回 ClipData 的文本表示,setClipData 接受字符串和类型作为输入。如果未指定类型,则默认使用纯文本。大多数应用会以文本形式返回 ClipData。
数据类型 |
描述 |
常见用途 |
Text |
纯文本,无格式。用于复制简单的字符串,如消息或标签。 |
复制简单字符串,如消息或标签。 |
Html |
使用 HTML 标签的格式化文本。适用于复制带格式的内容,如网页或电子邮件。 |
复制网页或电子邮件中的富文本内容。 |
Uri |
指向资源(文件、图片、网页链接)的引用。适用于复制指向内容或文件的链接。 |
复制文件、图片或内容的引用链接。 |
Intent |
用于触发操作的消息对象(例如打开网页或启动应用)。 |
复制用于打开网页或启动应用的操作。 |
用法:
getClipData()
getClipText()
setClipData(<text>[, <type>])
返回值:
setClipData 成功时返回 true,失败时返回 null。getClipData 成功时返回数据及其类型,失败时返回 null。
示例:
如果粘贴到常规文本字段中,通常会转换为纯文本。但是,在支持 HTML 的文本输入框(如 Gmail)中,长按时会有“粘贴”或“以纯文本粘贴”的选项。选择“粘贴”选项后,内容将在电子邮件中显示为 3 行独立的文本。
>> device.sendAai({action:"setClipData(<br>Line 1<br>Line 2<br>Line3, html)"})
{retval: true}
当从电子邮件输入框(如 Gmail)中复制 3 行文本时,剪贴板会以 HTML 格式存储内容,并包含内联样式。除了基本的 HTML 标签(如 <br>)外,还可能包含额外的 CSS 用于字体、颜色和格式,以保留原始外观,因此 ClipData 可能会更大且更复杂。
>> device.sendAai({action:"getClipData"})
{retval: '<span style="color: ...>Line 3</span>', type: 'html'}
>> device.sendAai({action:"getClipText"})
{retval: '<br>Line 1<br>Line 2<br>Line3'}
URI 和文本示例(暂不支持 Intent 的复制粘贴):
>> device.sendAai({action:"setClipData(http://www.sigma-rt.com, uri)"})
{retval: true}
>> device.sendAai({action:"getClipData()"})
{retval: 'http://www.sigma-rt.com', type: 'uri'}
>> device.sendAai({action:"getClipText"})
{retval: 'http://www.sigma-rt.com'}
>> device.sendAai({action:"setClipData(http://www.sigma-rt.com)"})
{retval: true}
>> device.sendAai({action:"getClipData()"})
{retval: 'http://www.sigma-rt.com', type: 'text'}
>> device.sendAai({action:"getClipText"})
{retval: 'http://www.sigma-rt.com'}
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 |
增加 repeat 命令的最大重复次数 |
action:enhancedParser |
布尔 |
true |
启用解析器支持转义字符 |
action:oldReturnKey |
布尔 |
false |
自版本 18 起,所有 get 命令的返回值都存储在 "retval" 中,启用此选项可恢复到旧的返回格式 |
示例:
>> 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}
sleep
该命令将暂停执行指定的毫秒数,对于需要在操作前等待一定时间的情况非常有用。
用法:
sleep(<time>)
参数:
time: 指定等待的时间,单位为毫秒。
返回:
返回 true
命令参考
操作名称 |
OQ |
参数 |
输入 |
输出 |
ML 变化 |
属性 |
|
获取命令 |
|
getAllPackageNames |
否 |
- |
无 |
retval:[<pname>] |
- |
- |
getBounds |
是 |
- |
所有节点 |
ids:[<id>], bounds:[<bounds>] |
- |
- |
getBoolProp |
是 |
- |
首个节点 |
retval:[<prop>] |
- |
- |
getBoolPropInAncestors |
是 |
- |
首个节点 |
retval:[<prop>] |
- |
- |
getChecked |
是 |
- |
单个节点 所有节点 |
retval:<boolean> retval:[boolean|N/A] |
- |
- |
getClipData |
否 |
- |
无 |
retval:<data>, type:<type> |
- |
- |
getConfig |
否 |
<name(S)> |
无 |
retval:<value> |
- |
- |
getCount |
是 |
- |
所有节点 |
retval:<count> |
- |
- |
getDeviceName |
否 |
- |
无 |
retval:<text> |
- |
- |
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>] |
- |
- |
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 |
newQuery |
否 |
<query>[,<timeout>] |
无 |
retval:<count> |
是 |
- |
reduceNodes |
否 |
- |
所有节点 |
retval:<count> |
是 |
RN |
pop/load |
否 |
- |
无 |
retval:<boolean> |
是 |
- |
push/save |
否 |
- |
所有节点 |
retval:<boolean> |
- |
- |
sort |
否 |
<type(S)> |
所有节点 |
retval:<boolean> |
是 |
ST |
viewGroup |
是 |
[level(I)] |
首个节点 |
retval:<count> |
是 |
VG |
waitQuery |
否 |
<query>[, <timeout>] |
无 |
retval:<boolean> |
- |
- |
|
节点操作命令 |
|
assert | 否 | <CondExp> | 无 | retval:<boolean> | - | - |
click | 是 | - | 首个节点 | retval:<boolean> | - | - |
click2 | 是 | - | 首个节点 | retval:<boolean> | - | - |
echo | 否 | <CondExp> | 首个节点 | retval:<value> | - | - |
filter | 否 | <CondExp> | 所有节点 | retval:<boolean> | - | - |
forEach | 否 | <action|JSON name> | 所有节点 | retval:<name>:[[...]] | - | - |
if | 否 | <CondExp>,<then>,<else> | 首个节点 | retval:<value> | - | - |
log | 否 | <CondExp> | 首个节点 | retval:<boolean> | - | - |
longClick | 是 | - | 首个节点 | retval:<boolean> | - | - |
nsClick | 是 | [<x>,<y>] | 首个节点 | retval:<boolean> | - | - |
refresh | 是 | - | 所有节点 | retval:<boolean> | - | - |
repeat | 否 | <count, action|JSON> | 所有节点 | retval:<name>:[[...]] | - | - |
saveImage | 是 | - | 所有节点 | retval:<text> | - | - |
saveImageInfo | 否 | - | 无 | retval:<boolean> | - | - |
saveImageSettings | 否 | - | 无 | retval:<boolean> | - | - |
scrollIntoView | 是 | <query>[direction(S)] | 无 | retval:<boolean> | 是 | - |
setChecked | 是 | <boolean> | 所有节点 | retval:<boolean> | - | - |
setProgress | 是 | <value(D/I)> | 首个节点 | retval:<boolean> | - | - |
setText | 是 | <text(S)> | 首个节点 | retval:<boolean> | - | - |
showOnScreen | 是 | - | 首个节点 | retval:<boolean> | - | - |
swipeAcross | 是 | <direction>[,<steps>] | 首个节点 | retval:<boolean> | - | - |
until | 是 | <option> <arg> | 无 | retval:<boolean> | - | - |
|
操作命令 |
|
closeApp | 否 | [<name>(S)>] | 无 | retval:<boolean> | - | - |
error | 否 | <message(S)> | 无 | null | - | - |
exec | 否 | <command(S)> | 无 | retval:<text> | - | - |
function | 否 | <name + arg> | 无 | retval:<boolean> | - | - |
funcExists | 否 | <name> | 无 | retval:<boolean> | - | - |
funcDelete | 否 | <name> | 无 | retval:<boolean> | - | - |
funcList | 否 | - | 无 | retval:[<names>]; count:<count> | - | - |
funcReset | 否 | - | 无 | retval:<boolean> | - | - |
listAndroidSettings | 否 | - | 无 | retval:[<settings>]; count:<count> | - | - |
openApp | 否 | <name>(S)> | 无 | retval:<boolean> | - | - |
openAndroidSetting | 否 | <setting(S)> | 无 | retval:<boolean> | - | - |
restartApp | 否 | <name>(S)> | 无 | retval:<boolean> | - | - |
return | 否 | <CondExp> | 无 | retval:<value> | - | - |
saveImageInfo | 否 | - | 无 | retval:<JSON> | - | - |
saveImageSettings | 否 | <JSON> | 无 | retval:<boolean> | - | - |
sendKey | 否 | <code(I)> [meta(I)] | 无 | retval:<boolean> | - | - |
setClipData | 否 | <data>[,<type>] | 无 | retval:<boolean> | - | - |
setConfig | 否 | <name(S)> <value(*)> | 无 | retval:<boolean> | - | - |
sleep | 否 | <duration in ms(I)> | 无 | retval:<boolean> | - | - |
unsetConfig | 否 | <name(S)> | 无 | retval:<boolean> | - | - |
version | 否 | - | 无 | retval:<version number> | - | - |
|
(S) - 字符串, (I) - 整数, (B) - 布尔值, (D) - 双精度/浮点数, (*) - 所有类型
[可选参数] 或 <name>:[<返回数组>]
|
CondExp - 条件表达式
|
device/devices.sendAai({query:<query>, action:<command>})
device/devices.sendAai({query:<query>, actions:[<command>]})
|
支持
请发送电子邮件至 support@sigma-rt.com 以获取支持和反馈。