脚本编写
Total Control 最大的优势之一就是其脚本编写功能。Total Control 提供了丰富的 API,可以控制一个或多个 Android 设备。它提供了两套 API:JavaScript 和 REST API。JavaScript 可以在本地运行脚本,REST API 则提供了语言和主机的灵活性。您还可以使用 REST API 来控制运行 "Total Control" 应用程序的多台计算机。
JavaScript API
语言
Total Control 提供以下 JavaScript 框架(ECMAScript 5,包括一些 ES6 特性):
Rhino 的优势在于可以直接调用 Java API(Total Control 8 使用的是 OpenJDK 15),如果 Rhino + RingoJS 提供的功能不足以满足需求。
Total Control 提供了许多类,以下是一些示例:
Device 和 DeviceArray 类
当设备连接时(自动连接或按下“连接”按钮),将创建一个来自“Device”类的设备对象,使用静态方法“searchObject”来定位设备对象。对于多个设备对象,它提供了“DeviceArray”类(从 Array 继承的子类)。DeviceArray 对象中的设备将同时执行相同的任务。
Device 类提供近 100 个方法和属性来操作设备。DeviceArray 类提供了大约 20 个常用方法。请参考 {{JavaScript API 文档}} 查看所有方法。您可以轻松扩展 Device 和 DeviceArray(请参阅扩展 Device 和 DeviceArray)。
使用“Device.searchObject()”来定位创建的设备对象,设备对象的格式为“device@<10位数字>”。
// 返回一个设备对象
var mySamsung = Device.searchObject('Samsung-S9');
// 返回所有设备对象,DeviceArray
var allDevices = Device.searchObject(tcConst.DevAll);
// 返回属于同一组的设备对象,DeviceArray
var allGroupX = Device.searchObject(tcConst.DevGroup, 'first row');
var allGroupY = Device.searchObject(tcConst.DevGroup, 'second row');
// 将两个组合成一个更大的组,DeviceArray
var groupXY = allGroupX.concat(allGroupY);
由于 DeviceArray() 是 Array() 的子类,它继承了 Array 类的大部分方法,由于所有 TC 特定的方法都与 DeviceArray 绑定,如果你有一个数组,请使用“var ary = new DeviceArray().concat(ary)”将其转换为 DeviceArray。
// 这会失败,因为 Array 中没有“click”方法
vardevice=Device.getMain();
varary=[device];
ary.click(100,100);
//Thiswillwork
vardevice=Device.getMain();
varary=newDeviceArray(device);
ary.click(100,100); // or ary.clickSync("OK");
以下是一些示例:
// 点击设备名称为 "Samsung-S10" 的设备上的位置 (100,200)。
var device = Device.searchObject('Samsung-S10');
if (device) {
device.click(100,200); // or device.clickSync("Start");
}
var devices = Device.searchObject(tcConst.DevAll);
if (devices) {
devices.click(0.5, 0.5); // or device.clickSync("John");
}
目录和 Userlib.js
默认情况下,脚本目录位于 \Users\<用户名>\Documents\Scripts 目录下,您可以通过在主窗口中点击 "Script",选择 "Script List",并在顶部行更改目录。
在该目录下,您可以创建一个名为 "Userlib.js" 的文件,该文件将在脚本执行之前始终加载,您可以在其中包含常用函数、添加现有类的原型或引入第三方软件。
要调试 "Userlib.js",请使用 "Terminal",要重新加载 "Userlib.js",请点击底部右侧终端窗口的重新加载图标。
绝对坐标与相对坐标
对于 x、y 坐标,TC 提供了 "绝对" 和 "相对" 坐标(系统设置中显示的坐标会同时显示两种坐标),绝对坐标从 (0, 0) 到 (width – 1, height – 1),相对坐标通常是四位小数,从 (0, 0) 到 (0.9999, 0.9999)。将相对坐标乘以设备的宽度和高度将得到绝对坐标。坐标并非完美无缺,专业版支持 AAI,它使用 UI 元素中的文本来在运行时检索坐标。
扩展设备和设备阵列
JavaScript 的额外好处是可扩展性,您可以通过向类添加原型来扩展方法。假设您要为单个和多个设备创建一个名为"longPress"的方法,一种编写方法的方式如下:
Device.prototype.longPress=function(x,y){
varretval=this.click(x,y,tcConst.STATE_DOWN);
if(retval!=0){
returnretval;
}
delay(500);
returnthis.click(x,y,tcConst.STATE_UP);
}
DeviceArray.prototype.longPress=function(x,y){
for(leti=0;i<this.length;++i){
retval=this[i].longPress(x,y);
if(retval!=0){
returnretval;
}
}
return 0;
}
这个实现对于包含大量设备的 DeviceArray 效率不高,如果阵列中有 100 个设备,该方法可能需要长达 50 秒。
更好的方法是先向所有设备发送 STATE_DOWN,然后等待剩余的 500 毫秒。幸运的是,Device 和 DeviceArray(多个设备)引入了"sendAll"方法,它执行指定的方法并等待指定的时间。这样,执行时间几乎为 500 毫秒。由于 "sendAll" 在 Device 和 DeviceArray 中都可用,因此 Device 和 DeviceArray 的代码实现完全相同。
Device.prototype.longPress=function(x,y){
varretval=this.sendAll(Device.prototype.click,
[x,y,tcConst.STATE_DOWN],500);
if(retval!=0){
returnretval;
}
returnthis.sendAll(Device.prototype.click,[x,y,tcConst.STATE_UP]);
}
DeviceArray.prototype.longPress=Device.prototype.longPress;
"sendAll" 方法仅适用于成功返回 0 的方法。
脚本工具
Total Control 提供许多工具,点击 "Script" 将打开一个新窗口:
Terminal(终端):打开 Rhino + RingoJS 命令提示符,您可以在此进行测试或开发。点击右下方的按钮重新加载解释器和 Userlib.js。可以使用 load("filename.js") 来运行脚本。
脚本列表 ⇒ 路径:脚本默认路径,点击图标可更改为其他目录。
脚本列表 ⇒ JS 源文件:用于快速执行 JavaScript 文件,提供了一个简单的编辑器,可以快速修改或点击箭头键来执行脚本。
脚本列表 ⇒ 录制脚本:将设备上的操作记录到 Excel 文件中,Excel 文件还可以用于生成 JS 脚本或 JSON 文件(对于 REST API 可能有用)。没有 JavaScript 经验的用户可以使用此工具生成简单的脚本。
脚本列表 ⇒ 图片助手:从主设备屏幕内容生成 BMP 文件,该图像文件用于 seekImage(),它扩展了 BMP 文件以提供其他信息,如应用程序名称、活动、宽度和高度信息。seekImage() 将利用这些信息而无需提供大量参数。
脚本列表 ⇒ 颜色助手:从主设备加载屏幕内容(类似于图像助手),使用放大的颜色选择器选择颜色的 RGB 值,并生成 seekColor(),具有复杂参数。可以支持单个和多个颜色。
脚本列表 ⇒ UI 探测:打开 UI 探测窗口,点击截屏按钮捕获现有的主手机窗口,并允许用户使用查询语言选择 UI 元素。命令助手 窗口将为查询语言中的每个键提供帮助描述。
执行器(将更名为任务):创建一个任务,在各种条件下运行脚本,例如日期和时间、迭代次数、常规间隔或设备。任务执行的输出和结果将保存供查看。任务也可以通过 JS API 创建或更改。Runner 的执行将显示为任务。
检查:检查与脚本和引擎相关的各种内部变量。
AAI 将屏幕上的 UI 元素识别为对象,传统的 (x, y) 坐标方式将屏幕视为一个巨大的对象,因此需要使用图像/颜色查找和光学字符识别 (OCR) 来识别屏幕上的对象。
无障碍功能是一种将屏幕上的 UI 元素表示为底层节点的功能,一个节点包含许多属性,如文本/描述、尺寸、布尔属性(可点击、可编辑或可滚动)、底层类名等。文本/描述可以轻松访问(无需使用 OCR),尺寸(和可点击性)确保按钮可以在特定位置被点击,即使节点被移动到另一个位置也能有效。
一个节点可以表示一个 UI 元素(例如按钮)、一组 UI 元素或某些元素的布局。节点(元素、组或布局)可以通过节点 ID(用十六进制字符串表示)进行标识。我们将无障碍功能、TC 脚本框架和 UI Automator 库进行集成,以实现以下目标:
AAI 的最简单案例:
devices.inputTextSync([位置], "文本") // 输入文本,位置用于多个输入
devices.runAppSync(<包名>, [查询])
devices.restartAppSync(<包名>, [查询])
整个屏幕由许多节点组成,一个节点可以是最小的 UI 元素或许多节点的容器,有些节点是不可见的。整个屏幕是从单个根节点开始的树状结构。根据应用程序的复杂程度,一个屏幕可能包含50-300个节点。
由于用户只对节点的一个小子集感兴趣,挑战在于找到用户想要的正确节点并从中提取信息或执行操作。
如何找到节点是一个挑战?我们发明了一种查询语言来查找节点,FindNode 程序安装在每个设备上,查询语言将被执行以获取满足条件的节点,意图是将大量节点减少为一个或少数几个目标节点,用户可以获取信息或对节点应用操作。
例如:Java 中的 UI Automator 提供了 "UiSelector" 和 "BySelector" 在 UiDevice.findObject() 或 findObjects() 中定位节点,对于多个条件可能会很复杂:
new UiSelector().className("android.widget.TextView").text("OK")
我们创建了一个简单的查询语言,它更短且可移植,因为查询将发送到许多设备,上述代码可以用我们的查询语言重写为:
"C: android.widget.TextView&&T:OK"
AAI 项目包括以下内容:
查询
每个查询包含一个或多个 "<key>:<value>" 对,多个键可以使用 "&&" 作为分隔符添加。
每个节点由一个节点 ID 来标识。查询可以分为三个阶段:
查询执行后,找到的一个或多个节点将列在"ML"(匹配列表)中,可以在 ML 上应用一系列操作,这些操作可以是获取信息或对 ML 执行操作。
模板:
为 BQ 或 EQ 生成初始节点。
TP:all -所有节点
TP:more -除了以"Layout"结尾的节点外的所有节点
TP:basic -所有叶子节点(子节点数为零)
TP:reduced -优化"TP:more",返回屏幕上重要的节点
TP:anyText[,<min>[,<max>]] -具有特定长度的"text"内容的节点。
TP:anyDescription[,<min>[,<max>]] -具有特定长度的"description"内容的节点。
TP:textInput -从左上到右下排序的所有可编辑字段。
TP:findText,<text> -具有参数中的文本的节点,可以包含 "*" 和 "/…/"。
TP:line,top|bottom,<number> -返回位于可滚动节点之外的前/后节点。
TP:scrollable,<position> -滚动容器内的节点,对于多个可滚动节点,使用位置参数。
基本查询(BQ):
用于获取节点级别信息的查询,从 TP 中的每个节点将与 BQ 中的节点匹配(如果提供)以继续执行。
扩展查询(EQ):
这里的查询通常涉及多个节点。
扩展查询的顺序很重要,所有的扩展查询从左到右执行。允许具有相同键的命令。
对于 BQ,查询语法可以包含 "!" 表示非,">" 和 "<" 表示大于和小于,"*" 表示通配符匹配,"/<regexp>/" 表示正则表达式。它可以匹配包名、类名、资源ID、文本、描述、子节点数和输入类型。
FindNode 已安装在每个设备上(作为 Total Control 应用的一部分),它是唯一能识别查询语法的程序,它解析查询、定位节点并对找到的节点执行操作。FindNode 将复杂的 JavaScript 和 Total Control 的 CPU 利用率卸载到设备上,所有的搜索都在设备上进行。
device.sendAAi() 和 devices.sendAai() 是与 FindNode 直接通信的方式,可以向一个或多个设备发送 JS 对象,发送前会将其转换为 JSON 格式,返回值以 JS 对象格式返回。如果遇到错误,返回值为 null,lastError() 中包含错误消息。
一个简单的查询示例,用于获取型号名称的文本,使用 X 偏移 1(右侧):
>> device.sendAai({query:"T:Model name&&OX:1", action:"getText"})
{retval: 'Galaxy S10+'}
FindNode 甚至可以检测屏幕顶部/底部的固定图标:
>> device.sendAai({query:"TP:line,bottom,-1", action:"getText"})
{retval: ['Chats','Calls','Contacts','Notifications']}
以下 3 个命令都可以点击 "Calls" 文本:
>> device.sendAai({query:"TP:line,bottom,-1&&T:Calls", action:"click"})
{retval: true}
>> device.sendAai({query:"TP:line,bottom,-1&&IX:1", action:"click"})
{retval: true}
>> device.sendAai({query:"TP:line,bottom,-1&&T:Chats&&OX:1", action:"click"})
{retval: true}
点击 "Contacts" 图标:
>> device.sendAai({query:"TP:line,bottom,-1&&T:Contacts&&OY:-1", action:"click"})
{retval: true}
>> device.sendAai({query:"TP:line,bottom,-1&&IX:2", action:"click"})
{retval: true}
// 在屏幕上找到多个 "Contacts",IX:-1 是选择最后找到的节点
>> device.sendAai({query:"T:Contacts&&IX:-1&&OY:-1", action:"click"})
{retval: true}
请阅读 FindNode 用户指南获取完整信息。