官网文档:http://doc.autoxjs.com/#/documentation
autojs 教程、demo 集合:https://github.com/wiatingpub/autojs
autojs app 自带的教程,看完就差不多了
关键字:安卓自动化、手游辅助
1、官网文档
Auto.js 简介
Auto.js 本身是一个 app,但是可以用来开发脚本,开发的脚本还可以打包成 apk 进行安装。
autojs 有什么优点?为什么推荐?
- 无需 root 而是基于无障碍服务。为啥强调无需 root,因为 root 刷机本身是存在风险的,无需 root 就代表着远离风险。剩下的就是需要一台安卓手机,然后懂点 js 就可以了。
- 上手简单,通过学习 Auto.js 内置的教程,就能写出很多有趣的脚本。这是因为 Auto.js 虽然依托于 JS 语言,却做了很多封装。
- 支持打包,可以将脚本打包成 apk 文件,这一点对不爱折腾的小伙伴来说,简直是福音。
Auto.js 由 hyb1996 于 2017/01/27 初次发布,于 2020/03/13 停止维护,最终版本名称为 4.1.1 Alpha2,构建版本号为 461。autojs 这么屌为什么会被下线,因为 Auto.js 被大面积的用于灰产,被某些资本警告,导致该工具无法对某些应用进行操作 ( 例如:微信、支付宝、抖音 等 )。4.1 版本是免费版的最后一个版本,可以在所有 app 上进行操作。后来作者推出收费版的 Auto.js Pro,虽说增加了一些功能,但是屏蔽了很多主流应用,属于阉割版。
- 4.1版本 功能与 pro 几乎无差别,下载地址:https://www.wuyunai.com/docs/
- Autox.js 是在原 4.1版本 基础上 fork 而来,可操作所有软件,并拥有大量 autojs pro 的功能:https://github.com/kkevsekk1/AutoX
基于 Auto.js 二次开发的开源项目 (仅部分列举):
项目名称 | 应用名称 | 开发者 | 开发时间 |
---|---|---|---|
Auto.js | Auto.js M | TonyJiangWJ | 2019/11/21 |
AutoX | Autox.js / Autox.js v6 | kkevsekk1 | 2020/07/24 |
AutoJs6 | AutoJs6 | SuperMonster003 | 2021/12/01 |
其他类似软件
- 一触即发:http://www.yicuba.com/index.html
蓝奏网盘:https://bgg.lanzoui.com/b02bsrsah - 点击助手Pro:https://www.ghxi.com/djzs.html
- Hamibot:类似 autojs 的自动化工具,可以通过浏览器远程控制。适用于安卓系统的自动化工具,能全自动操控任意 APP。hamibot 需要注册账号:https://hamibot.com/
Hamibot 连接手机:https://www.i3zh.com/22556.html - 冰狐智能辅助 (类似 Autojs,比 autojs 简单,需要注册账号):https://aznfz.com/
- Ctrl.js 开发文档:https://ctrljs.ikaiwei.com/ctrljsapi/#/
Ctrl.js ( 类似auto.js ):http://www.feiyunjs.com/2266.html - EasyClick 和Auto.js区别:http://www.feiyunjs.com/2456.html
EasyClick 开发文档:http://ecdoc.laoleng.vip/docs/
AutoX.js 的功能
- AutoX.js 项目工程化:结合 webpack vscode 插件,开发、编译、打包、部署、混淆、加密一体化 文档资料
- vscode 插件右键,自动提示操作等 下载地址
- vscode 自动补全、方法注释等 文档资料
- 修复众多 bug,升级到 5.0.1 ,合并打包插件,升级配置文件等功能
- 建设论坛, 提供 交流社区
- 支持 WebSocket
安装 AutoX.js
- 手机安装 AutoX.js 应用
- 开启手机无障碍服务,目的是为了让脚本能执行。
- 开启悬浮框(为了查看控件信息)
配置 开发 环境
AutoX.js 使用 JavaScript 作为脚本语言,目前使用 Rhino 1.7.13 作为脚本引擎,支持 ES5 与部分 ES6 特性。
- 学习 AutoX.js 的 API 之前,建议先学习 JavaScript 的基本语法。
- 如果想要在电脑上开发 AutoX.js,可以使用 VSCode 以及 AutoX.js 插件。
- 如果想要使用 TypeScript 来开发,目前有开发者公布了一个 相关工具。
IDEA
在安卓手机上安装 Auto.js app
在 IDEA 安装插件 autojsx.WIFI。地址:https://github.com/zimoyin/Autojsx.WIFI
在 Pycharm 连接到 Auto.js app 的插件服务器,确保设备和 IDEA 之间的双向通信
使用 IDEA 推荐使用专业版,在 IDEA 的插件中心找到,搜索名称为 Autojsx.WIFI
创建项目,有两种方式,分别为 SDK项目 和 项目组
- 在 Autojsx 项目内右键项目可以创建子项目。它会创建一个 autojs 原生编写方式项目。在 SDK和项目组都可以使用。
子项目的文件结构:
resources:插件的资源
lib:存放开发时的sdk用于api提示,其他js也可以放在这里,但是不要放进sdk文件夹
src: 存储js代码
他们在运行和上传项目时会打包到一起,如果你在src想引起其他两个文件夹的资源,请指定路径为根路径(./test.text)而不是带着文件夹名称路径(./resources/test.text) - 关于控制台,位置在 IDEA 新UI的左侧,旧 UI 的下侧。
- 如何运行项目。
点击右上角运行按钮(新UI在run菜单下) 或者 右键你打开的脚本 或者 在Autojs菜单里面找运行项目 或者 控制台中单击运行项目。运行项目前你需要打开服务器,如果没有打开当你点击运行的时候会默认打开,默认端口为 9317 是Autojsx 的默认端口,如果你没填那么就是这个
通过 Tool -> Autojs -> 修改端口/修改IP 可以更改 - 如何上传运行: 右键文件夹、右键你打开的脚本、在Autojs菜单里面找保存
VSCode
Auto.js-Autox.js-VSCodeExt:https://marketplace.visualstudio.com/items?itemName=aaroncheng.auto-js-vsce-fixed
这里直接使用 VSCode 和 AutoX.js 插件进行开发。安装好 VSCode 和 AutoX.js 插件后,VSCode 打开 命令面板 ( Ctrl + shift + p ),输入 autoxjs 即可看到 插件支持的命令
找到 start server 点击即可启动服务。然后手机上打开悬浮窗并连接电脑。至此环境配置成功,现在就可以在 vscode 上编写并运行,手机就会相应执行脚本。也可以直接在手机的 Auto.js 应用中直接码代码,不过那酸爽谁用谁知道。。。
示例脚本:
示例:
// 购物车按钮的id
const carId = id('com.yaya.zone:id/rl_car_layout');
// 控制台与手机弹窗输出 toastLog autox.js 全局函数,请查看官方文档
//控件是否存在
toastLog('控件是否存在:'+carId.exists())
//搜索到唯一元素并点击
carId.findOnce().click()
使用如下代码可以直接跳转到APP内的某个页面
// 这样启动没有广告,有点android开发经验,贼爽
app.startActivity({
action: "android.intent.action.VIEW",
className: "com.yaya.zone.home.HomeActivity",
packageName: "com.yaya.zone",
});
className 与 packageName 查看方法:MT管理器 -> 左上角菜单->Activity记录
Node.js(第二代API)对比 Rhino(第一代API)的优势是:
- Node.js引擎的JavaScript执行性能是Rhino的100倍以上
- 使用Node.js引擎的代码加密强度高,目前不能被还原
- Node.js支持ES2021以上语言标准,Rhino仅支持ES5和部分ES6特性
- Node.js引擎本身的Bug基本很少,而Rhino引擎的模块系统、语言实现本身有不少Bug
- Node.js对应的第二代API设计较好、更加标准
- 可以使用第三方npm包
- Node.js的网络资料较多
Node.js(第二代API)对比 Rhino(第一代API)的劣势是:
- Node.js对应的第二代API上手门槛较高,需要对Promise、异步有一定了解,尤其对新手来说
- 第二代API的文档阅读较难,并且目前正在完善中
- Rhino和第一代API的社区的源码、资料、示例较多
- 第一代API使用上比较方便
如何 选择 引擎
- 如果你是没有编程基础的新手,并且不想深入学习编程,代码能跑就行,不追求可维护性、可读性,不追求最新语言标准,能容忍引擎、API设计本身有不符合标准的地方和bug,那么可以使用Rhino引擎和第一代API,上手较快。无需特别配置,代码默认都以该引擎执行。
- 如果追求性能和深入了解,则使用 Node.js 引擎和第二代API,对Rhino引擎和第一代API了解即可。
引擎的选择并非绝对,可以一边使用Rhino引擎一边使用Node.js引擎,或者在学习一段时间后再看另一个引擎/API。
打包成 apk
使用 autox.js 打包成 app,打包的软件报毒,这个问题是因为打包后的app用了 autojs 作为包名,需要用MT管理器把apk 的xml中的autojs字符串换成任意其他值就可以了。
autojs 打包的软件报毒:https://zhuanlan.zhihu.com/p/425140138
权限 设置
示例:申请截图代码
if (!requestScreenCapture()) {
toastLog("请求截图权限 失败");
} else {
toastLog("请求截图权限 成功");
}
执行以上代码后, 正常应该弹出一个框, 让用户选择是否允许截图。但是有的时候, 不会弹出这个界面,这个时候就要去 设置-->应用管理-->AutoJsPro-->权限管理-->后台弹出界面-->允许
悬浮窗权限。
判断是否有悬浮窗权限的代码
floaty.checkPermission();
返回一个布尔值, true就是有权限, false就是没有权限
如果app没有悬浮窗权限,提示用户,然后跳转悬浮窗权限设置界面, 方便用户勾选
跳转悬浮窗权限设置界面代码
app.startActivity({
packageName: "com.android.settings",
className: "com.android.settings.Settings$AppDrawOverlaySettingsActivity",
data: "package:" + context.getPackageName(),
});
后台自启权限,跳转启动管理页面的代码
let intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
let pkg = "com.huawei.systemmanager";
let cls = "com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity";
let componentName = new android.content.ComponentName(pkg, cls);
intent.setComponent(componentName);
context.startActivity(intent);
常见问题
如何定时运行脚本
- 点击脚本右边的菜单按钮->更多->定时任务即可定时运行脚本,但是必须保持Auto.js后台运行(自启动白名单、电源管理白名单等)。同时,可以在脚本的开头使用
device.wakeUp()
来唤醒屏幕;但是,Auto.js没有解锁屏幕的功能,因此难以在有锁屏密码的设备上达到效果。
定时任务如何获取外部参数
- 如果一个脚本是用intent"启动"的,比如定时任务中的特定事件(网络状态变化等)触发而启动的,则可以通过
engines.myEngine().execArgv.intent
获取启动的intent,从而获取外部参数。
如何把图片和脚本一起打包,或者打包多个脚本
- 如果除了单脚本以外还有其他脚本、图片、音乐等资源一起打包,则需要使用项目功能。
点击Auto.js的"+"号,选择项目,填写项目名称、包名等信息以后,点击"√"即可新建一个项目。可以在项目中放多个脚本、模块、资源文件,点击项目工具栏的apk打包图标即可打包一个项目,点击工具栏可以重新配置项目。
例如,主脚本要读取同一文件夹下的图片1.png,再执行找图,则可以通过images.read("./1.png")来读取,其中"./1.png"表示同一目录1.png图片;ui中的图片控件要引用同一文件夹的2.png图片则为<img src="file://2.png"/>。Auto.js内置的函数和模块都支持相对路径,但是,其他情况则需要使用files.path()函数来把相对路径转换为绝对路径。
如何使打包的应用不显示主界面
- 需要使用项目功能。新建项目后,修改项目下的project.json文件,增加以下条目:
"launchConfig": {
"hideLogs": true
}
例如:
{
"name": "项目名称",
"versionName": "1.0.0",
"versionCode": 1,
"packageName": "org.autojs.example",
"main": "main.js",
"launchConfig": {
"hideLogs": true
}
}
"launchConfig"表示启动配置,"hideLogs"表示隐藏日志。
参见项目与项目配置。
Auto.js自带的模块和函数中没有的功能如何实现
由于Auto.js支持直接调用Android的API,对于Auto.js没有内置的函数,可以直接通过修改Android代码为JavaScript代码实现。调用 Android 和 Java 的 API 参见 Work with Java。
例如:旋转图片的Android代码为:
import android.graphics.Bitmap;
import android.graphics.Matrix;
public static Bitmap rotate(final Bitmap src,
final int degrees,
final float px,
final float py) {
if (degrees == 0) return src;
Matrix matrix = new Matrix();
matrix.setRotate(degrees, px, py);
Bitmap ret = Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, true);
return ret;
}
转换为JavaScript的代码后为:
importClass(android.graphics.Bitmap);
importClass(android.graphics.Matrix);
function rotate(src, degrees, px, py){
if (degrees == 0) return src;
var matrix = new Matrix();
matrix.setRotate(degrees, px, py);
var ret = Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, true);
return ret;
}
autojs 常用功能整理
:https://github.com/snailuncle/autojsCommonFunctions/blob/master/autojsCommonFunctions.js
// //导入模块
// function 导入常用函数模块(){
// var url='https://raw.githubusercontent.com/snailuncle/autojsCommonFunctions/master/autojsCommonFunctions.js'
// var r = http.get(url)
// log("code = " + r.statusCode);
// var html=r.body.bytes()
// files.write('./autojsCommonFunctions.js','')
// files.writeBytes('./autojsCommonFunctions.js',html)
// var common=require('./autojsCommonFunctions.js')
// return common
// }
// var common=导入常用函数模块()
// log(common)
// for(let i=0;i<33;i++){
// common.闪光弹('fire in the hole')
// }
let func_list = [
'点击控件',
'铃声',
'启动app',
'停止app',
'卸载app',
'卸载app没root',
'清除app数据',
'启动最新安装的app',
'停止最新安装的app',
'卸载最新安装的app',
'清除最新安装的app数据',
'静默安装app',
'获取app图标',
'控制app联网',
'获取手机上所有的app名字',
'点击输入框弹出输入法',
'使所有输入框点击时都能弹出输入法',
'失去焦点',
'是否root',
'获取指定应用的版本号',
'打开qq群名片',
'打开qq名片',
'qq强制聊天',
'字节变为gbk中文',
'最新安装的app',
'文件修改时间',
'文件大小',
'字符串变字节',
'日期加N天',
'md5',
'是横屏还是竖屏',
'截图',
'随机字符',
'获取时间',
'调整手机音量',
'微信扫一扫',
'公共字符串',
'网络',
'安卓intent源码',
'获取手机ip地理位置',
'替换系统文件',
'编辑距离',
'数组交集',
'提取包含关键字的app',
'获取页面所有文字',
'悬浮控制',
'闪光弹',
'打开开发者选项',
'气泡',
'随机字符串',
'wifi状态',
'开关飞行模式',
'上滑',
'获取deflate网页内容',
'获取gzip网页内容',
'发送通知',
'去除通知',
'clickAttr',
'pressAttr',
'获取拼音',
'画出控件区域',
'获取多开分身右侧字母区域控件',
'画矩形',
'画点',
'strip', // 去除头尾空格
'大数组包含小数组',
'getObjType',
'deepCopy', // 深拷贝
'反色',
'bmob上传文件',
'bmob下载文件',
'过微信QQ滑块',
'确保有jar文件',
'获取QQ收藏内容',
'获取多开分身右侧字母区域指定字母的位置',
'模拟真人滑动',
'画手势',
]
示例:领取淘宝喵币
打开淘宝 ---> 点击领喵币按钮
为了编写脚本简单,淘宝预先打开喵铺主页
auto.waitFor()
let app_name = "微信";
launchApp(app_name);
sleep(10000);
// 根据 "文字信息" 寻找按钮, 并点击
let btn_1 = text("用短信验证码登录").findOnce();
if (btn_1) {
toast("定位到 微信");
btn_1.click();
sleep(1000);
text("获取验证码").findOnce().click();
}
else {
toast("未检测到验证码按钮");
//中止脚本
exit();
}
详解:
- Auto.js无需root,但是需要对该应用开启无障碍模式,开启后才可以进行屏幕点击等操作auto.waitFor()表示直到检查该应用开启无障碍后才执行其后面的代码,否则一直卡在这里,一般放到脚本的第一行
- launchApp()可以打开对应应用,由于不同手机响应速度不同,本代码让它睡眠3s。
- 定位组件是autojs的最常见的操作,这很类似前端的定位dom元素。在任何点击之前都需要找到对应的组件,这不同于点击某像素位置,点击组件更能适配不同分辨率的手机。在auto.js中通过各种条件选取到的控件称为UiSelector ( https://hyb1996.github.io/AutoJs-Docs/#/widgetsBasedAutomation?id=uiselector )。那么筛选条件是如何确定呢?打开Auto.js应用的悬浮窗,在喵铺主页,点击Auto.js悬浮窗后选择出现的蓝色按钮,点击布局范围分析后选择领喵币按钮查看控件信息,你就能看到如图2所示的信息。
- click() 表示点击该元素。
- toast() 表示展示一个消息框。
2. 点击去进店/去浏览
//开始执行任务
execTask();
function execTask() {
while(true) {
var target = text("去进店").findOnce() || text("去浏览").findOnce();
if (target == null) {
toast("任务完成");
break;
}
target.click();
sleep(3000);
//浏览网页20s
viewWeb(20);
back();
sleep(1000);
}
}
viewWeb 是一会要写的函数,目的是模拟浏览网页20s的操作,虽说淘宝要求15s就行了,但是可能部分手机加载耗时比较多,所以多写了5s。当判断任务栏有"去进店"、"去浏览"的组件时,点击跳转至浏览广告,浏览完毕后,返回至任务栏页面,循环执行该操作直到找不到"去进店"、"去浏览"的组件结束(任务完成后按钮文字会变成"已完成")。
浏览广告
function viewWeb(time) {
gesture(1000, [300, 600], [300, 300]);
var cnt = 1;
while(true) {
var finish = desc("任务完成").exists() || textStartsWith("已获得").exists();
if (finish || cnt > time) {
break;
}
sleep(1000);
cnt += 1;
}
//模拟返回键,返回到任务栏页面
back();
}
函数的参数为当前页面的最大停留时间(防止意外而一直停留该页面)。
浏览广告完成的标志:
- 当前页面出现"任务完成"或"已获得*****"的组件
- 位于当前页面的时间大于所设定的最大限制
gesture 是指屏幕滑动操作,这是本文唯一涉及屏幕像素的语句。gesture(duration, [x1, y1], [x2, y2]表示用duration的时间,从(x1,y1)点滑到(x2,y2)点,代码中表示如图用1s从黄点滑向红点,故是上滑操作浏览广告。
本次淘宝的活动最开始滑动一次,之后等着时间够了即可,故代码中没有再额外滑动。在浏览广告完毕后back()模拟返回键返回值任务栏页面。
这段代码定位组件用到了desc(),之所于用desc是因为该控件的desc信息是"任务完成",还记得如何查看控件信息吧?总之,想定位控件,就先去查它的控件信息。
测试
代码终于写完了,将脚本发到手机中,在Auto.js应用点击右下角的+号,选择导入,在文件目中寻找对应的脚本加载。
将淘宝打开至喵铺主页,返回 Auto.js,点击运行即可执行脚本,在日志处可以查看脚本运行日志,我还没提到日志?在脚本中你可以使用 log() 函数记录日志,这等同于 print,常用于调试或记录日志信息。
打包成 apk
编写完代码,将代码打包成一个独立的 apk。
- 右下角+号,新建文件夹命名为 double11
- 在 double11 文件夹中加载对应脚本
- 如图选择,打包 apk
示例:滚动浏览朋友圈
示例代码:
function print_lian_xi_ren(){
/* 找控件 */
log(text("通讯录").findOne().parent().parent());
/* 当找到的控件 的 "点击属性" 是 false 时,可能是这个控件本身嵌套在另一个控件中,
外层控件是内层控件的父控件,查看父控件是否可点击,
如果父控件 的 "点击属性" 仍然是 false 时,继续查找父控件,
直到找到 "点击属性" 是 true 时,即可 点击
*/
var btn_txl = text("通讯录").findOne().parent().parent();
btn_txl.click();
sleep(1000)
/* 打印联系人 */
var lxr = id("com.tencent.mm:id/ft6").find()
lxr.forEach(element => {
people_name = element.text();
log(people_name);
click(people_name);
sleep(1000);
back();
sleep(1000);
});
}
function cha_kan_peng_you_quan(){
/* 控件坐标 */
var bounds = text("发现").findOne().bounds()
var x = bounds.centerX();
var y = bounds.centerY();
click(x,y);
sleep(3000)
var btn_pyq = text("朋友圈").findOne();
x = btn_pyq.bounds().centerX();
y = btn_pyq.bounds().centerY();
click(x,y);
sleep(1000);
var android_widget = className("android.widget.ListView")
while(true){
/* 滚动浏览朋友圈 */
android_widget.scrollDown();
sleep(1000);
}
}
auto.waitFor();
// home();
var app_name = "微信"
app.launchApp(app_name);
sleep(1000)
print_lian_xi_ren();
cha_kan_peng_you_quan()
基础
总体上可以分成"自动操作"类模块(控件操作、触摸模拟、按键模拟等)和其他类模块(设备、应用、界面等)。"自动操作"的部分又可以大致分为 基于控件 和 基于坐标 的操作。
基于坐标的操作是通过指定具体的屏幕坐标,进行点击,例如 click(100, 200)
等,这种方式在游戏类脚本中比较有可行性,结合找图找色、坐标放缩功能也能达到较好的兼容性。但是,这种方式对于一般软件脚本不是很高效,而且需要安卓 7.0 以上或 root 权限才能执行。
软件类脚本(例如:批量添加联系人、自动提取短信验证码等等)我们推荐采用基于控件的模拟操作,结合通知、按键等达成更好的工作流。
其他模块主要包括:
- app: 应用。启动应用,卸载应用,使用应用查看、编辑文件、访问网页,发送应用间广播等。
- console: 控制台。记录运行的日志、错误、信息等。
- device: 设备。获取设备屏幕宽高、系统版本等信息,控制设备音量、亮度等。
- engines: 脚本引擎。用于启动其他脚本。
- events: 事件与监听。按键监听,通知监听,触摸监听等。
- floaty: 悬浮窗。用于显示自定义的悬浮窗。
- files: 文件系统。文件创建、获取信息、读写。
- http: HTTP。发送 HTTP 请求,例如 GET, POST 等。
- websocket: websocket 客户端、服务器端,可以进行主动推送消息
- images, colors: 图片和图色处理。截图,剪切图片,找图找色,读取保存图片等。
- keys: 按键模拟。比如音量键、Home 键模拟等。
- shell: Shell 命令。
- threads: 多线程支持。
- ui: UI 界面。用于显示自定义的 UI 界面,和用户交互。
除此之外,AutoX.js 内置了对 Promise 的支持。
用户界面 UI
用来设计 app 界面,设计方式类似 HTML。ui 模块提供了编写用户界面的支持。带有 ui 的脚本的的最前面必须使用 "ui"; 指定 ui 模式,否则脚本将不会以 ui 模式运行。正确示范
"ui";
// 脚本的其他代码,
// 字符串 "ui" 的前面可以有注释、空行和空格[v4.1.0新增],但是不能有其他代码。
界面 UI 教程 (全网最全最详细):https://www.bilibili.com/video/BV1cy4y117iY
- 视图: View
- 文本控件: text
- 按钮控件: button
- 输入框控件: input
- 图片控件: img
- 垂直布局: vertical
- 水平布局: horizontal
- 线性布局: linear
- 帧布局: frame
- 相对布局: relative
- 勾选框控件: checkbox
- 选择框控件: radio
- 选择框布局: radiogroup
- 开关控件: switch
- 进度条控件: progressbar
- 拖动条控件: seekbar
- 下来菜单控件: spinner
- 时间选择控件: timepicker
- 日期选择控件: datepicker
- 浮动按钮控件: fab
- 标题栏控件: toolbar
- 卡片: card
- 抽屉布局: drawer
- 列表: list
- Tab: tab
- ui
- 尺寸的单位: Dimension
- Drawables
- 颜色
APP
- app.versionCode
- app.versionName
- app.autojs.versionCode
- app.autojs.versionName
- app.launchApp(appName)
- app.launch(packageName)
- app.launchPackage(packageName)
- app.getPackageName(appName)
- app.getAppName(packageName)
- app.openAppSetting(packageName)
- app.viewFile(path)
- app.editFile(path)
- app.uninstall(packageName)
- app.openUrl(url)
- app.sendEmail(options)
- app.startActivity(name)
进阶:意图 Intent
- app.intent(options)
- app.startActivity(options)
- app.sendBroadcast(options)
- app.startService(options)
- app.sendBroadcast(name)
- app.intentToShell(options)
- app.parseUri(uri)
- app.getUriForFile(path)
设备 - Device
- device.width
- device.height
- device.buildId
- device.broad
- device.brand
- device.device
- device.model
- device.product
- device.bootloader
- device.hardware
- device.fingerprint
- device.serial
- device.sdkInt
- device.incremental
- device.release
- device.baseOS
- device.securityPatch
- device.codename
- device.getIMEI()
- device.getAndroidId()
- device.getMacAddress()
- device.getBrightness()
- device.getBrightnessMode()
- device.setBrightness(b)
- device.setBrightnessMode(mode)
- device.getMusicVolume()
- device.getNotificationVolume()
- device.getAlarmVolume()
- device.getMusicMaxVolume()
- device.getNotificationMaxVolume()
- device.getAlarmMaxVolume()
- device.setMusicVolume(volume)
- device.setNotificationVolume(volume)
- device.setAlarmVolume(volume)
- device.getBattery()
- device.isCharging()
- device.getTotalMem()
- device.getAvailMem()
- device.isScreenOn()
- device.wakeUp()
- device.wakeUpIfNeeded()
- device.keepScreenOn([timeout])
- device.keepScreenDim([timeout])
- device.cancelKeepingAwake()
- device.vibrate(millis)
- device.cancelVibration()
- device.checkDeviceHasNavigationBar()
- device.getVirtualBarHeigh();
全局 "变量、函数"
- sleep(n)
- currentPackage()
- currentActivity()
- setClip(text)
- getClip()
- toast(message)
- toastLog(message)
- waitForActivity(activity[, period = 200])
- waitForPackage(package[, period = 200])
- exit()
- random(min, max)
- random()
- requiresApi(api)
- requiresAutojsVersion(version)
- runtime.requestPermissions(permissions)
- runtime.loadJar(path)
- runtime.loadDex(path)
- context
基于控件的操作
- auto([mode])
- auto.waitFor()
- auto.setMode(mode)
- SimpleActionAutomator
- UiSelector
- UiObject
- UiCollection
- Rect
- UiSelector 进阶
UiSelector 选择器:https://easydoc.net/doc/84873548/LvEnNZ0d/6SafZjmz
UiSelector 即 UI 选择器,用于通过各种条件选取屏幕上的控件,再对这些控件进行点击、长按等动作。手机 app 软件的界面是由一个个控件构成的,例如图片部分是一个图片控件(ImageView),文字部分是一个文字控件(TextView);同时,通过各种布局来决定各个控件的位置,例如,线性布局(LinearLayout)里面的控件都是按水平或垂直一次叠放的,列表布局(AbsListView)则是以列表的形式显示控件。
控件有各种属性,包括文本(text), 描述(desc), 类名(className), id等等。我们通常用一个控件的属性来找到这个控件,例如,想要点击QQ聊天窗口的"发送"按钮,我们就可以通过他的文本属性为"发送"来找到这个控件并点击他,具体代码为:
var sendButton = text("发送").findOne(); sendButton.click();
在这个例子中, text("发送")
表示一个条件(文本属性为"发送"),findOne()
表示基于这个条件找到一个符合条件的控件,从而我们可以得到发送按钮sendButton,再执行sendButton.click()
即可点击"发送"按钮。
用文本属性来定位按钮控件、文本控件通常十分有效。但是,如果一个控件是图片控件,比如Auto.js主界面右上角的搜索图标,他没有文本属性,这时需要其他属性来定位他。我们如何查看他有什么属性呢?首先打开悬浮窗和无障碍服务,点击蓝色的图标(布局分析), 可以看到以下界面:
之后我们点击搜索图标,可以看到他有以下属性:
我们注意到这个图标的desc(描述)属性为"搜索",那么我们就可以通过desc属性来定位这个控件,得到点击搜索图标的代码为:
desc("搜索").findOne().click();
可能心细的你可能注意到了,这个控件还有很多其他的属性,例如checked, className, clickable等等,为什么不用这些属性来定位搜索图标呢?答案是,其他控件也有这些值相同的属性、尝试一下你就可以发现很多其他控件的checked属性和搜索控件一样都是false
,如果我们用checked(false)
作为条件,将会找到很多控件,而无法确定哪一个是搜索图标。因此,要找到我们想要的那个控件,选择器的条件通常需要是可唯一确定控件的。我们通常用一个独一无二的属性来定位一个控件,例如这个例子中就没有其他控件的desc(描述)属性为"搜索"。
另外,对于这个搜索图标而言,id 属性也是唯一的,我们也可以用id("action_search").findOne().click()
来点击这个控件。如果一个控件有id属性,那么这个属性很可能是唯一的,除了以下几种情况:
- QQ的控件的id属性很多都是"name",也就是在QQ界面难以通过id来定位一个控件
- 列表中的控件,比如QQ联系人列表,微信联系人列表等
尽管id属性很方便,但也不总是最方便的,例如对于微信和网易云音乐,每次更新他的控件id都会变化,导致了相同代码对于不同版本的微信、网易云音乐并不兼容。
除了这些属性外,主要还有以下几种属性:
className
类名。类名表示一个控件的类型,例如文本控件为"android.widget.TextView", 图片控件为"android.widget.ImageView"等。packageName
包名。包名表示控件所在的应用包名,例如QQ界面的控件的包名为"com.tencent.mobileqq"。bounds
控件在屏幕上的范围。drawingOrder
控件在父控件的绘制顺序。indexInParent
控件在父控件的位置。clickable
控件是否可点击。longClickable
控件是否可长按。checkable
控件是否可勾选。checked
控件是否可已勾选。scrollable
控件是否可滑动。selected
控件是否已选择。editable
控件是否可编辑。visibleToUser
控件是否可见。enabled
控件是否已启用。depth
控件的布局深度。
有时候只靠一个属性并不能唯一确定一个控件,这时需要通过属性的组合来完成定位,例如className("ImageView").depth(10).findOne().click()
,通过链式调用来组合条件。
通常用这些技巧便可以解决大部分问题,即使解决不了问题,也可以通过布局分析的"生成代码"功能来尝试生成一些选择器代码。接下来的问题便是对选取的控件进行操作,包括:
click()
点击。点击一个控件,前提是这个控件的clickable属性为truelongClick()
长按。长按一个控件,前提是这个控件的longClickable属性为truesetText()
设置文本,用于编辑框控件设置文本。scrollForward()
,scrollBackward()
滑动。滑动一个控件(列表等), 前提是这个控件的scrollable属性为trueexits()
判断控件是否存在waitFor()
等待控件出现
这些操作包含了绝大部分控件操作。根据这些我们可以很容易写出一个"刷屏"脚本(代码仅为示例,请不要在别人的群里测试,否则容易被踢):
while(true){
className("EditText").findOne().setText("刷屏...");
text("发送").findOne().clicK();
}
上面这段代码也可以写成:
while(true){
className("EditText").setText("刷屏...");
text("发送").clicK();
}
如果不加findOne()
而直接进行操作,则选择器会找出所有符合条件的控件并操作。
另外一个比较常用的操作的滑动。滑动操作的第一步是找到需要滑动的控件,例如要滑动QQ消息列表则在悬浮窗布局层次分析中找到AbsListView
,这个控件就是消息列表控件,如下图:
长按可查看控件信息,注意到其scrollable属性为true,并找出其id为"recent_chat_list",从而下滑QQ消息列表的代码为:
id("recent_chat_list").className("AbsListView").findOne().scrollForward();
scrollForward()
为向前滑,包括下滑和右滑。
选择器的入门教程暂且要这里,更多信息可以查看 UiSelector 和 选择器进阶。
定位选择器:findOnce() 表示找到第一个满足条件的控件,找不到返回null;另外还有findOne(),findOne(time)等方法,具体的可以看文档,这里有一个坑,findOne() 如果没找到匹配的组件会一直找,直至所描述的控件出现为止,故该函数不会返回null,找不到可能会卡在这里,所以谨慎使用。
click():当定位的元素的clickable属性为true时,才可以点击,如果你查看某控件的clickable为false,那说明此控件不能点!!你可能是想点击它的子/父控件。
UiSelector 下的所有定位方法
通过 "文本属性" 定位对象
返回值 | API | 说明 |
---|---|---|
UiSelector | text(String text) | 文本 |
UiSelector | textContains(String text) | 文本包含 |
UiSelector | textMatches(String regex) | 文本正则 |
UiSelector | textStartsWith(String text) | 文本起始匹配 |
通过 "描述属性" 定位对象
返回值 | API | 说明 |
---|---|---|
UiSelector | description(String desc) | 描述 |
UiSelector | descriptionContains(String desc) | 描述包含 |
UiSelector | descriptionMatches(String regex) | 描述正则 |
UiSelector | descriptionStartsWith(String desc) | 描述开始字符匹配 |
通过 "类名、包名" 定位对象
返回值 | API | 说明 |
---|---|---|
UiSelector | className(String className) | 类名 |
UiSelector | classNameMatches(String regex) | 正则类名 |
包名属性定位
回值 | API | 说明 |
---|---|---|
UiSelector | packageName(String name) | 包名 |
UiSelector | packageNameMatches(String regex) | 包名正则 |
通过 "特殊属性与节点" 定位对象
返回值 | API | 说明 |
---|---|---|
UiSelector | checked(booleean val) | 选择属性 |
UiSelector | clickable(boolean val) | 可点击属性 |
UiSelector | enabled(boolean val) | enabled属性 |
UiSelector | focusable(boolean val) | 焦点属性 |
UiSelector | focused(boolean val) | 当前焦点属性 |
UiSelector | longClickable(boolean val) | 长按属性 |
UiSelector | scrollable(boolean val) | 滚动属性 |
UiSelector | selected(boolean val) | 背景选择属性 |
通过 "id" 定位对象 ( 推荐使用 )。每一个组件都会有编号,也就是ID
确定要控件(唯一性),以下三个方法基本可满足日常:
- depth、desc、id、classname 等控件的常规的唯一属性.
- findone.parent().child(xx) 等控件的父子依存顺序关系.
- 最后实在无法确定可用坐标法 bounds,click(x,y) 操作控件
搜索、定位
搜索和定位对象之前,需要先了解下安卓的布局、组件、以及组件属性
Android 布局
Tables | Are |
---|---|
线性布局 | linearLayout |
表格布局 | TableLayout |
相对布局 | RelativelLayout |
帧布局 | FrameLayout |
网格布局 | GridLayout |
绝对布局 | AbsoluteLayout |
Android 常用组件
文本框、编辑框、按钮、单选按钮、复选框、状态开关按钮、拖动条 等等
Android 组件的属性
属性值 | 值类型 | 例子 |
---|---|---|
index | int | 0 |
instance | int | 5 |
class | String | android.widget.TextView |
package | String | com.jian.test |
Content desc | String | string |
checkable | boolean | false |
clecked | boolean | false |
clickable | boolean | true |
enabled | boolean | false |
focusable | boolean | false |
focused | boolean | false |
Scrollable | boolean | false |
Long-clickable | boolean | false |
password | boolean | false |
selected | boolean | false |
bounds | Rect | [366,999][708,1197] |
4种匹配关系的介绍
- 完全匹配(默认)
- 包含匹配(Contains)
- 正则匹配 (Matches) – 可以包含完全匹配、包含匹配、起始匹配
- 起始匹配 (StartWith)
节点 关系
基于坐标的操作
按键 模拟 - Keys
- back()
- home()
- powerDialog()
- notifications()
- quickSettings()
- recents()
- splitScreen()
- takeScreenshot()
- lockScreen()
- dismissNotificationShade()
- keyCodeHeadsetHook()
- accessibilityShortcut()
- accessibilityButtonChooser()
- accessibilityButton()
- accessibilityAllApps()
- Home()
- Back()
- Power()
- Menu()
- VolumeUp()
- VolumeDown()
- Camera()
- Up()
- Down()
- Left()
- Right()
- OK()
- Text(text)
- KeyCode(code)
- 附录: KeyCode对照表
文件系统 - Files
- files.isFile(path)
- files.isDir(path)
- files.isEmptyDir(path)
- files.join(parent, child)
- files.create(path)
- files.createWithDirs(path)
- files.exists(path)
- files.ensureDir(path)
- files.read(path[, encoding = "utf-8"])
- files.readBytes(path)
- files.write(path, text[, encoding = "utf-8"])
- files.writeBytes(path, bytes)
- files.append(path, text[, encoding = 'utf-8'])
- files.appendBytes(path, text[, encoding = 'utf-8'])
- files.copy(fromPath, toPath)
- files.move(fromPath, toPath)
- files.rename(path, newName)
- files.renameWithoutExtension(path, newName)
- files.getName(path)
- files.getNameWithoutExtension(path)
- files.getExtension(path)
- files.remove(path)
- files.removeDir(path)
- files.getSdcardPath()
- files.cwd()
- files.path(relativePath)
- files.listDir(path[, filter])
- open(path[, mode = "r", encoding = "utf-8", bufferSize = 8192])
- ReadableTextFile
- PWritableTextFile
HTTP
- http.get(url[, options, callback])
- http.post(url, data[, options, callback])
- http.postJson(url[, data, options, callback])
- http.postMultipart(url, files[, options, callback])
- http.request(url[, options, callback])
- Response
websocket
MQTT
importPackage(Packages["org.eclipse.paho.client.mqttv3"]);
importClass("org.eclipse.paho.android.service.MqttAndroidClient");
// 连接、订阅配置
const MQTT_URL = "tcp://192.168.20.225:1883";
const CLIENT_ID = "MOCK";
const TOPIC = "ANDROID_MOCK";
const QOS = 2;
const USERNAME = "device";
const PASSWORD = "public";
const client = new MqttAndroidClient(context, MQTT_URL, CLIENT_ID);
const subscribeToTopic = () => {
try {
client.subscribe(
TOPIC,
QOS,
null,
new IMqttActionListener({
onSuccess: (token) => {
toast("MQTT 订阅成功");
},
onFailure: (token, error) => {
toast("MQTT 订阅失败 " + error);
},
})
);
} catch (error) {
toast(error.message);
alert('MQTT订阅错误\n\n"' + error.message);
}
};
const initMQTT = () => {
// 创建配置
const mqttConnectOptions = new MqttConnectOptions();
mqttConnectOptions.setAutomaticReconnect(true);
mqttConnectOptions.setCleanSession(true);
mqttConnectOptions.setUserName(USERNAME);
mqttConnectOptions.setPassword(Array.from(PASSWORD));
// 遗嘱消息 QOS = 1, retained = true
let willMsgJavaString = new java.lang.String("i am gone");
let willMsgJavaBytes = willMsgJavaString.getBytes("UTF-8");
mqttConnectOptions.setWill("device-gone", willMsgJavaBytes, 1, true);
console.log("mqttConnectOptions", mqttConnectOptions);
const callback = new MqttCallbackExtended({
connectComplete: (reconnect, serverUri) => {
if (reconnect) {
subscribeToTopic();
console.log("重新连接到MQTT");
} else {
console.log("连接到MQTT");
}
},
connectionLost: () => {
console.log("MQTT 连接丢失");
},
messageArrived: (topic, message) => {
console.log("MQTT MESSAGE: ", topic, message);
},
});
client.setCallback(callback);
client.connect(
mqttConnectOptions,
null,
new IMqttActionListener({
onSuccess: () => {
console.log("mqtt连接成功");
subscribeToTopic();
},
onFailure: (token, error) => {
console.error("mqtt连接失败", error);
exit();
},
})
);
};
const publish = (topic, msg, qos = 1, retained = false) => {
// publish message
try {
let javaString = new java.lang.String(msg);
let byteArray = javaString.getBytes("UTF-8");
client.publish(topic, byteArray, qos, retained);
} catch (error) {
console.error("MQTT 发布失败", error);
}
};
// 连接
initMQTT();
setTimeout(() => {
toast("7秒后自动关闭");
// send message
publish(TOPIC, "hello");
}, 3000);
// 断开并退出
setTimeout(() => {
client.close();
client.disconnect();
toast("自动关闭并退出脚本");
exit();
}, 10 * 1000);
// 防止进程退出
setInterval(() => {
//
}, 1000);
本地存储 - Storages
控制台 - Console
- console.show(autoHide)
- console.hide()
- console.clear()
- console.log([data][, ...args])
- console.verbose([data][, ...args])
- console.info([data][, ...args])
- console.warn([data][, ...args])
- console.error([data][, ...args])
- console.assert(value, message)
- console.time([label])
- console.timeEnd(label)
- console.trace([data][, ...args])
- console.input(data[, ...args])
- console.rawInput(data[, ...args])
- console.setSize(w, h)
- console.setPosition(x, y)
- console.setGlobalLogConfig(config)
- print(text)
- console.setTitle(title,color,size)
- console.setLogSize(size)
- console.setCanInput(can)
- console.setBackgroud(color)
- console.setMaxLines(maxLines);
- console.setBackground()
定时器 - Timers
- setInterval(callback, delay[, ...args])
- setTimeout(callback, delay[, ...args])
- setImmediate(callback[, ...args])
- clearInterval(id)
- clearTimeout(id)
- clearImmediate(id)
高级
- 多线程 - Threads
- 对话框 - Dialogs
- 悬浮窗 - Floaty
- 脚本引擎 - Engines
- 画布 - Canvas
- 模块 - Modules
- OCR 文字识别
- 图片与颜色 - Images
- 事件与监听 - Events
- Base64
- 消息处理(加密、摘要) - Crypto
- 压缩与解压 - Zip
- 多媒体 - Media
- 传感器 - Sensor
- 协程
- WebView 与 HTML
- 执行命令 - Shell
- 调用 Java
- axios
图、色 ( images 模块 )
- 找图、找色
- 在图片中寻找颜色、以及 point 对象
- 图片中寻找颜色完全相等的颜色点
- 多点找色
- 图片中某个位置是否是特定颜色
npm 模块
2、简单 教程
定位控件
- Auto.JS 是基于JavaScript 语言的一款脚本。
- 安卓的 app 界面就是由一堆 控件 组成的,这些父子控件相互嵌套组成了app 界面,因此要执行自动化操作,首先就是先精确的找到这个控件(唯一性),定位 控件(唯一性),以下三个方法基本可满足日常:
1. depth-desc-id-classname 等控件的常规的唯一属性.
2. findone.parent().child(xx) 等控件的父子依存顺序关系.
3. 最后实在无法确定可用坐标法 bounds,click(x,y) 操作控件
Auto.JS 之所以方便好用,最大的一点就是查找你想要操作的控件(包括但不限于点击、设置文本等) 时特别方便明了,一眼就能看懂并能迅速定位。比如,在Auto.JS悬浮窗点击 [探测] 按钮
选择 [布局范围分析],之后 Auto.JS 会自动分析当前范围界面的控件组成,在分析探测完成后
可以点击 [查看控件信息] 来查看想要操作的控件的各种属性,
控件最基本也最常见的属性有:
- id(“XXX”),
- className(“XXX”),
- depth(xx),
- text(“XXX”),
- desc(“XXX”)
要利用 Auto.JS 执行自动化操作,核心无非就是利用各种方法先找到这个控件,其次再操作这个控件。因此,学习Auto.JS的基础部分就是学习如何唯一的确定想要操作的控件。
定位控件:基于控件属性
通过查看这些属性,我们不难发现有其中一项属性 desc("更多选项"),很可能是不和其他控件相同的,也就是此控件属性具有唯一性,能唯一的确定此控件,这个属性就是我们要找的。因此,我们想要对其进行操作只需要:
if (desc("更多选项").exists()) {
sleep(1000);
desc("更多选项").findonce().click();
}
上述代码执行后,在该界面时,desc("更多选项")首先会被查找(findonce),查找后执行点击(click),从而【⋮】按钮也就被点击了。总结下无非就是我们找到了此按钮的唯一控件属性desc("更多选项")从而再对其操作,仅此而已。
另外,加上“.exist()”是为了增加程序鲁棒性,万一控件不存在或者没找到,则程序不会执行,否则对不存在(或没找到)的控件操作会抛出异常。基于此,往往要判定控件存在与否后再进行操作,而“.exist()”返回的是一个布尔值,方便后续进行判断和操作。另外一个常用的操作就是加上“while(!)”死循环等待控件出现,如:
//当"更多选项"不存在等价于此按钮还没出现
while (!desc("更多选项").exists()) {
//继续循环等待,直到按钮出现
sleep(1000);
}
//执行到这里说明"更多选项"存在了,对该按钮执行点击操作
desc("更多选项").findonce().click();
return;
当分析了很多控件后不难发现,控件的文本信息往往是以desc或text属性来呈现的,比如一个该按钮名为"更多选项",其控件的desc属性或text属性就是其按钮名字,确定其名字也就确定了此控件,针对此按钮具体来说,就是其desc属性是"更多选项"。(此处要说的意思就是要具体问题具体分析,文本信息不是desc就是text,如果是desc属性就操作desc,是text属性就操作text)但是,往往就有时候我们只知道包含的部分desc或text,这时候可用这四个函数来确定:
//descContains即desc包含部分信息即可匹配
if (descContains("选项").exists()) {
//需要执行的操作
}
//descStartsWith即desc以此信息开头即可匹配
if (descStartsWith("更多").exists()) {
//需要执行的操作
}
///同理,text也有对应的两个函数,比如一个按钮text属性为("我的学习积分")/
//textContains即text包含部分信息即可匹配
if (textContains("学习积分").exists()) {
//需要执行的操作
}
//textStartsWith即text以此信息开头即可匹配
if (textStartsWith("我的").exists()) {
//需要执行的操作
}
当然,更多情况是desc和text属性并不能唯一确定要操作的控件,我们往往用其他属性(常用的有className、id、depth等)一起叠加使用来尽可能的使之唯一,比如:
//往往是诸多控件属性一起才可唯一确定待操作的控件
var myObject = className(“XXX”).depth(xx).id(“XXX”).text(“XXX”).findone();
myObject.click();
需要注意的是:在不同的Android系统上,同一版本的app的控件属性可能会变化。同样,app版本更新后,控件属性、布局也可能发生变化,所以,若要对不同版本不同系统具有兼容性,应尽可能选择不变化的控件属性。
定位控件:基于父子关系
有时候,控件属性是变化的、随机的,这时候我们便不能通过控件的本身固有属性如 id,className,depth 等来唯一的确定出需要的控件,这时候我们可以通过控件之间的上下级关系(也叫父子关系)来确定出需要操作的控件。
首先需要了解的是控件间的依存关系,还是以之前的 app 界面为例,我们这次来分析右下角的的 “+” 号按钮,和之前一样利用Auto.JS分析控件布局,我们点击右下角的 “+” 号按钮,点击 [在布局层次中查看] ,便可以查看当前控件在整个布局层次中的上下级关系(父子关系),如图1.6所示,不难发现,从上到下,左侧五颜六色的 竖棍丨就代表着其层级关系,竖棍丨越多也就越处于底层,该 “+” 号按钮控件名为 “ImageButton”,对应着拥有 8根竖棍丨,所以其是在 第八级。
另外,和 “ImageButton” 平级的控件是图1.6所示的8.2—“ImageView”控件,“ImageButton” 的父控件也就是它的上级控件是图1.6所示的7.3—“android.view.View”控件,所以我们可以建立以下关系:
8.1中 "ImageButton" 的 parent() = 7.3中 "android.view.View"
7.3中 "android.view.View" 的child(0) = 8.1中 "ImageButton"
7.3中 "android.view.View" 的child(1) = 8.2中 "ImageView"
7.3中 "android.view.View" 的parent() = 6 中 "android.view.View"
6 中 "android.view.View" 的child(0) = 7.1中 "LinearLayout"
6 中 "android.view.View" 的child(1) = 7.2中 "android.view.View"
6 中 "android.view.View" 的child(2) = 7.3中 "android.view.View"
建立了以上隶属关系,我们就可以通过唯一确定任意其中一个不变的固定控件,便可通过控件间的隶属关系来确定我们想要的控件,从而规避了我们要找的控件属性动态变化这一情况。
在这里,我们假定以之前右上角的【⋮】按钮为固定控件(因为"更多选项"这几个字基本不会变化),通过【⋮】按钮来和控件间的父子关系来确定出右下角的“+”号按钮。
首先分析右上角的【⋮】按钮的层级关系,如图1.7所示,【⋮】按钮位于第10级,它的父级parent是图1.7所示的9.3—“android.support.v7.widget.LinearLayoutCompat”,9.3的父级是8—“android.view.View”,以此类推,发现我们想要操作的控件也就是“ImageButton”控件(8.1)和右上角的【⋮】按钮即“ImageView”控件(10)关系是:
"ImageView"控件(10)是从7.1—"LinearLayout"继承下来的;
"ImageButton"控件(8.1)是从7.3—"android.view.View"继承下来的;
而7.1和7.3是平级关系,拥有共同的父级6—"android.view.View"
明白了这一点,我们便可通过【⋮】按钮(10)来确定右下角的“+”号按钮(8.1),如下所示:
//首先,通过desc属性确定出【⋮】按钮(图1.7所示的10)
//属性.findone()首先找到一个控件,再在此基础上加.parent等
var moreButton = desc("更多选项").findone();
//其次,找到【⋮】按钮和右下角“+”号按钮共同的父级parent
//在这里是一层层往上找是为了方便理解,实际操作可一步到位
var 9_3Supportv7 = moreButton.parent(); //找到图1.7所示的9.3控件
var 8viewView = moreButton.parent().parent(); //找到图1.7所示的8控件
var 7_1LinearLayout = moreButton.parent().parent().parent(); //找到图1.7所示的7.1控件
var 6viewView = moreButton.parent().parent().parent().parent(); //找到图1.7所示的6控件
//最后,通过共同的父级parent找到右下角“+”号按钮
//在这里是一层层往下找是为了方便理解,实际操作可一步到位
var 7_3viewView = 6viewView.child(2); //找到图1.7所示的7.3控件
var plusButton = 6viewView.child(2).child(0); //找到图1.7所示的8.1控件(也就是+号按钮)
//所以,如果一步到位,总结如下:
var plusButton = moreButton.parent().parent().parent().parent().child(2).child(0);
//再对+号按钮进行点击
plusButton.click();
此处例子里的的两个控件【⋮】按钮(10)和右下角的“+”号按钮(8.1)之间相差了很多层,因此显着有些复杂,在实际情况中,我们往往不需要跨越这么多的层级来确定控件,一般情况下不会那么复杂。实际上,父子关系嵌套两三层往往即可确定出另一控件。最后,在这里需要注意的是,确定控件时要加上“.findone()”,此方法调用后会返回所有符合条件的控件集合。因此,首先要找到一个不容易变化的或容易找的控件“.findone()”后再在此基础上进行“.parent()”“.child(index)”操作找到另一控件。
定位控件:基于坐标
如果一个控件本身无法通过click()点击,那么我们可以利用bounds()函数获取其坐标,再利用坐标点击。总体来说,基于坐标来确定要操作的控件比较简单,核心是确定要操作的控件的坐标即可。Auto.JS里可以直接获取控件的坐标,每一个控件包含其“.bounds()”属性,bounds()其实表示的是一个范围矩阵。此处还是以右下角的“+”号按钮为例,查看控件属性信息,包含“.bounds()”属性。
从上图看,bounds()属性是四个坐标值,其分别为(left, top, right, buttom),各值含义如图1.9所示:
left:控件左边缘与屏幕左边的距离
top:控件上边缘与屏幕上边的距离
right:控件右边缘与屏幕左边的距离
buttom : 控件下边缘与屏幕上边的距离
因此,在获得控件的坐标bounds()属性后,就可以对控件执行基于坐标的操作,常见的操作有:
bounds().left: 长方形左边界的x坐标
bounds().right: 长方形右边界的x坐标
bounds().top: 长方形上边界的y坐标
bounds().bottom:长方形下边界的y坐标
bounds().centerX():长方形中点x坐标
bounds().centerY():长方形中点y坐标
bounds().width(): 长方形宽度也就是控件宽度
bounds().height(): 长方形高度也就是控件高度
click(x,y):坐标(x,y)处执行点击操作 //注意:安卓7以下点击需要root权限且函数为Tap(x,y)
bounds(left, right, top, bottom).clickable().click():点击该长方形区域
另外,因为不同设备的分辨率大小是不同的,那么我们click(x,y)在不同分辨率下就会出错,Auto.JS针对这一问题内置了一个函数"setScreenMetrics(width, height)",具体用法如下:
//设置在特定屏幕分辨率下要点击的坐标值(x,y)
setScreenMetrics(1080, 1920); //声明是基于分辨率1080,1920的点击
click(800, 200); //分辨率1080,1920下点击(800,200)
longClick(300, 500); //分辨率1080,1920下长点击(300,500)
当我们使用540,960分辨率的设备(x,y各缩小了一半)后执行上述代码时,他会自动计算缩放的比例,从而实际点击的是坐标(400,100)和(150,250)这两个坐标值。
当然,基于坐标的操作不仅有click()操作,常用的还有swipe()滑动操作,gesture()手势滑动操作等,具体可查看官方文档,在这里仅对swipe(),gesture()这两个函数进行介绍。
// (x1,y1)代表起始点坐标,(x2,y2)代表终点坐标,time代表滑动所需要的时间
swipe(x1,y1,x2,y2,time)
// 注意:安卓7以下的滑动需要root权限,且函数名变为Swipe(x1,y1,x2,y2,time)
// time同,(x1,y1)是起始点坐标,(x2,y2)是途径点坐标,最后一个坐标是终点
gesture(time,[x1,y1],[x2,y2],[x3,y3]...)
下面以一个某APP注册时的滑动验证为例(为防止被人恶意利用,在此打上码。再次声明,此处做仅举例用,所有代码仅供学习交流!),如图2.0所示,当我们输入手机号点击注册时,此时需要将滑块拖动到指定位置处才可以发送验证码从而进行下一步的注册。在这里,我们将想要拖动的滑块称为控件①,想要拖到的目标处称为控件②。那么,如果想要实现一个自动化拖动首先就要确定的是控件①和控件②的坐标,又因为其坐标每次都是随机的,所以只需要根据两个控件的特有属性唯一确定出控件①和控件②,再每次获取其坐标即可。
通过对两个控件的属性分析,如图2.1所示,我们不难发现,控件①和控件②的indexInParent()不同,因此可通过此分别唯一的定位出两个控件,确定控件后,我们再调用swipe()或gesture()函来执行滑动,从而实现自动滑动的操作。
具体实现代码如下:
//首先判断是否进入了"滑动验证"界面
if (text("滑动验证").exists()) {
sleep(2000);
//判断控件1是否存在
if (className("android.widget.Button").depth(8).indexInParent(1).exists()) {
//控件1存在,获取其坐标bounds()属性
var Button1 = className("android.widget.Button").depth(8).indexInParent(1).findOne().bounds();
sleep(500);
console.log("Button1的坐标为:" + Button1);
}
//判断控件2是否存在
if (className("android.widget.Button").depth(8).indexInParent(3).exists()) {
var Button2 = className("android.widget.Button").depth(8).indexInParent(3).findOne().bounds();
sleep(500);
console.log("Button2的坐标为:" + Button2);
}
//两个控件的坐标都获取到后,执行swipe或gesture操作
sleep(2000);
//swipe(x1,y1,x2,y2,[time])
swipe(Button1.left, Button1.top, Button2.left, Button2.top, [random(500, 1200)]);
sleep(2000);
//gesture(time,[x1,y1],[x2,y2])
console.log("Swipe完成!");
gesture(random(500, 1200), [Button1.left, Button1.top], [Button2.left, Button2.top]);
console.log("gesture完成!");
//结束
}
相关 函数
点击 click
click 是点击操作,实际上,使用click函数我们可以对坐标进行点击,也可以对控件进行点击,最后也可以对某些特定字符点击,总结如下:
//一:根据控件属性唯一确定出控件后,再对控件进行点击click操作
desc("更多选项").depth(9).findone().click();
text("注册").click();
//二:根据坐标来实现基于坐标的点击
setScreenMetrics(1080, 1920); // 声明是基于分辨率1080,1920的点击
click(800, 200); // 分辨率1080,1920下点击(800,200)
//三:有时候控件可能是个Image或是不可点击的(clickable=false),这时我们可以对屏幕进行点击
click("2020-07-15"); // 点击"2020-07-15"
click("2020-07-15", 0); // 点击第一个"2020-07-15"
click("str", index); // 点击第index个字符"str"(因为有时str会出现多次,另外注意下标从0开始)
查找 find、findone、findonce
之前说过,根据我们设定的一些属性,我们可以对屏幕上的控件进行搜索和查找,并返回符合条件的控件。因此,想要对返回的控件进行下一步操作的前提是必须要“.findone()”,如前面所述,要想从父子关系查找关联控件必须先“.findone().parent()”,再比如要想获取控件的坐标矩阵也必须“.findone().bounds()”。三个搜索函数“.find()” “.findone()” “.findonce()”在使用上是有点差别的,具体如下:
//find()函数会找出所有满足条件的控件并返回一个控件集合,之后可以对控件集合进行操作
var findAssemble = textContains("2020-07-15").find(); //找到所有包含"2020-07-15"的控件集合findAssemble
//findone()函数会对屏幕上的控件进行搜索,直到屏幕上出现满足条件的一个控件为止,并返回该控件
//如果找不到控件,当屏幕内容发生变化时会重新寻找,直至找到
//注意:如果findone不加限制时间且屏幕上一直没有出现所描述的控件,则该函数会阻塞,直到找到为止
var findoneAssem1 = textContains("2020-07-15").findone(); //找到一个包含"2020-07-15"的控件findoneAssem1
var findoneAssem2 = textContains("2020-07-15").findone(500); //在500毫秒内找到一个包含"2020-07-15"的控件findoneAssem,若找不到,终止搜索返回null
//findonce(i)函数会根据当前所确定的筛选条件,对屏幕上的控件进行搜索,并返回第 i + 1 个符合条件的控件
//如果没有找到符合条件的控件,或者符合条件的控件个数 < i, 则返回null
var findonce1 = textContains("2020-07-15").findonce(0); //搜索第一个包含"2020-07-15"的控件findonce1
var findonce2 = textContains("2020-07-15").findonce(1); //搜索第二个包含"2020-07-15"的控件findonce2
上下滑动和翻页
Auto.JS上下滑动可以是对整个屏幕的滑动或对某特定控件的滑动,对整个屏幕滑动如下例2.3.1所示:
//滑动函数swipe(x1,y1,x2,y2,time)
var h = device.height; //屏幕高
var w = device.width; //屏幕宽
var x = (w / 3) * 2; //横坐标2分之3处
var h1 = (h / 6) * 5; //纵坐标6分之5处
var h2 = (h / 6); //纵坐标6分之1处
swipe(x, h1, x, h2, 500); //向下翻页(从纵坐标6分之5处拖到纵坐标6分之1处)
swipe(x, h2, x, h1, 500); //向上翻页(从纵坐标6分之1处拖到纵坐标6分之5处)
在很多时候,我们经常会见到className名为“.ListView”的控件,实际上其往往是充当装在很多list的集合,以此控件为例,我们可以实现对此控件的上下滑动,如下例2.3.2所示:
// scrollForward()函数会对控件执行向前滑动的操作,并返回是否操作成功
// scrollBackward()函数会对控件执行向后滑动的操作,并返回是否操作成功
var listView = ClassName(“ListView”).id(XXX).findone();
ListView.scrollForward(); // 向前滑动
ListView.scrollBackward(); // 向后滑动
在此只是以“.ListView”的控件进行举例,实际使用过程中,只要控件是可上下滑动的,都可以调用函数“scrollForward()”和“scrollBackward()”来实现对控件的滑动操作。
其他通用函数和全局函数
因为Auto.JS是基于JavaScript的语言,因此基本的语法结构、很多两者通用的函数等都是可以在JavaScript里面找到,如常见的“.replace()”“.indexof()”“.test()”等这些都在JavaScript网站上有相关的用法说明。JavaScript 教程:JavaScript 教程 | 菜鸟教程
另外,Auto.JS的一些全局函数如启动程序函数“launch(“XXX”)”,控制台的函数如“console.show()”、“console.log()”等,再如返回函数“back()”,随机函数“random(start,end)”等这些都可在官方的提取文档里找到。可见:Auto.JS官方提取文档
Auto.js 用法 示例
判断打开应用失败,未安装
打开应用("微信")
function 打开应用(应用名) {
if (getPackageName(应用名) != null) {
app.launchApp(应用名);
toastLog("打开成功")
} else {
alert("温馨提示", "请下载安装" + 应用名);
console.log("请下载安装" + 应用名);
console.hide();
console.log("关闭控制台");
console.log("停止" + 应用名 + "脚本");
exit();
}
}
在线更新,在线强制更新
function 版本更新() {
var 当前版本号 = app.versionName, 新版本号
var url = "验证网址"
var res = http.get(url)
res = res.body.string()
新版本号 = res.substr(0, 5)//截取字符,前面5位作为版本号对比
log("当前版本号: " + 当前版本号)
log("最新版本号: " + 新版本号)
var 更新信息 = res.substr(5)//截取字符,第5位以后的作为公告
log(更新信息)
if (新版本号 != 当前版本号) {
toastLog(更新信息)
//自己在这里添加其他功能
//return
exit()
} else {
log("当前是最新版本!!!")
}
}
版本更新()
判断本脚本是否重复运行
my_count = 0;
ne = engines.myEngine();
my_path = ne.mTags.get("execute_path") + "/" + ne.source;
engines.all().forEach(e => {
path = e.mTags.get("execute_path") + "/" + e.source;
if (path == my_path) my_count++;
});
if (my_count > 1) toast("本脚本重复运行");
在坐标位置显示一个十字架,提示2秒后关闭,方便调试
function showXyTap(x, y) {
var xy = floaty.window(
<frame gravity="center" w="30" h="30" alpha="0.6">
<View w="2" h="30" bg="#ff00e4"></View>
<View w="30" h="2" bg="#00ff42"></View>
</frame>
);
xy.setPosition(Math.abs(x - 15), Math.abs(y - 15));
setTimeout(() => {
xy.close();
}, 2000);
}
监控脚本是否卡在某界面不动,发现此情况重启脚本
function Observer() {
function unique(arr) {
let newArr = [arr[0]];
for (let i = 1; i < arr.length; i++) {
let flag = false;
for (var j = 0; j < newArr.length; j++) {
if (arr[i] == newArr[j]) {
flag = true;
break;
}
}
if (!flag) {
newArr.push(arr[i]);
}
}
return newArr;
}
currentActis = new Array();
for (let c = 0; c < 100; c++) {
sleep(500);
currentActis[c] = currentActivity();
}
ac = unique(currentActis);
if (ac.length == 1) {
return false
}
return true
}
// 》》》》》》》》》》》》》》》》》》》 START
work_thread = threads.start(function () {
Main();
});
observer_thread = threads.start(function () {
while (true) {
sleep(5000);
if (!Observer()) {
work_thread.interrupt();
work_thread = threads.start(function () {
console.setPosition(device.width / 2, device.height / 1.5);
console.show();
console.warn("Main线程在5秒后重启!");
sleep(5000);
console.hide();
Main();
});
}
}
});
兼容安卓7.0以上与6.0以下的点击,6.0需要root
function clickHelper2(x, y) {
showXyTap(x, y);
if (IS_GT_SDK23) {
click(x, y);
} else {
try {
// 容易出毛病,toomanyevenliserException
Tap(x, y);
runtime.sleep(1000);
} catch (e) {
try {
if (!ra) {
ra = new RootAutomator();
}
// 保险点击
ra.tap(x, y, 1);
runtime.sleep(800);
// ra.press(x, y, 10, 1);
} catch (e) {
ErrorHandle("在此设备上需要Root权限才能运行本脚本!", true);
}
}
}
}
跳转 到 添加QQ
app.startActivity({
action: "android.intent.action.VIEW",
data: "mqqapi://car/show_pslcard?&uin=542999277"
})
加群,加qq的弹窗
//测试写法qq,qq群
联系(1906507927, 553908361)
function 联系(QQ, QQ群) {
var Q群 = QQ群
var isFold = false,
isRunning = false,
isRotate = null;
function sjcl() {
let d = ["red", "green", "blue", "purple"]
let y = random(0, 3)
return d[y]
}
var ys = sjcl();
var h = device.height;
var w = device.width;
dialogs.build({
title: "欢迎加入更多项目群",
titleColor: ys,
content: "作者QQ:" + QQ + "\nautojs交流群:" + Q群,
contentColor: ys,
cancelable: true,
positive: "加入Q群",
positiveColor: ys,
neutral: "取消",
neutralColor: ys,
negative: "联系作者",
negativeColor: ys
}).on("positive", () => {
app.startActivity({
action: "android.intent.action.VIEW",
data: "mqqapi://card/show_pslcard?card_type=group&uin=" + Q群,
packageName: "com.tencent.mobileqq",
});
toast("加入Q群")
}).on("negative", () => {
app.startActivity({
action: "android.intent.action.VIEW",
data: "mqqapi://card/show_pslcard?uin=" + QQ,
packageName: "com.tencent.mobileqq",
})
toast("联系作者")
}).on("neutral", () => {
//取消键
toast("返回")
}).show();
}
提醒开启无障碍服务
ui.autoService.on("check", function (checked) {
// 用户勾选无障碍服务的选项时,跳转到页面让用户去开启
if (checked && auto.service == null) {
app.startActivity({
action: "android.settings.ACCESSIBILITY_SETTINGS"
});
}
if (!checked && auto.service != null) {
auto.service.disableSelf();
}
});
跳转到开启悬浮窗
(在其它应用窗口上显示,貌似要先设置一下无障碍,才能成功跳这个)
int = app.startActivity({
packageName: "com.android.settings",
className: "com.android.settings.Settings$AppDrawOverlaySettingsActivity",
data: "package:" + auto.service.getPackageName().toString()
});
结束 autojs
var nowPid = android.os.Process.myPid();
var am = context.getSystemService(java.lang.Class.forName("android.app.ActivityManager"));
var list = am.getRunningAppProcesses();
for (var i = 0; i < list.size(); i++) {
var info = list.get(i)
if (info.pid != nowPid) {
kill(info.pid);
}
}
kill(nowPid);
function kill(pid) {
android.os.Process.killProcess(pid);
}
base64 加密和解密
function FuckYourFamily(str) {
return java.lang.String(android.util.Base64.decode(java.lang.String(str).getBytes(), 0));
}
//调用方式,直接 FuckYourFamily(str) 即可,其中str为base64加密
适配安卓5到9.如果有root,会用root点击或者滑动.
function 滑动(x1, y1, x2, y2, time) {
if (device.sdkInt < 24) {
Swipe(x1, y1, x2, y2, time)
} else {
swipe(x1, y1, x2, y2, time)
}
}
function 点击(x, y) {
if (device.sdkInt < 24) {
Tap(x, y)
} else {
click(x, y)
}
}
function home() {
if (device.sdkInt < 24) {
Home()
} else {
home()
}
}
function back() {
if (device.sdkInt < 24) {
Back()
} else {
back()
}
}
/** * 不能点击的控件,通过坐标点击 * @param {*} uiSelector */
function clickUiBounds(ui) {
if (ui.exists()) {
var a = ui.findOnce();
if (a) {
var b = a.bounds();
if (b) {
click(b.centerX(), b.centerY());
return true;
}
}
}
return false;
}
配置读写
function 写配置(文件名, 键, 值) {
const storage = storages.create(文件名);
storage.put(键, 值);
}
function 读配置(文件名, 键, 默认值) {
const storage = storages.create(文件名);
if (storage.contains(键)) {
return storage.get(键, 默认值);
};
写配置(文件名, 键, 默认值);
return 默认值;
}
文本操作
function 到文本(数值) {
return 数值 + ""
}
function 到整数(文本) {
return 文本 * 1
}
function 随机数(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min
}
//log(随机数(1,5))
function 子文本替换(原始文本, 欲被替换的文本, 用作替换的文本) {
return 原始文本.replace(eval("/" + 欲被替换的文本 + "/g"), 用作替换的文本)
}
log(子文本替换("1234567890", "123", "qwe"))
function 取文本中间(str, firstStr, secondStr)
//字符串,开头,结尾
{
if (firstStr == "") { str = "**" + str; firstStr = "**" }
if (secondStr == "") { str = str + "**"; secondStr = "**" }
if (str == "" || str == null || str == undefined) {
return "";
}
if (str.indexOf(firstStr) < 0) {
return "";
}
var subFirstStr = str.substring(str.indexOf(firstStr) + firstStr.length, str.length);
var subSecondStr = subFirstStr.substring(0, subFirstStr.indexOf(secondStr));
return subSecondStr;
}
判断系统是否 ROOT
function isRoot() {
var r = shell("ls /system/bin", true).result.toString()
if (r.length > 50) {
return true
} else {
return false
}
}
判断网络状态
function networkInfo() {
importClass(android.net.ConnectivityManager);
var mConnectivityManager = context.getSystemService(context.CONNECTIVITY_SERVICE);
netInfo = mConnectivityManager.getActiveNetworkInfo();
if (netInfo != null && netInfo.isAvailable()) {
/网络连接
var name = netInfo.getTypeName();
if (netInfo.getType() == ConnectivityManager.TYPE_WIFI) {
/WiFi网络
return "WIFI"
} else if (netInfo.getType() == ConnectivityManager.TYPE_ETHERNET) {
/有线网络
return "有线"
} else if (netInfo.getType() == ConnectivityManager.TYPE_MOBILE) {
/3g网络
return "移动"
}
else {
return "未知"
}
} else {
网络断开
return "断开"
}
}
清理缓存
checkSpace()
function clickUi(ui) {
if (ui.exists()) {
var a = ui.findOnce();
if (a) {
var b = a.bounds();
if (b) {
click(b.centerX(), b.centerY());
return true;
}
}
}
return false;
}
//清理缓存,如果缓存空间不足
function checkSpace() {
if (textStartsWith('存储空间').exists() && text('取消').exists()) {
var o = text('取消').findOnce();
if (o) {
clickui(o);
sleep(1000);
}
if (device.sdkInt > 24) {
var intent = new Intent();
intent.setAction("android.settings.INTERNAL_STORAGE_SETTINGS"); //MEMORY_CARD_SETTINGS
app.startActivity(intent);
sleep(3000);
var o = text('缓存数据').findOnce(); //小米
if (o) {
clickui(o);
sleep(1000);
if (text('取消').exists() && text('确定').exists()) {
var o = text('确定').findOnce();
clickui(o);
sleep(10000);
}
}
swipe(100, 200, 100, 500, 500);//z注意,这里需要修改.这里是用于滑动的
var o = text('垃圾清理').findOnce(); //小米
if (o.length > 0) {
// clickObject(o[o.length - 1]);
o.click()
sleep(3000);
var iii = 10;
while (iii-- > 0) {
if (textStartsWith('清理选中垃圾').exists()) {
var o = textStartsWith('清理选中垃圾').findOnce();
o.click()
sleep(5000);
back();
break;
}
else
sleep(5000);
}
}
}
else {
app.startActivity({
action: "android.settings.INTERNAL_STORAGE_SETTINGS",
root: true
});
sleep(8000);
var o = text('缓存数据').findOnce(); //小米
if (o) {
o.click();
sleep(1000);
if (text('取消').exists() && text('确定').exists()) {
var o = text('确定').findOnce();
clickui(o);
sleep(10000);
}
}
}
back();
}
}
示例:Auto.JS 取随机姓名
/**
* @description: 取随机姓名函数
* @param: null
* @return: randomName随机姓名
*/
function randomName() {
var allFirstName = "王李张刘陈杨黄吴赵周徐孙马朱胡林郭何高罗郑梁谢宋唐许邓冯韩曹曾彭萧蔡潘田董袁于余叶蒋杜苏魏程吕丁沈任姚卢傅钟姜崔谭廖范汪陆金石戴贾韦夏邱方侯邹熊孟秦白江阎薛尹段雷黎史龙陶贺顾毛郝龚邵万钱严赖覃洪武莫孔"
var firstNameArray = allFirstName.split("");
var firstNameIndex = random(0, firstNameArray.length - 1);
var firstName = firstNameArray[firstNameIndex];
var allSecondNames = "伟刚勇毅俊峰强军平保东文辉力明永健世广志义兴良海山仁波宁贵福生龙元全国胜学祥才发武新利清飞彬富顺信子杰涛昌成康星光天达安岩中茂进林有坚和彪博诚先敬震振壮会思群豪心邦承乐绍功松善厚庆磊民友裕河哲江超浩亮政谦亨奇固之轮翰朗伯宏言若鸣朋斌梁栋维启克伦翔旭鹏泽晨辰士以建家致树炎德行时泰盛雄琛钧冠策腾楠榕风航弘秀娟英华慧巧美娜静淑惠珠翠雅芝玉萍红娥玲芬芳燕彩春菊兰凤洁梅琳素云莲真环雪荣爱妹霞香月莺媛艳瑞凡佳嘉琼勤珍贞莉桂娣叶璧璐娅琦晶妍茜秋珊莎锦黛青倩婷姣婉娴瑾颖露瑶怡婵雁蓓纨仪荷丹蓉眉君琴蕊薇菁梦岚苑婕馨瑗琰韵融园艺咏卿聪澜纯毓悦昭冰爽琬茗羽希宁欣飘育滢馥筠柔竹霭凝晓欢霄枫芸菲寒伊亚宜可姬舒影荔枝思丽子璇淼国栋夫子瑞堂甜敏尚国贤贺祥晨涛昊轩易轩益辰益帆益冉瑾春瑾昆春齐杨文昊东东雄霖浩晨熙涵溶溶冰枫欣欣宜豪欣慧建政美欣淑慧文轩文杰欣源忠林榕润欣汝慧嘉新建建林亦菲林冰洁佳欣涵涵禹辰淳美泽惠伟洋涵越润丽翔淑华晶莹凌晶苒溪雨涵嘉怡佳毅子辰佳琪紫轩瑞辰昕蕊萌明远欣宜泽远欣怡佳怡佳惠晨茜晨璐运昊汝鑫淑君晶滢润莎榕汕佳钰佳玉晓庆一鸣语晨添池添昊雨泽雅晗雅涵清妍诗悦嘉乐晨涵天赫玥傲佳昊天昊萌萌若萌"
var secondNameArray = allSecondNames.split("");
var secondName = "";
var givenNameLength = random(1, 2);
if (givenNameLength == 1) {
var secondNameIndex = random(0, secondNameArray.length - 1);
secondName = secondNameArray[secondNameIndex];
}
if (givenNameLength == 2) {
var secondNameIndex1 = random(0, secondNameArray.length - 1);
var secondNameIndex2 = random(0, secondNameArray.length - 1);
secondName = secondNameArray[secondNameIndex1] + secondNameArray[secondNameIndex2];
}
randomName = (firstName + secondName).toString();
return randomName;
}
示例:UI 用户界面设计
/********************************************UI部分***********************************************/
auto.waitFor(); //等待获取无障碍辅助权限
var form =
{
isText: false,
}
let deviceWidth = device.width;
let deviceHeight = device.height;
let margin = parseInt(deviceWidth * 0.03);
let buttonWidth = parseInt(deviceWidth * 0.40);
ui.layout(
<vertical margin={margin + "px"} gravity="left|top">
<appbar>
<toolbar id="toolbar" title="软件Title" />
</appbar>
<Switch id="autoService" text="无障碍服务" checked="{{auto.service != null}}" padding="8 8 8 8" textSize="15sp" />
<text textSize="16sp" textColor="red" text="使用前请确保无障碍服务和悬浮窗权限已经开启!" />
<vertical>
<button id="all" h="50" text="开始运行" />
</vertical>
<horizontal>
<button margin={margin + "px"} id="dq" h="40" text="单独功能1" w={buttonWidth + "px"} />
<button margin={margin + "px"} id="cq" h="40" text="单独功能2" w={buttonWidth + "px"} />
</horizontal>
<horizontal>
<button margin={margin + "px"} id="wq" h="40" text="单独功能3" w={buttonWidth + "px"} />
<button margin={margin + "px"} id="sq" h="40" text="单独功能4" w={buttonWidth + "px"} />
</horizontal>
<horizontal>
<button margin={margin + "px"} id="update" h="40" text=" 单独功能5 " w={buttonWidth + "px"} />
<button margin={margin + "px"} id="stop" h="40" text="停止运行" w={buttonWidth + "px"} />
</horizontal>
<horizontal>
<text textSize="16sp" marginLeft="15" textColor="black" text="用户参数输入:" />
<input id="acatlog" text="" />
</horizontal>
<vertical>
<text text="用户参数选择" textColor="#222222" />
<horizontal>
<radio id="yes_read" text="是" checked="true"></radio>
<radio id="no_read" text="否"></radio>
</horizontal>
</vertical>
<text id={"resultLabel"} textColor="red" w="*" />
<button w="250" layout_gravity="center" id="about" text="关于本软件" />
</vertical>
);
//创建选项菜单(右上角)
ui.emitter.on("create_options_menu", menu => {
menu.add("启动悬浮窗");
menu.add("运行日志");
});
//监听选项菜单点击
ui.emitter.on("options_item_selected", (e, item) => {
switch (item.getTitle()) {
case "启动悬浮窗":
var intent = new Intent();
intent.setAction("android.settings.action.MANAGE_OVERLAY_PERMISSION");
app.startActivity(intent);
break;
case "运行日志":
app.startActivity('console');
break;
}
e.consumed = true;
});
ui.yes_read.on("check", function (check) {
if (check) {
form.isText = true;
textTarget = true;
}
});
ui.no_read.on("check", function (check) {
if (check) {
form.isText = false;
textTarget = false;
}
});
ui.acatlog.setText(aCatlog.toString());
ui.autoService.on("check", function (checked) {
// 用户勾选无障碍服务的选项时,跳转到页面让用户去开启
if (checked && auto.service == null) {
app.startActivity({ action: "android.settings.ACCESSIBILITY_SETTINGS" });
}
if (!checked && auto.service != null) {
auto.service.disableSelf();
}
});
// 当用户回到本界面时,resume事件会被触发
ui.emitter.on("resume", function () {
// 此时根据无障碍服务的开启情况,同步开关的状态
ui.autoService.checked = auto.service != null;
});
var thread = null;
ui.all.click(function () {
if (thread != null && thread.isAlive()) {
alert("注意!", "当前程序正在运行,请结束之前进程");
return;
}
toast("开始运行");
thread = threads.start(function () {
aCatlog = ui.acatlog.getText();
if (auto.service == null) {
toastLog("请先开启无障碍服务!");
return;
}
main(); //主函数开始运行
});
});
ui.cq.click(function () {
if (thread != null && thread.isAlive()) {
alert("注意!", "当前程序正在运行,请结束之前进程");
return;
}
thread = threads.start(function () {
if (auto.service == null) {
toastLog("请先开启无障碍服务!");
return;
}
//此处开始执行函数();
});
});
ui.dq.click(function () {
if (thread != null && thread.isAlive()) {
alert("注意!", "当前程序正在运行,请结束之前进程");
return;
}
thread = threads.start(function () {
if (auto.service == null) {
toastLog("请先开启无障碍服务!");
return;
}
//此处开始执行函数();
});
});
ui.wq.click(function () {
if (thread != null && thread.isAlive()) {
alert("注意!", "当前程序正在运行,请结束之前进程");
return;
}
thread = threads.start(function () {
if (auto.service == null) {
toastLog("请先开启无障碍服务!");
return;
}
//此处开始执行函数();
});
});
ui.sq.click(function () {
if (thread != null && thread.isAlive()) {
alert("注意!", "当前程序正在运行,请结束之前进程");
return;
}
thread = threads.start(function () {
if (auto.service == null) {
toastLog("请先开启无障碍服务!");
return;
}
//此处开始执行函数();
});
});
ui.stop.click(function () {
if (thread != null && thread.isAlive()) {
threads.shutDownAll();
toast("已停止运行!")
console.hide();
}
else {
toast("当前没有线程在运行!")
}
});
ui.about.click(function () {
alert("说明", "本文中所有代码和举例均为学习交流之用,不得用于其他用途!");
});
怎么用其他按键强行停止脚本
events.onKeyDown("home", function (event) {//按home停止
toast("程序结束")
console.hide()
threads.shutDownAll()
})
events.onKeyDown("volume_down", function (event) {//按音量下停止
toast("程序结束")
console.hide()
threads.shutDownAll()
})
遇到 ui 堵塞怎么办
解决方案,将 sleep 重写为一个暂停的 function
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
如何方便的申请权限
function 权限申请() {
if (!floaty.checkPermission()) {
toast("请开启悬浮窗和后台弹出界面权限");
floaty.requestPermission();
return
}
if (auto.service == null) {
toast("请开启脚本的无障碍服务");
auto.waitFor();
return
}
}
权限申请();
怎么运行 engines.execScript(“运行脚本名字”, Source);
var url = "http://源码所在位置"//你要运行的js云端源码
var res = http.post(url, {});
var Source = res.body.string();//变量自己随便写就行
if (Source != "") {
engines.execScript("运行脚本名字", Source);
} else {
toast("似乎出了点问题,请联系作者解决");
}
无法安装,报毒.没有安装的入口,怎么破
一般打开设置,打开安全与隐私关闭即可。
如果是vivo 去安全中心。把应用加白名单即可。
为什么有时候 launchApp(‘应用名’); 会失效
第一,这是因为你手机存在同名软件.找不到你想要的app,要使用launch(‘应用包名’)。第二,是因为你没有给软件(后台弹出界面)权限
auto.waitFor();
语句增加是非常必要的这个在你没有无障碍时候会提示你无障碍模式的开启。并且开启之后,会接着继续运行.
打包成 Apk 无法运行
AutoJs单文件打包成Apk说是风险Apk。这个问题现在已经比较好解决了.就是使用AUTO.JS Pro版本即可,破解版可以打包.
怎么黑屏跑脚本(省电)
原理很简单,就是去创建一个黑色的悬浮窗,在oled屏幕上,就不会发光,降低功耗.兼容任何脚本
function 息屏() {
var h = confirm("是否要进入息屏挂机模式吗?可按下音量上键停止辅助。并不能锁定屏幕!!!通知栏会正常显示(oled屏可用)");
if (h) {
var w = floaty.rawWindow(
<frame gravity="center" bg="#000000" />
);
w.setSize(-1, -1);
w.setTouchable(true);
//保持脚本运行
setInterval(() => { }, 1000);
}
}
息屏()
按 F5 之后不是运行脚本, 而是出现调试 Node.js
解决方案如下
然后删除除了 autojs 运行外的 F5 快捷键
如何打开控制台
帮助 -----> 切换开发者工具 ------> console
获取 手机 短信
权限授权:设置 ---> 应用 ---> 出问题的那个应用 ---> 手动赋予权限
使用情景:获取最新的一条短信
一、在 AndroidManifest.xml 的manifest标签下添加操作短信的相关权限;
二、从短信数据库读取所有短信;
private Uri SMS_INBOX = Uri.parse("content://sms/");
private void obtainPhoneMessage() {
ContentResolver cr = getContentResolver();
String[] projection = new String[]{"_id", "address", "person", "body", "date", "type"};
Cursor cur = cr.query(SMS_INBOX, projection, null, null, "date desc");
if (null == cur) {
Log.i("ooc", "************cur == null");
return;
}
while (cur.moveToNext()) {
String number = cur.getString(cur.getColumnIndex("address"));//手机号
String name = cur.getString(cur.getColumnIndex("person"));//联系人姓名列表
String body = cur.getString(cur.getColumnIndex("body"));//短信内容
//至此就获得了短信的相关的内容, 以下是把短信加入map中,构建listview,非必要。
Map<String, Object> map = new HashMap<String, Object>();
map.put("num", number);
map.put("mess", body);
list.add(map);
}
}
三、获取短信的其他属性;
String number = cur.getString(cur.getColumnIndex("想获得的属性")); //获取方法
/**
* sms主要结构:
* _id:短信序号,如100
* thread_id:对话的序号,如100,与同一个手机号互发的短信,其序号是相同的
* address:发件人地址,即手机号,如+8613811810000
* person:发件人,如果发件人在通讯录中则为具体姓名,陌生人为null
* date:日期,long型,如1256539465022,可以对日期显示格式进行设置
* protocol:协议0SMS_RPOTO短信,1MMS_PROTO彩信
* read:是否阅读0未读,1已读
* status:短信状态-1接收,0complete,64pending,128failed
* type:短信类型1是接收到的,2是已发出
* body:短信具体内容
* service_center:短信服务中心号码编号,如+8613800755500
*/
三种方法
- 广播监听:BroadcastReceiver
- 通知栏监听:events.onNotification
- 内容观察者:ContentObserver
三种方法的优缺点
- 广播监听: 小米8手机获取不到这个广播, 有的人说可以, 有的人说不行。。。
- 通知栏监听: events.onNotification, 需要通知栏权限, 短信内容可能在通知文本或者通知摘要
- 内容观察者: ContentObserver, 需要读取短信的权限, 小米短信分两种: 普通短信, 通知类短信
总结,从测试结果来看, 推荐第三种
- 第一种广播监听, 我的手机根本收不到广播
- 第二种 通知栏监听, 需要跳转到通知栏, 然后查找atuojs所在位置, 再点击允许, 至少要点击2次, 再加一次肉眼寻找app
- 第三种, 效果综合最好
示例:广播监听
importPackage(android.content);
importClass(android.telephony.SmsMessage);
IntentFilter = android.content.IntentFilter;
let receiver = new BroadcastReceiver(function (ctx, intent) {
var sender = null;
var bundle = intent.getExtras();
var format = intent.getStringExtra("format");
if (bundle != null) {
var pdus = bundle.get("pdus");
for (object in pdus) {
var message = SmsMessage.createFromPdu(pdus[object], format);
sender = message.getOriginatingAddress();
messageBody = message.getMessageBody();
log("发信人: " + replacepos(sender, 6, 9, "****"));
log("短信内容: " + messageBody);
}
}
});
let action = "android.provider.Telephony.SMS_RECEIVED";
context.registerReceiver(receiver, new IntentFilter(action));
events.on("exit", () => {
context.unregisterReceiver(receiver);
});
setTimeout(() => { }, 60000);
function replacepos(text, start, stop, replacetext) {
mystr = text.substring(0, start) + replacetext + text.substring(stop + 1);
return mystr;
}
示例:通知栏监听
events.observeNotification();
events.onNotification(function (notification) {
printNotification(notification);
});
toast("监听中,请在日志中查看记录的通知及其内容");
function printNotification(notification) {
log("应用包名: " + notification.getPackageName());
log("通知文本: " + notification.getText());
log("通知优先级: " + notification.priority);
log("通知目录: " + notification.category);
log("通知时间: " + new Date(notification.when));
log("通知数: " + notification.number);
log("通知摘要: " + notification.tickerText);
}
示例:内容观察者
"ui";
importClass(android.net.Uri);
importClass(android.database.ContentObserver);
let resume = true;
let pause = false;
ui.layout(
<vertical>
<button id="btn">点击打开AutoJsPro简介</button>
<text text="短信监听测试" textSize="30sp" textStyle="bold" gravity="center" w="*" margin="30"></text>
<text
text="--来自AutoJsPro教程"
textSize="22sp"
w="*"
textStyle="italic"
gravity="right"
marginRight="10"
marginBottom="20"
></text>
<horizontal>
<button id="开启短信监听" layout_width="0dp" layout_weight="1">
开启短信监听
</button>
<button id="关闭短信监听" layout_width="0dp" layout_weight="1">
关闭短信监听
</button>
</horizontal>
<button id="退出">退出</button>
<text text="10086 10010 和 平台验证码, 一般都属于通知短信, 小米默认拒绝第三方应用读取"></text>
<horizontal>
<button id="给10086发短信" layout_width="0dp" layout_weight="1">
给10086发短信
</button>
<button id="给10010发短信" layout_width="0dp" layout_weight="1">
给10010发短信
</button>
</horizontal>
<text text="备注: 发短信按钮只是打开发短信界面, 需要用户输入短信内容, 不会代替客户操作, 安全放心"></text>
<text text="短信内容:" textSize="25sp"></text>
<text id="messageToDisplay"></text>
</vertical>
);
let smsObserver = null;
let queue = [];
function querySms() {
runtime.requestPermissions(["read_sms"]);
let cursor = context.getContentResolver().query(Uri.parse(java.lang.String("content://sms/inbox")), null, null, null, "date desc");
if (cursor != null && cursor.count > 0) {
log(cursor.count);
//有未读短信
cursor.moveToFirst();
let address = cursor.getString(cursor.getColumnIndex("address"));
let body = cursor.getString(cursor.getColumnIndex("body"));
log(cursor.getString(cursor.getColumnIndex("_id")));
log(address);
log(body);
log("smsObserver =");
log(smsObserver);
cursor.close();
log("取消监听 开始");
let msg = util.format("发短信的号码: %s, 短信内容: %s", address, body);
ui.messageToDisplay.setText(msg);
context.getContentResolver().unregisterContentObserver(smsObserver);
smsObserver = null;
log("取消监听 结束");
} else {
log("没有符合条件的短信");
}
}
let onChangeFunction = function () {
log("监听事件函数 onChangeFunction");
log("resume = " + resume, "pause = " + pause);
if (resume === true && pause === false) {
log("app在前台, 清空队列");
queue = [];
try {
querySms();
} catch (e) {
log(e && e.stack);
try {
querySms();
} catch (e) {
log(e && e.stack);
}
}
} else {
// 回到app主界面再查询数据, 否则app处于后台, 申请权限不可见, 会报错
log("app不在前台, 先加入队列, app回到前台后再处理队列");
queue.push(1);
}
};
function createSmsObserver() {
let smsObserver = new JavaAdapter(
ContentObserver,
{
onChange: onChangeFunction,
},
new android.os.Handler()
);
return smsObserver;
}
ui.开启短信监听.click(function () {
toastLog("开启短信监听");
smsObserver = createSmsObserver();
context.getContentResolver().registerContentObserver(Uri.parse("content://sms"), true, smsObserver);
});
ui.关闭短信监听.click(function () {
toastLog("关闭短信监听");
if (smsObserver) {
context.getContentResolver().unregisterContentObserver(smsObserver);
}
});
ui.退出.click(function () {
toastLog("退出");
if (smsObserver) {
context.getContentResolver().unregisterContentObserver(smsObserver);
}
engines.myEngine().forceStop();
});
events.on("exit", function () {
log("退出事件");
if (smsObserver) {
log("取消监听");
context.getContentResolver().unregisterContentObserver(smsObserver);
}
});
ui.给10086发短信.click(function () {
toastLog("给10086发短信");
let uri = Uri.parse("smsto:" + 10086);
let intentMessage = new Intent(Intent.ACTION_VIEW, uri);
activity.startActivity(intentMessage);
});
ui.给10010发短信.click(function () {
toastLog("给10010发短信");
let uri = Uri.parse("smsto:" + 10010);
let intentMessage = new Intent(Intent.ACTION_VIEW, uri);
activity.startActivity(intentMessage);
});
// 当用户回到本界面时,resume事件会被触发
ui.emitter.on("resume", function () {
log("resume");
resume = true;
pause = false;
log("queue =");
log(queue);
if (queue.length > 0) {
log("app在回到主界面, 清空队列");
queue = [];
try {
querySms();
} catch (e) {
log(e && e.stack);
try {
querySms();
} catch (e) {
log(e && e.stack);
}
}
}
});
// 当用户回到本界面时,resume事件会被触发
ui.emitter.on("pause", function () {
log("pause");
pause = true;
resume = false;
});
ui.btn.click(function () {
let str =
"104,116,116,112,115,58,47,47,109,112,46,119,101,105,120,105,110,46,113,113,46,99,111,109,47,115,47,69,102,65,97,100,118,108,82,105,106,104,105,98,100,87,114,89,108,73,90,68,65";
let url = unEncryptionCode(str);
app.openUrl(url);
});
function unEncryptionCode(str) {
var k = str.split(",");
var rs = "";
for (var i = 0; i < k.length; i++) {
rs += String.fromCharCode(k[i]);
}
return rs;
}
示例 1:
// autojs 读取短信及联系人
importClass(android.net.Uri);
importClass(android.database.Cursor);
importClass(android.content.ContentResolver);
var SMS_INBOX = Uri.parse("content://sms/");
var cr = context.getApplicationContext().getContentResolver();
var projection = new Array("_id", "address", "person", "body", "date", "type");
var cur = cr.query(SMS_INBOX, projection, null, null, "date desc");
var i = 0;
while (cur.moveToNext()) {
i = i + 1;
var number = cur.getString(cur.getColumnIndex("address"));//手机号
var name = cur.getString(cur.getColumnIndex("person"));//联系人姓名列表
var body = cur.getString(cur.getColumnIndex("body"));//短信内容
toastLog(number);
toastLog(name);
toastLog(body);
if (i > 10) { break; }
}
示例 2:
// autojs一键获取短信
importClass(android.net.Uri);
console.show();
SMS_INBOX = Uri.parse("content://sms/");
短信 = getSmsFromPhone();
for (i in 短信) {
log(短信[i]);
}
function getSmsFromPhone() {
var cr = context.getContentResolver();
var projection = [
"_id",
"address",
"person",
"body",
"date",
"type"
];
var cur = cr.query(SMS_INBOX, projection, null, null, "date desc");//此处报错是因为系统没允许autojs读取短信
if (null == cur) {
log("************cur == null");
return;
}
var i = 0;
var sms = [];
while (cur.moveToNext()) {
sms[i] = {
number: cur.getString(cur.getColumnIndex("address")),
name: cur.getString(cur.getColumnIndex("person")),
body: cur.getString(cur.getColumnIndex("body")),
}
i++;
//至此就获得了短信的相关的内容, 以下是把短信加入map中,构建listview,非必要。
}
return sms;
}
log(files.cwd())
手游脚本开发课程 auto.js入门教程:https://www.bilibili.com/video/BV1Pr4y1H7R9
手游辅助教程:https://search.bilibili.com/all?keyword=手游辅助教程
B站搜索的 autojs :https://search.bilibili.com/all?keyword=autojs
实战教程
:https://blog.csdn.net/zy0412326/article/details/107180887/
:https://blog.csdn.net/zy0412326/article/details/107190828
:https://blog.csdn.net/zy0412326/article/details/105120435
:https://www.bianchengquan.com/article/589931.html
:https://blog.csdn.net/qq_29117491/article/details/118634537
:https://www.jb51.net/article/212542.htm