三星的「模式和日常程序」里有个能力,我一直很羡慕:定时执行某个 App 长按图标弹出来的快捷方式。比亚迪 App 的每日签到就藏在长按菜单里,三星能把它挂到早上七点半,手机自己点。
小米的自动任务做不到这件事。它只能打开应用、开关设置、发通知,碰不到应用内部的快捷方式。所以想自动签到,剩下两条路:用无障碍模拟点击,或者让系统替我把那个快捷方式按下去。前一条要一直开着无障碍,页面一改版就失效;后一条干净得多,前提是能把快捷方式找出来并且执行掉。
这个应用做的就是后一条。项目在 https://github.com/z7ui/RoutineX。
一、快捷方式藏在哪
先试最直接的办法,让系统自己报出来:
adb shell cmd shortcut get-shortcuts --flags 15 com.byd.aeri.caranywhere
四条都在,名字也对。但 intent 那一段被截断成了省略号,最关键的参数拿不到。cmd shortcut 只能查,不能启动,等于看得到吃不到。
完整的定义在系统里放着:
/data/system_ce/0/shortcut_service/packages/<包名>.xml
这个文件是二进制封装的,但里面的字符串是明文。用 root 读出来,扫一遍就能把 URI 抠出来。比亚迪那四条是这样的:
车辆位置 path_jump_navi_host/...
车门解锁 path_jump_navi_host/...
车门上锁 path_jump_navi_host/...
每日签到 path_jump_navi_host/...
路径完全一样,差别全在后面挂的那串参数。解码之后看到这么一段:
"method":"daySign=="
"data":{"0":"C00AU","1":"","2":"每日签到"}
daySign 就是签到的方法名。四条快捷方式其实是同一个入口,靠参数区分动作。这也解释了为什么长按菜单里那四项看起来一模一样,行为却完全不同。
有个小坑值得记一下。参数里的 JSON 是转义过的,长这样:"2":"每日签到"。我第一版正则没处理这层反斜杠,抓出来的名字全是 jump navi host,四个快捷方式在界面上长得一模一样,根本分不清哪个是签到。
二、能打开,不等于签上了
拿到 URI 之后我很自然地写了个 startActivity,结果直接抛 SecurityException。
查下来是这个 Activity 挂了自定义权限:
com.byd.aeri.jump.permission
而且是 signature 级别。意思是只有和比亚迪用同一张证书签名的应用才能调它。普通应用进不去,用 adb shell 也不行,一样被拒。
signature 权限拦的是普通进程,root 不在这个体系里。所以执行那一步改成走 root:
su -c "am start -a android.intent.action.VIEW -f 0x10000000 -d '<完整 URI>'"
第一次跑通的时候,界面上跳出「已连续签到 446 天,积分 +1」,当天那格打了个勾。那一刻才确定这条路是通的。
中间试过一条看起来更优雅的路:把 URI 里的 path_jump_navi_host 改成 path_jump_navi。这个 Activity 没有挂权限,普通应用能起。代价是语义全丢了,它只会把比亚迪的主界面打开,签到那个参数被忽略。所以还是得回到 root。
三、锁屏那 55 秒
到点执行这一半做完,剩下的一半是「到点时手机还锁着怎么办」。这也是三星那个功能让我觉得贴心的地方:它不会因为锁屏就跳过,而是等你下次解锁再补上。
我第一版的做法是注册一个 ACTION_USER_PRESENT 广播,收到就执行。测试那天定在 19:26,锁屏,等解锁。
结果通知出来了,任务没执行。
日志把过程记得很清楚:
19:26:00.020 闹钟触发
19:26:00.025 locked=true → 挂起等待
19:26:00.038 开始轮询锁屏状态
19:26:55.333 轮询发现已解锁
19:26:55.364 root am start exit=0
闹钟 19:26:00 准点响了,人也确实是在 19:26:55 解锁的。中间那 55 秒服务一直挂着,但一次都没醒。
问题出在投递时机上。MIUI 把这个闹钟压到了解锁那一刻才送到应用手里。等我的服务启动、注册好广播接收器,系统的解锁广播早就发完了。接收器注册得比广播晚,自然一辈子等不到。
只监听广播这种方案有个结构性缺陷:它假设「事件发生时我听得见」,而锁屏加省电策略恰恰让应用听不见。
现在的做法是两条腿走路:
- 广播当快路径,正常情况下立刻响应
- 每 500ms 轮询一次,判断条件是
KeyguardManager.isKeyguardLocked() || !PowerManager.isInteractive()
不看信号,只看状态。状态变了就执行。改完之后解锁到启动比亚迪之间是毫秒级。
顺带说一句日志。一开始全打在 logcat 里,后来发现 MIUI 上 logcat | grep 包名 经常抓不到自己应用的行,排查等于没有。现在改成落盘到 files/rx.log,用 adb shell run-as com.z7ui.routinex cat files/rx.log 读。这个文件默认不写,设置里手动打开才记。
四、现在长什么样
主界面是任务卡片,一张卡一条任务:时间、名称、重复规则、目标应用和快捷方式,右边一个开关。点卡片进编辑页,可以改时间、改重复的星期、重新选应用和快捷方式,还有一个「测试执行一次」,不用等到点就能验证。
设置区三项:运行日志开关、清除日志(右边实时显示占用)、关于。
日志默认关闭是有意的。这功能平时不需要,开着反而是个负担,需要排查时再打开。
五、它做不到什么
先说清楚边界,省得有人装完发现不能用。
只有动态快捷方式能读出来。应用自己在运行时用 ShortcutManager 注册的那种才行;写死在 APK 里的静态快捷方式,系统文件里没有对应的 URI 记录,读不到。
必须 root。读快捷方式要 root,执行要 root,因为绕不过那个 signature 权限。没 root 的设备只能做到「到点打开某个应用」。
带时效性参数的快捷方式会失效。有些应用的 URI 里塞了会过期的 token,过一阵子得重新选一次。
只在一台小米 17 Pro 上实测过。原生 Android 理论上也能跑,但没验证。
装起来
# 需要 JDK 17+ 和 Android SDK(compileSdk 37)
.gradlew.bat assembleDebug
产物在 app/build/outputs/apk/debug/app-debug.apk。
装上之后要做四件事:给 root、给通知权限、给精确闹钟、把电池优化设成无限制。小米还要去「应用设置 → 自启动」里放行,不然后台服务会被清掉。主界面会把还缺的项列出来,点按钮能直接跳到对应设置页。
代码和说明都在 https://github.com/z7ui/RoutineX。
项目作者:Deepseek · 视频 B站 @z7ui
1.3:执行成功 ≠ 签到成功(2026-10-29 更新)
用了一周多,回头对账发现 10-25、10-27 两天漏签了。可日志里那两天写得清清楚楚:
10-25 11:07:49 执行(来自 poll):每日签到
10-25 11:07:49 root am start exit=0 out=Starting: Intent { ... }
10-25 11:07:49 执行结果=true
问题就在这个「true」上:am start 返回 0,只代表快捷方式被系统接受了,不代表比亚迪那边签到真的完成。应用被拉起来、页面还没渲染完就被切走,或者停在登录态,进程这边一无所知。
所以 1.3 做了两件事。
一、执行后校验
拉起之后等 2.5 秒,用 root 跑一次 uiautomator dump,把界面文字抓下来:
- 出现「已签到 / 签到成功 / 今日已签」→ 成功;
- 出现「立即签到 / 去签到 / 点击签到」→ 没签上,重试一次再校验;
- 两类都没有 → 未确认。
最后一条是刻意的:宁可承认判断不了,也不谎报成功——上一版就是谎报成功,才让漏签藏了一周。
二、执行历史
日志是「排查用」的:会被清、会轮转、默认还关着。历史是「对账用」的,必须一直在。
现在每次执行都记一条,保留最近 30 天,在 App 里点「执行历史」直接看:
10-29 08:24 成功 解锁 每日签到
界面确认已签到
顺手把闹钟、解锁服务、测试按钮三条路径合并到一个执行入口,校验和记账只写一遍。
留下的坑
签到页的关键词是照界面文字推的。如果哪天界面改版、两个关键词都没命中,结果会落在「未确认」而不是「失败」——这时日志里会有一条 校验:done=false todo=false len=…,按它把新词补进去就行。
1.4:我猜的关键词,一个都不存在(2026-10-29 深夜)
1.3 装好之后第一次真机测试,结果是这样的:
22:52:21 root am start exit=0
22:52:25 校验:done=false todo=false len=814
22:52:25 执行结果=成功(已拉起,界面无法校验)
两个问题叠在一起。
一、814 字节是什么
完整加载后的签到页是 67 KB。814 字节是 WebView 还没渲染出来的空壳——我固定等 2.5 秒就抓,抓早了。
改成轮询:先等 2.5 秒给首帧,之后每 1.5 秒抓一次,最多 18 秒,一有明确结论立刻返回。实测第 1 轮还是空壳,第 2 轮就拿到了 67485 字节的完整页面。
二、关键词一个都不在
这是我这次最想记下来的一条:我照着”签到页应该有签到按钮”的想象造了两组词——「已签到 / 签到成功」算成功,「立即签到 / 去签到」算未完成。
真机抓下来,页面上一个字都没有。真实文案是:
每日签到
已连续签到 2 天
积分 +1
补签卡24张
那”今天签没签”体现在哪?在日历上今天那一格下面有没有一个勾。这个勾是个图标字体的私有区字符,不会以文字形式出现在关键词里。
好在同一页可以横向对照,这给了很硬的判据:
| 格子 | 下方节点 | 实际 |
|---|---|---|
| 今日(29) | 3 个 | 已签到 |
| 25、27 | 0 个 | 漏签 |
| 26 | 3 个 | 已签到 |
25、27 正是那两天漏签的日子。于是判据改成:找到日历里文字为「今日」的那一格(全页唯一),看它下面 90px 内有没有节点。认不出日历结构,才退回关键词兜底。
三、还有一个我自己的 bug
result = v == FAIL ? FAIL : OK; // UNKNOWN 也记成成功
「判断不了」被记成了「成功」。这和 1.3 想解决的那个问题(日志写成功、实际没签)是同一个错误的另一种形式——只不过上次是系统骗我,这次是我自己骗自己。
现在原样保留三态:看得到勾才叫成功,其余记「未确认」。
四、验证
23:00:06 am start exit=0
23:00:11 第 1 轮:日历没认出来(页面还没渲染)
23:00:15 第 2 轮:今日格 718,2047-2112 下方节点数=3 → 判定=成功 len=67485
历史里落了一条 1793286006896|1|测试|每日签到|界面确认已签到,主界面显示「最近一次:10-29 23:00 成功 测试 每日签到 · 界面确认已签到」。
教训
别猜界面上的字。 先 dump 一次,看清楚它到底写了什么,再写判据。我这次多花了一个版本的时间,代价只是把「已签到」换成「看日历上的勾」——但如果不做这一步,校验就会一直”活着但没用”,比没有校验更糟:它会给人一个已经检查过的错觉。
发表回复