无障碍服务经常故障
有2种现象,重新授权无障碍权限才能恢复。
1、左侧菜单,“无障碍服务”权限下面出现“无障碍服务故障”一行小字。 这个时候必须重新授权,程序才能跑了。
2、没有任何提示,auto.service 不为空,auto.waitFor()也没有反应。 但是布局分析失效,部分api也失效,比如UiSelector.findOne(); 必须重新授权无障碍服务才能恢复。
第二种场景,试验出规律了,可能是个别api有问题。
复现步骤: 1、关闭无障碍服务。 2、执行下面场景的代码:
auto.waitFor(); // 这一步将跳转手动操作开启无障碍服务
// 这里尝试打开一个APP
app.launch(App.WECHAT.packageName);
sleep(3000)
log(existsOne('微信','通讯录')); // existsOne返回false
把existsOne()换成 content().exists()||的写法好像可以返回true。 不知道是不是existsOne()有问题。 前提是,必须在通过auto.waitFor()手动开启无障碍服务才能复现,也有可能是auto.waitFor()的问题。
这个问题似乎在三星手机上更容易复现,同一个脚本在小米,红米和1+手机上可以正常运行,但是三星手机就不行,似乎跟 #329 有些关系
auto.waitFor();
openConsole();
console.setTitle("自动抢票");
const monitorIntervalSeconds = 2;
var defaultPlayEtcStr = "7-27";
var defaultExactTicketPrice = 780;
var defaultTicketType = "看台";
main();
function getExactTicketPrice() {
var ticketPrice = rawInput("请输入指定票价", "");
if (ticketPrice == null || ticketPrice.trim() == "") {
alert("请输入指定票价!");
return getExactTicketPrice();
}
return ticketPrice;
}
function getTicketType() {
var ticketType = rawInput("请输入票档类型(如'看台'或'内场')", "");
if (ticketType == null || ticketType.trim() == "") {
alert("请输入票档类型!");
return getTicketType();
}
return ticketType;
}
function getPlayEtc() {
var playEtc = rawInput("请输入场次关键字(按照默认格式)", "");
if (playEtc == null || playEtc.trim() == "") {
alert("请输入场次信息!");
return getPlayEtc();
}
return playEtc;
}
function main() {
var playEtcStr = defaultPlayEtcStr ? defaultPlayEtcStr : getPlayEtc();
let playEtcArr = playEtcStr.split(",");
console.log("监控购票场次:" + playEtcArr);
var exactTicketPrice = defaultExactTicketPrice
? defaultExactTicketPrice
: getExactTicketPrice();
console.log("监控指定票价:" + exactTicketPrice);
var ticketType = defaultTicketType ? defaultTicketType : getTicketType();
console.log("监控票档类型:" + ticketType);
sleep(50);
if (!textContains("¥").exists()) {
refresh_ticket_dom();
}
if (!textContains("¥").exists()) {
console.log("请主动点击中间票档区域任意按钮刷新dom");
}
console.log("进入监控");
threads.start(function () {
log("刷新按钮自动点击线程已启动");
while (true) {
textContains("刷新").waitFor();
textContains("刷新").findOne().click();
log("点击刷新...");
sleep(100);
}
});
threads.start(function () {
console.log("开启票档扫描线程");
while (true) {
while (textContains("¥").exists()) {
cycleMonitor(ticketType, exactTicketPrice);
sleep(50);
}
sleep(1000);
}
});
sleep(1000);
while (true) {
if (
className("android.widget.TextView").text("场次").exists() &&
!textContains("数量").exists()
) {
for (let playEtc of playEtcArr) {
log("刷新场次余票信息:" + playEtc);
textContains(playEtc).findOne().click();
sleep(monitorIntervalSeconds * 1000);
}
}
}
}
function cycleMonitor(ticketType, exactTicketPrice) {
let targetTickets = get_exact_type_and_price_tickets(
ticketType,
exactTicketPrice
);
for (let ticket of targetTickets) {
log("抢票:" + ticket.type + " ¥" + ticket.price);
doSubmit(ticket);
}
}
function doSubmit(ticket) {
let targetText = ticket.type + " ¥" + ticket.price;
log("点击票档: " + targetText);
let found = false;
textContains("¥")
.find()
.forEach(function (btn) {
let btnText = btn.text() || "";
let parentText = "";
try {
parentText = btn.parent().text() || "";
} catch (e) {}
if (
btnText.includes(ticket.price) &&
(btnText.includes(ticket.type) || parentText.includes(ticket.type)) &&
!btnText.includes("缺货") &&
!parentText.includes("缺货")
) {
if (!found) {
btn.click();
found = true;
log("找到并点击了: " + btnText);
}
}
});
if (!found) {
log("未找到匹配的票档: " + targetText);
return false;
}
textContains("数量").waitFor();
let attemptCnt = 0;
let attemptMaxCnt = 150;
while (text("确认").exists() && attemptCnt <= attemptMaxCnt) {
click("确认");
console.log("点击确认");
if (className("android.widget.Button").exists()) {
console.log("找到支付按钮");
break;
}
attemptCnt++;
}
if (
attemptCnt >= attemptMaxCnt &&
!className("android.widget.Button").exists()
) {
console.log("尝试次数过多");
return false;
}
log("尝试次数:" + attemptCnt);
log("等待支付按钮出现...");
let buttonFound = false;
for (let i = 0; i < 10000; i++) {
if (className("android.widget.Button").exists()) {
buttonFound = true;
log("支付按钮已出现");
break;
}
log("等待支付按钮,尝试" + (i + 1) + "/10");
sleep(1000);
}
log("继续执行支付流程");
function clickPaymentButton() {
log("准备点击支付按钮");
sleep(1000);
let success = false;
for (let attempt = 1; attempt <= 5; attempt++) {
log("第" + attempt + "次尝试点击支付按钮");
if (text("立即支付").exists()) {
let payButton = text("立即支付").findOne();
log("找到立即支付按钮(文本)");
try {
let rect = payButton.bounds();
log(
"支付按钮位置: " +
rect.left +
"," +
rect.top +
" - " +
rect.right +
"," +
rect.bottom
);
payButton.click();
log("点击立即支付按钮");
success = true;
break;
} catch (e) {
log("方法1点击失败: " + e);
}
} else {
log("未找到'立即支付'文本按钮");
}
let payBtns = className("android.widget.Button")
.textMatches(/(立即|确认).*(支付|付款)/)
.find();
if (payBtns.length > 0) {
log("方法2: 找到" + payBtns.length + "个疑似支付按钮");
try {
payBtns[0].click();
log("点击找到的支付按钮: " + payBtns[0].text());
success = true;
break;
} catch (e) {
log("方法2点击失败: " + e);
}
} else {
log("方法2未找到匹配的按钮");
}
let allBtns = className("android.widget.Button").find();
if (allBtns.length > 0) {
log("方法3: 找到" + allBtns.length + "个按钮");
let bottomButtons = [];
for (let i = 0; i < allBtns.length; i++) {
let btn = allBtns[i];
let bounds = btn.bounds();
log("按钮" + i + ": 文本=" + btn.text() + ", 位置=" + bounds.bottom);
if (bounds.bottom > device.height * 0.7) {
bottomButtons.push({
button: btn,
bottom: bounds.bottom,
text: btn.text() || "",
});
}
}
bottomButtons.sort((a, b) => b.bottom - a.bottom);
if (bottomButtons.length > 0) {
try {
let btn = bottomButtons[0].button;
log("点击位置最底部的按钮: " + bottomButtons[0].text);
btn.click();
success = true;
break;
} catch (e) {
log("方法3点击失败: " + e);
}
}
} else {
log("方法3未找到任何按钮");
}
if (attempt >= 2) {
try {
let x = device.width / 2;
let y = device.height * 0.93;
log("方法4: 直接点击坐标 (" + x + "," + y + ")");
click(x, y);
sleep(500);
y = device.height * 0.85;
log("方法4: 再试坐标 (" + x + "," + y + ")");
click(x, y);
success = true;
} catch (e) {
log("方法4点击失败: " + e);
}
}
sleep(800);
}
return success;
}
let paymentSuccess = false;
let maxRetries = 3;
for (let retry = 0; retry < maxRetries; retry++) {
let clickSuccess = clickPaymentButton();
log("判断是否抢到");
sleep(2000);
if (
textContains("微信支付").exists() ||
descContains("微信支付").exists()
) {
log("抢票成功,请尽快支付");
paymentSuccess = true;
break;
} else {
if (retry < maxRetries - 1) {
log("未检测到支付页面,重新尝试点击支付按钮...");
sleep(1000);
}
}
}
if (!paymentSuccess) {
log("多次尝试后仍未成功进入支付页面");
try {
let imgPath = "/sdcard/抢票截图_" + new Date().getTime() + ".png";
captureScreen(imgPath);
log("已保存截图: " + imgPath);
} catch (e) {
log("截图失败: " + e);
}
}
return true;
}
function refresh_ticket_dom() {
threads.start(function () {
id("md_buttonDefaultPositive").findOne().click();
});
alert("刷新DOM");
}
function get_exact_type_and_price_tickets(ticketType, exactTicketPrice) {
var targetTickets = [];
textContains("¥")
.find()
.forEach(function (btn) {
let btnText = btn.text() || "";
let parentText = "";
try {
parentText = btn.parent().text() || "";
} catch (e) {}
if (
(btnText.includes(ticketType) || parentText.includes(ticketType)) &&
!btnText.includes("缺货") &&
!parentText.includes("缺货")
) {
let match = btnText.match(/¥(\d+)/);
let price;
if (match && (price = parseInt(match[1])) == exactTicketPrice) {
targetTickets.push({
type: ticketType,
price: price,
btn: btn,
});
log("找到符合条件的票档:" + ticketType + " ¥" + price);
}
}
});
log("符合条件票档数量:" + targetTickets.length);
return targetTickets;
}
临时解决方案: 必须要有root或者shizuku。 主要依赖“修改安全设置”权限(root和shizuku都能够开启)。 先开启以下设置:
AutoJS6→设置:
1、使用修改安全设置权限自动启用无障碍服务
2、使用root权限自动启用无障碍服务
然后:
// 这里用了存储,就是一个标志,也可以用写入文件代替,表示是否已重启。
var restart_time = storages.create('restart_time');
// 无障碍服务是否正常开启
let autoRun = 0;
if (auto.isRunning() && auto.service && auto.root) {
log("无障碍服务,[已启用]");
autoRun = 1;
storages.remove('restart_time');
} else {
console.error("无障碍服务,[未启用]");
}
//是否有条件重启无障碍服务
let canRestarAuto = 0;
if (autojs.canWriteSecureSettings()) {
log("修改安全设置授权,[已启用]");
canRestarAuto = 1;
} else {
log("修改安全设置授权,[未启用]!");
console.warn('当无障碍服务故障时,')
console.warn('程序可通过该权限自动重启无障碍')
console.info('该权限开启方式与[投影媒体权限]一样')
console.info('可通过Shizuku或root开启')
}
if (autojs.isRootAvailable()) {
log("Root授权,[已启用]");
canRestarAuto = 1;
} else {
log("Root授权,[未启用]!");
}
if (!autoRun && canRestarAuto) {
console.warn('发现已启用高级权限')
console.warn('可尝试重启无障碍服务')
console.error('正在重启无障碍服务......')
console.error('该功能需开启以下设置项')
console.info('-----------------')
console.error('AutoJS6→设置→')
console.error(' 1.使用修改安全设置权限自动启用无障碍服务')
console.error(' 2.使用 root 权限自动启用无障碍服务')
try {
auto.stop();
auto.start();
} catch (e) {}
try {
auto(true)
} catch (e) {}
// 重启程序
restart();
}
// 提醒用户只能手动处理
if (!autoRun && !canRestarAuto) {
console.error("需重新启用无障碍服务");
console.error("或重启手机");
if (notice.isEnabled()) {
notice(String('出错了!(' + nowDate().substr(5, 14) + ')'), String("无障碍服务故障或未启用"));
}
wait(() => false, 2000);
exit();
wait(() => false, 2000);
}
// 重启
function restart() {
let startTime = restart_time.get('startTime');
if (!startTime) {
restart_time.put('startTime', new Date().getTime());
let fileName = engines.myEngine().getSource().getName() + '.js';
console.info("即将重启本脚本:" + fileName)
console.error("提示:启动→" + fileName)
for (let i = 0; i < 12; i++) {
log('→起飞'.padStart(i * 2 + 3, '-'));
}
// 执行主程序
engines.execScriptFile("./" + fileName, {
delay: 2000
});
//退出本线程
exit();
} else {
console.error('重启失败');
wait(() => false, 2000);
exit();
wait(() => false, 2000);
}
}
实际上用的还是auto.stop(); auto.start();auto(true)这几个。 但我发现在开启root,或者“修改安全设置”权限的情况下,配合下面这两货,就不用去跳转到无障碍设置页面了:
AutoJS6→设置:
1、使用修改安全设置权限自动启用无障碍服务
2、使用root权限自动启用无障碍服务
只不过重新检测 log(auto.isRunning())和log(auto.service)有延迟,立刻打印结果依旧是没有开启无障碍服务,如果sleep(2000)肯定能看到想要的结果,就像这样:
log(auto.isRunning())
log(auto.service)
auto.stop();
sleep(2000)
log(auto.isRunning())
log(auto.service)
auto.start();
sleep(2000)
log(auto.isRunning())
log(auto.service)
为了避免有其它未知的问题,我所幸让程序重启了,这才有了restart() 函数。 var restart_time = storages.create('restart_time'); 只是为了标记是否已重启过了,可以用其它方式代替。
实际上,我上面if判断的条件还不够准确,我并不能够得知设置里那两个项目是否已开启,如果没开启,又会跳到无障碍服务设置页面😭😭😭😭
关于shizuku重启无障碍服务也没实现,设置里面没有关于shizuku的,不过shizuku可以用来开启“修改安全设置”权限,我觉得这样也更靠谱点,毕竟shizuku也很容易掉,“修改安全设置”开了之后就不会掉。
找到了,这两货:
if (Pref.shouldStartA11yServiceWithSecureSettings()) {
log('使用修改安全设置权限自动启用无障碍服务,[已开启]')
canRestarAuto = 1;
} else {
log('使用修改安全设置权限自动启用无障碍服务,[未开启]')
}
if (Pref.shouldStartA11yServiceWithRoot()) {
log('使用 root 权限自动启用无障碍服务,[已开启]')
canRestarAuto = 1;
} else {
log('使用 root 权限自动启用无障碍服务,[未开启]')
}
自己写了另一套方案,不过这一套方案需要依赖无障碍服务AutoJS6的serviceId,这个id只有在已启用无障碍服务的时候才能查询到,可能根据AutoJS6的版本变化会有变化。
另外,必须依赖root权限、shizuku权限、修改安全设置权限,才能执行。 1、serviceId 2、root权限、shizuku权限、修改安全设置权限
一、serviceId 我查询到AutoJS6 6.6.4版本的serviceId,所以我将它定义为默认值:
var serviceId = "org.autojs.autojs6/org.autojs.autojs.core.accessibility.AccessibilityServiceUsher";
我设计为:当无障碍服务权限正常的时候,动态读取serviceId,并存储到本地文件,并修改默认值。 毕竟无障碍权限掉线不是常态,通常第一次运行都会手动开启无障碍权限,这时候存储到文件即可。 所以定义一个存储文件路径:
var serviceId_file = "./tmp/service_id.txt"
然后需要实现动态查询的逻辑:
// 写入服务id
function writingServiceId() {
let id = getServiceId();
if (!id) serviceId = id;
//写入文件
files.write(serviceId_file, serviceId, "utf-8");
}
// 在已启动无障碍的条件下,查询服务id
function getServiceId() {
try {
// Android 8.0+ 标准方式
if (device.sdkInt >= 26) {
let am = context.getSystemService("accessibility");
let services = am.getEnabledAccessibilityServiceList(-1);
for (let i = 0; i < services.size(); i++) {
let id = services.get(i).getId();
if (id.startsWith("org.autojs.autojs6/")) return id;
}
return null;
}
// Android 7.x 反射调用
let Settings = android.provider.Settings.Secure;
let enabledServices = Settings.getString(
context.getContentResolver(),
"enabled_accessibility_services"
);
let match = enabledServices.match(/org\.autojs\.autojs6\/[\w\.]+/);
return match ? match[0] : null;
} catch (e) {
console.error("查询失败:", e);
return null;
}
}
所以,在查询到无障碍权限正常的情况下,调用writingServiceId()就完成了serviceId的初始化。 即便没有查询到,至少有初始化的默认值。如果默认值不对,大不了重启无障碍功能失败。
二、root权限、shizuku权限、修改安全设置权限的条件下,分别实现重启无障碍权限。 我直接贴实现代码:
//1.读取服务id
function readdingServiceId() {
if (files.exists(serviceId_file)) {
serviceId = files.read(serviceId_file, "utf-8");
}
// log(serviceId)
return serviceId;
}
// 2. Root 权限重启无障碍服务
function restartAccessibilityByRoot() {
if (!autojs.isRootAvailable())
return;
readdingServiceId();
// 获取当前已启用的服务列表
let enabledServices = shell("su -c 'settings get secure enabled_accessibility_services'", true).result;
// 移除目标服务(确保彻底关闭)
let newServices = enabledServices.replace(serviceId, "").replace(/::+/g, ":").replace(/^:|:$/g, "");
shell(`su -c 'settings put secure enabled_accessibility_services "${newServices}"'`, true);
sleep(1000); // 等待系统卸载服务
// 重新追加服务 ID 并激活全局开关
shell(`su -c 'settings put secure enabled_accessibility_services "${newServices}:${serviceId}"'`, true);
shell("su -c 'settings put secure accessibility_enabled 1'", true); // 强制开启总开关
}
// 2. Shizuku 权限重启无障碍服务
function restartAccessibilityByShizuku() {
if (!shizuku.hasPermission() ||
!shizuku.isOperational())
return;
readdingServiceId();
// 获取当前已启用的服务列表
let enabledServices = shizuku("settings get secure enabled_accessibility_services").result;
// 移除目标服务 ID
if (enabledServices.includes(serviceId)) {
enabledServices = enabledServices.replace(serviceId, "").replace(/::+/g, ":").replace(/^:|:$/g, "");
shizuku(`settings put secure enabled_accessibility_services "${enabledServices}"`);
sleep(1000);
}
// 避免重复添加
if (!enabledServices.includes(serviceId)) {
enabledServices += (enabledServices ? ":" : "") + serviceId;
shizuku(`settings put secure enabled_accessibility_services "${enabledServices}"`);
}
// 强制开启全局开关
shizuku("settings put secure accessibility_enabled 1");
}
// 2. 修改安全设置权限,重启无障碍服务
function restartAccessibilityService() {
if (!autojs.canWriteSecureSettings())
return;
readdingServiceId();
const contentResolver = context.getContentResolver();
// 获取当前启用的服务列表(避免覆盖其他服务[6](@ref))
const keyServices = android.provider.Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES;
const keyEnabled = android.provider.Settings.Secure.ACCESSIBILITY_ENABLED;
let enabledServices = android.provider.Settings.Secure.getString(contentResolver, keyServices) || "";
// 移除目标服务(清理残留符号)
let newServices = enabledServices
.replace(serviceId, "")
.replace(/::+/g, ":")
.replace(/^:|:$/g, "");
// 先禁用服务(触发系统卸载)
android.provider.Settings.Secure.putString(contentResolver, keyServices, newServices);
sleep(1000); // 等待系统生效
// 重新添加服务并强制开启全局开关
android.provider.Settings.Secure.putString(contentResolver, keyServices, newServices + ":" + serviceId);
android.provider.Settings.Secure.putString(contentResolver, keyEnabled, "1");
}
3个函数对应3个权限,每个函数开头第一个if...return;是因为权限未通过。 为确保serviceId更新,所以每个函数都读取一次readdingServiceId();
三、整合代码 不贴太长的代码,就贴个大概,能理解就行。
var serviceId = "org.autojs.autojs6/org.autojs.autojs.core.accessibility.AccessibilityServiceUsher";
var serviceId_file = "./tmp/service_id.txt"
if (auto.isRunning() && auto.service && auto.root) {
log("无障碍服务,[已启用]");
// 写serviceId
writingServiceId();
} else {
console.error("无障碍服务,[未启用]");
// 根据不同的权限,执行响应的重启无障碍服务逻辑
if (autojs.canWriteSecureSettings()) {
log("修改安全设置权限,[已启用]");
restartAccessibilityService();
}
if (autojs.isRootAvailable()) {
log("Root授权,[已授权]");
restartAccessibilityByRoot();
}
if (shizuku.hasPermission() &&
shizuku.isOperational()) {
log("Shizuku授权,[已授权]");
restartAccessibilityByShizuku();
}
}
大概就是这样。
最终我是两套方案一起用,就是前面回帖
AutoJS6→设置:
1、使用修改安全设置权限自动启用无障碍服务
2、使用root权限自动启用无障碍服务
总结:root权限、shizuku权限、修改安全设置权限,3个权限必须至少存在一个
看起来是一套十分详尽的 Rhino 脚本层面实现的无障碍服务控制代码. 突然回想起刚接触 Auto.js 时, 对文档中不存在的 API 探索并乐此不疲进行试错的新奇感觉.
无障碍服务在 AutoJs6 中已经做了一些封装, 但至今没有形成真正系统化的简洁方案, 反而似乎在 Auto.js 4.x 的基础上进一步增加了复杂程度 (包括可能的过度封装, 以及功能重复的代码等).
目前, 在应用层面, 无障碍服务有时会需要手动去主页抽屉进行开关重置, 尤其是应用冷启动之后.
在脚本层面, 可以使用 auto(true); 强制重启无障碍服务. 运行上述代码时, 你可以看到 AutoJs6 主页抽屉的无障碍服务开关有变化.
至于标题所描述的服务经常异常, 暂时还没有一个切实可行的办法. 能给出的方案, 无非是应用层面的后台自启和节电策略等方面的设置检查.