FindNode

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

FindNode 的关键组件:

  1. 查询语言:一种简单而强大的语法,专为精确识别节点而设计。
  2. 核心技术:FindNode 与每台设备无缝集成,由 Selector 核心驱动。
  3. 对象模式:增强多设备间的同步性,使查询和操作能够在不同屏幕分辨率下统一进行。
  4. UI Explorer:学习查询语言的必备工具,提供节点属性的深入解析,并帮助用户构建查询。
  5. AAIS:一种简单易用的脚本语言,适用于 AAI 生态系统中的多设备自动化。
  6. 集成:可与现有的 REST 和 JavaScript API 互补,通过 "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

操作分为两种类型:

  • 作用于节点(ML)的操作。
  • 独立于节点的操作。

对于需要单个节点的操作(例如“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)用于 ifassertfilter 等命令,以决定执行流程。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(匹配列表),主要用于多步骤命令。例如,可以 pushsave 现有的 ML,执行 newQuery 和其他操作后,再使用 popload 恢复之前的 ML 继续操作。

save/load 主要用于兼容性,因为它们没有存储 ML 的数量限制。实际上,在实现中,savepush 作用相同,loadpop 作用相同。

用法:

  • push
  • pop
  • save
  • load

返回值:

始终返回 retvaltrue,如果栈为空且尝试 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)

返回值:

返回 retvaltruefalse

另见: 查询 "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: 执行点击但不监视屏幕事件。

"nsClick" 还接受两个整数,如果使用硬编码的 x 和 y 值,它将在 x 和 y 坐标上执行点击,但脚本在新设备、分辨率或更新的应用程序上可能无法使用。某些使用本机界面的应用程序,一些节点可以找到(例如 WebView),可以利用已找到的节点计算缺失节点的位置并执行点击。使用 "getBounds" 或 "getNodes(B)" 获取节点的尺寸(或边界)。

用法:

  • click / nsClick / click2 / longClick [(<query>)]
  • nsClick(<x>, <y>) 版本 17

返回值:

返回 truefalse,表示操作是否成功。

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)

echolog 的用法完全相同,echoretval 中显示输出,而 log 将输出记录在 logcat 中。它们接受以下类型:

  • 值:布尔值、整数、浮点数、字符串、函数、数组和对象。
  • 函数调用:调用函数并返回其值。
  • 比较表达式:如果使用比较运算符,则返回 truefalse
  • 对于字符串,如果字符串包含有效的算术表达式,则会执行计算并输出结果。
  • 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)

ifassert 命令可以使用条件表达式来判断 truefalse(请参阅 Action 部分中的 "Conditional Expression")。

  • 对于这两个命令,都需要条件表达式来判断 truefalse。请参考 ACTIONS 部分中的 CondExp 章节。
  • 对于 if,如果条件为 true,则执行 then,如果为 false,则执行 else。如果 thenelse 设为 null,则不会执行任何操作。如果 else 未指定或设为 null,返回值为 {retval: true}
  • 如果 ML(匹配列表)中有多个节点,ifassert 仅使用 ML 中的第一个节点。
  • 对于 assert,如果条件为 true,返回 {retval: true},如果为 false,会引发错误,并将错误传播到 sendAai(),随后使用 RingoJS 的 assert.fail() 生成 AssertionError。在测试环境中,AssertionError 会导致测试失败。
  • if 语句可以包含 return 以退出函数。

用法:

  • if(<CondExp>, <then>, <else>)
  • assert(<CondExp>[, <message>])

返回值:

返回 thenelse 的结果。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}

创建一个函数,返回 truefalse,判断是否需要充电:

>> 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,可使用 setConfigaction: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 以获取支持和反馈。


TCHelp