脚本编写

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 特性):

  • 使用 Mozilla 的 Rhino 1.7.7(适用于 Total Control 7及以下版本),Rhino 1.7.12(适用于 Total Control 8)和 Rhino 1.7.14(适用于 Total Control 9,更新 20)。有关更多信息,请参阅https://github.com/mozilla/rhino
  • RingoJS 2.0。

Rhino 的优势在于可以直接调用 Java API(Total Control 8 使用的是 OpenJDK 15),如果 Rhino + RingoJS 提供的功能不足以满足需求。

Total Control 提供了许多类,以下是一些示例:

  • Device
  • DeviceArray
  • UiElement(Total Control 8)
  • UiElementArray(Total Control 8)
  • Notification
  • Keyboard
  • Excel

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)(Total Control 8+)

AAI 将屏幕上的 UI 元素识别为对象,传统的 (x, y) 坐标方式将屏幕视为一个巨大的对象,因此需要使用图像/颜色查找和光学字符识别 (OCR) 来识别屏幕上的对象。

无障碍功能是一种将屏幕上的 UI 元素表示为底层节点的功能,一个节点包含许多属性,如文本/描述、尺寸、布尔属性(可点击、可编辑或可滚动)、底层类名等。文本/描述可以轻松访问(无需使用 OCR),尺寸(和可点击性)确保按钮可以在特定位置被点击,即使节点被移动到另一个位置也能有效。

一个节点可以表示一个 UI 元素(例如按钮)、一组 UI 元素或某些元素的布局。节点(元素、组或布局)可以通过节点 ID(用十六进制字符串表示)进行标识。我们将无障碍功能、TC 脚本框架和 UI Automator 库进行集成,以实现以下目标:

  • 与坐标无关使脚本在不同分辨率、多种尺寸和品牌的设备上更具可移植性。
  • 同步 API 将等待屏幕重绘,使脚本更简单,无需猜测休眠时间。
  • 可以轻松地从应用程序中检索字符串,而无需使用容易出错的 OCR。

AAI 的最简单案例:

  • 如果找到屏幕上的 "OK",点击它,远比特定分辨率的 click(100, 100) 更好:devices.clickSync("OK")
  • 输入文本到文本输入框,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 项目包括以下内容:

  1. 查询语言,简单的单行语法语言,用于搜索目标节点。AAI 的核心。
  2. FindNode 在每个设备上执行查询或操作。所有的查询和某些操作都在 FindNode 中完成,它包含几十个命令。有关更多信息,请参阅FindNode 文档。
  3. 一对多同步中的对象模式,将节点(或 UI 对象)发送到所有设备,而不是协调,"OK" 的点击可以在具有不同分辨率的所有设备上运行,而不是 click(100,100)。
  4. UI Explorer 用于获取节点信息,可以直观地测试查询语言,是学习和探索工具。
  5. AAIS,一种在多个设备上执行自动化的简单语言。捕获和重放生成此语言,有关更多信息,请参阅AAIS 文档。
  6. REST 和 JS API 包括对 FindNode 的无障碍功能。
  7. UiElement 类位于 FindNode 之上,方便访问节点。


查询

每个查询包含一个或多个 "<key>:<value>" 对,多个键可以使用 "&&" 作为分隔符添加。

每个节点由一个节点 ID 来标识。查询可以分为三个阶段:

  1. 模板("TP")。该类别用于"生成"初始节点。例如,"TP:textInput" 将返回一个可编辑文本字段的列表。此类别是必需的,如果未指定,则将使用默认模板。
  2. 基本查询(BQ)。每个节点包含有关自身的信息,例如类别、文本/描述、属性等。BQ 将逐个匹配节点,拒绝不符合条件的节点,并不会传递到下一个阶段。如果未指定 BQ,则从 TP 生成的节点将传递到 EQ。
  3. 扩展查询(EQ)。一组通常与多个节点一起使用的键。多个 EQ 从左到右执行,同一个键可以指定多次。EQ 的示例:"OX:1" 查找当前节点右侧的元素/节点。

查询执行后,找到的一个或多个节点将列在"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 中的节点匹配(如果提供)以继续执行。

  • P:<package name> - 不应使用,默认为正在运行的应用程序
  • C:<class name> 类名(字符串)
  • R:<resource ID> 资源 ID(字符串)
  • D:<text> 描述(字符串)
  • T:<text> 文本(字符串)
  • IT:<number> 文本输入类型(整数)
  • CC:<number> 子节点数量(整数)
  • ID:<ID> 十六进制格式的节点 ID(字符串)
  • BI:[x, y] 节点包含坐标 (x,y)
  • BI:[x1, y1, x2, y2] 矩形范围内的节点,如果 x 或 y 为 -1,则忽略
  • BP:<prop name> 布尔属性(字符串)
    • checkable、checked、clickable、editable、enabled、focused、longClickable
    • scrollable、visibleToUser。
  • TD:<text> 匹配文本或描述(字符串)


扩展查询(EQ):

这里的查询通常涉及多个节点。

扩展查询的顺序很重要,所有的扩展查询从左到右执行。允许具有相同键的命令。

  • IX:<number> 基于位置从匹配节点列表中获取一个节点
  • OX:<number> 水平偏移到相邻节点(正数表示向右,负数表示向左)
  • OY:<number> 垂直偏移到相邻节点(正数表示向下,负数表示向上)
  • ON:<type> 从匹配节点列表中选择一个节点的不同方法
    • first(第一个)、last(最后一个)、min(最小值)
  • ST:<sort type> 基于节点在屏幕上的位置返回排序后的节点
    • x(按水平位置排序)、y(按垂直位置排序)、yx(按水平和垂直位置排序,或者全部排序)
  • TX 返回与参考节点水平相交的节点。
  • TY 返回与参考节点垂直相交的节点。
  • VG:[level number] 从 ML 中的第一个节点返回一个视图组中的一组节点。
  • RN 从匹配节点列表中返回优化后的节点。
  • BQ:<query> 执行基本查询。
  • X:<key in BQ> 以“X”为前缀的基本查询键

对于 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 用户指南获取完整信息。

后台查询(在 8.0 更新 20 中提供)

后台查询允许用户将查询映射到 JavaScript 回调函数中的设备,当查询条件满足时,它将触发一个带有节点 ID 的回调函数。

例如,如果您想在计算器显示 999 时关闭计算器应用程序。

function closeMe(name, device, packageName, nodeId) {
	device.closeApp(packageName);
	return true;
}
addQueryListener("closeCalculator", device, "T:/^999$/", closeMe);

如果设备没有运行计算器应用程序,需要包含计算器的包名:例如 "P:com.sigma_rt.calc||T:/^999$/"。

"return true" 允许 Total Control 重新激活后台查询,没有 "return true",回调函数只会运行一次。

在计算器应用程序中输入 "999" 来终止计算器。

同步 API

在版本 8 之前,所有的操作和移动命令都是异步的。当一个命令(如 device.click())返回时,意味着该命令已经传递给我们的移动代理进行执行,当命令返回时,很可能命令尚未执行,因此需要使用 sleep 来给予动作完成和屏幕刷新的时间:

device.click(100, 100);
sleep(300);

这种情况可能发生在所有的操作和移动命令中,使得编码变得繁琐。UI Automator 包括同步命令和等待窗口更新的功能,当点击被按下时,窗口已经更新,很可能命令已经完成。所有同步命令的后缀为 "Sync"。所有的 "Sync" 命令都使用 AAI 标签而不是坐标:

runAppSync()

restartAppSync()

clickSync()

inputTextSync()

UiElement 和 UiElementArray 对象的方法

同步功能的一个缺点是命令完成所需的时间比异步命令长得多,这可能会减慢大量设备的脚本执行速度,我们已经解决了这个问题。

随后的版本将为所有的操作和移动函数提供同步功能。

AAI 脚本(AAIS)

我们还开发了一个小型脚本,利用 AAI 的能力进行自动化,目前在 WDM 和 MDCC 中可用(文件扩展名为 ".tst"),捕获和回放(带有对象选项)将生成该脚本。它还提供与 JavaScript 的无缝集成(包含在 "{}" 中)。

例如:

exec "teslaLib.js"
open "Tesla"
find "T:VIN:"
get "T:VIN:&&OX:1", "text"
{ saveVar("vin", getOutput().retval) }
get "T:/[0-9,]+ miles/", "text"
{ saveMileage(new Date(), loadVar("vin"), getOutput().retval) }
print "Done"

此脚本可以同时在多个设备上运行,可在任何屏幕尺寸上运行,"find" 命令会滚动屏幕,直到找到满足查询条件的元素。任何一行的失败都会停止脚本的执行。它提供了 AAIS 和 JavaScript API 的命令。

另一个示例:

{
    var apps = ["Skype", "Whatsapp", "Telegram"];
    var arguments = getArg();
    var app = apps[0];
    if (arguments.length > 0) {
        var appNum;
        if(isNaN(appNum = parseInt(arguments[0]))) {
            throw "Need a number;"
        }
        if (appNum < 1 || appNum > apps.length) {
            throw "Option out of range";
        }
        app = apps[appNum-1];
    }
}
open "${app}"
print "${app} opened"

请阅读 AAIS 用户指南 以获取有关 AAIS 的更多信息。