AutoJs6 icon indicating copy to clipboard operation
AutoJs6 copied to clipboard

无障碍服务经常故障

Open wengzhenquan opened this issue 5 months ago • 6 comments

有2种现象,重新授权无障碍权限才能恢复。

1、左侧菜单,“无障碍服务”权限下面出现“无障碍服务故障”一行小字。 这个时候必须重新授权,程序才能跑了。

2、没有任何提示,auto.service 不为空,auto.waitFor()也没有反应。 但是布局分析失效,部分api也失效,比如UiSelector.findOne(); 必须重新授权无障碍服务才能恢复。

wengzhenquan avatar Jul 11 '25 19:07 wengzhenquan

第二种场景,试验出规律了,可能是个别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()的问题。

wengzhenquan avatar Jul 13 '25 07:07 wengzhenquan

这个问题似乎在三星手机上更容易复现,同一个脚本在小米,红米和1+手机上可以正常运行,但是三星手机就不行,似乎跟 #329 有些关系

Image Image

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;
}

YueMiyuki avatar Jul 13 '25 13:07 YueMiyuki

临时解决方案: 必须要有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也很容易掉,“修改安全设置”开了之后就不会掉。

wengzhenquan avatar Jul 19 '25 07:07 wengzhenquan

找到了,这两货:

if (Pref.shouldStartA11yServiceWithSecureSettings()) {
            log('使用修改安全设置权限自动启用无障碍服务,[已开启]')
            canRestarAuto = 1;
        } else {
            log('使用修改安全设置权限自动启用无障碍服务,[未开启]')
        }
if (Pref.shouldStartA11yServiceWithRoot()) {
            log('使用 root 权限自动启用无障碍服务,[已开启]')
            canRestarAuto = 1;
        } else {
            log('使用 root 权限自动启用无障碍服务,[未开启]')
        }

wengzhenquan avatar Jul 19 '25 09:07 wengzhenquan

自己写了另一套方案,不过这一套方案需要依赖无障碍服务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个权限必须至少存在一个

wengzhenquan avatar Jul 20 '25 01:07 wengzhenquan

看起来是一套十分详尽的 Rhino 脚本层面实现的无障碍服务控制代码. 突然回想起刚接触 Auto.js 时, 对文档中不存在的 API 探索并乐此不疲进行试错的新奇感觉.

无障碍服务在 AutoJs6 中已经做了一些封装, 但至今没有形成真正系统化的简洁方案, 反而似乎在 Auto.js 4.x 的基础上进一步增加了复杂程度 (包括可能的过度封装, 以及功能重复的代码等).

目前, 在应用层面, 无障碍服务有时会需要手动去主页抽屉进行开关重置, 尤其是应用冷启动之后. 在脚本层面, 可以使用 auto(true); 强制重启无障碍服务. 运行上述代码时, 你可以看到 AutoJs6 主页抽屉的无障碍服务开关有变化.

至于标题所描述的服务经常异常, 暂时还没有一个切实可行的办法. 能给出的方案, 无非是应用层面的后台自启和节电策略等方面的设置检查.

SuperMonster003 avatar Jul 29 '25 05:07 SuperMonster003