写 RoutineX 的时候,我一直在对照三星。同样一件事,三星的系统设置里点几下就能定时执行别人 App 的快捷方式,小米上我得写个应用、要 root、还得处理锁屏。
差别到底在哪,我决定把三星那两个包装下来拆开看。
分析的版本是 S25 Ultra 上的 com.samsung.android.app.routines 5.0.03.22 和 com.samsung.android.app.routineplus 1.1.28。下面是一路看下来的结论。
一、它是特权应用,这一点不意外
package:/system/priv-app/Routines/Routines.apk
sharedUserId = android.uid.systemui
priv-app 是系统镜像里的特权目录。更有意思的是 sharedUserId:它和 SystemUI 共用一个进程身份,也就是平台签名加上高权限 uid。
manifest 里申请的东西也印证了这一点:
| 权限 | 能干什么 |
|---|---|
WRITE_SECURE_SETTINGS |
直接改亮度、屏幕超时、旋转这些系统设置 |
REAL_GET_TASKS |
看到真实的最近任务,判断当前前台是哪个应用 |
START_ACTIVITIES_FROM_BACKGROUND |
后台直接拉起界面,不受 Android 10 之后的后台启动限制 |
BLUETOOTH_SCAN / CHANGE_WIFI_STATE |
无线相关的条件和动作 |
到这里还算常规。真正让我改主意的是下一步。
二、快捷方式能执行,靠的不是权限
我原本的猜测很简单:系统 uid 大概能绕过那个 signature 级权限检查。比亚迪快捷方式指向的 Activity 挂了个 com.byd.aeri.jump.permission,我们普通应用调 startActivity 会被直接拒掉,root 却能过。三星大概也是同理。
然后我查了这个权限的持有者:
Permission [com.byd.aeri.jump.permission]
sourcePackage=com.byd.aeri.caranywhere
prot=signature
底下一条 granted 记录都没有。整个系统里没有任何一个包持有它。
可桌面上长按比亚迪图标、点「每日签到」,是能用的。这说明快捷方式的启动根本不走普通 startActivity 那条权限检查路径。
Android 给快捷方式单独留了一条通道。应用注册快捷方式时,把自己的一个 Intent 交给系统保管;启动器通过 LauncherApps.startShortcut() 让系统去启动它。真正执行的时候,系统是以发布者自己的身份发起的,所以不会撞上发布者自己声明的签名权限。
这个设计说得通:快捷方式是应用主动暴露给启动器的入口,用户长按点它,本来就该能进去。
三星做的事就是调这条通道。它是特权应用,天然有启动器级别的访问权。我们不是桌面,拿不到这个权限,只能退回去用 root 发 Intent。
顺着这个结论,我马上想验证一件事:普通应用能不能也走这条通道。如果能,root 就只需要用来做一次性的枚举,日常执行可以完全不用。
写了个探针应用,把 getShortcuts() 和 startShortcut() 各试一遍。源码在 https://github.com/z7ui/RoutineX/tree/main/probe,想在自己机器上复现的可以直接编。
hasShortcutHostPermission=false
getShortcuts 失败: SecurityException: Caller can't access shortcut information
startShortcut 失败: SecurityException: Caller can't access shortcut information
两条路都被同一个异常挡住。这套接口只对启动器开放,第三方应用得先拿到桌面角色或者 ACCESS_SHORTCUTS 这种特权权限才行。root 省不掉,读取和执行都得要。
所以结论还是那个:三星能这么做,靠的是它站在系统里。我们站在外面,只能用 root 借钥匙。
三、怎么把功能目录挖出来
一开始我想逐个反编译界面,太慢。后来找到一条捷径。
先拿类表:
apkanalyzer dex packages Routines.apk | awk '$1=="C"{print $NF}'
四万六千个类,其中三星自己的有三千六百多个。翻的时候注意到一个包:
com/samsung/android/app/routines/preloadproviders/<能力域>/<子类>/actions|conditions/<实现类>
每个子目录对应一个能力域,目录名就是功能名。把这个包的类名全抠出来,按目录分组,完整清单就出来了,一共 241 个类。比看界面快得多,而且能看到界面上没暴露的东西。
中文文案也顺手导了一份,aapt2 dump resources 出来 2393 条 zh-CN 字符串,用来核对每个能力的实际叫法。
四、它到底能做什么
条件这边,覆盖得比我预想的宽:
| 类别 | 具体条件 |
|---|---|
| 时间 | 特定时间、时间段、日出日落(含「日出到日落」这种区间)、睡眠时间表 |
| 地点 | 到达或离开某个位置 |
| 网络 | Wi-Fi 连上断开(可指定名字)、蓝牙连某设备、移动数据、热点、飞行模式、NFC |
| 设备 | 充电中、电量低于某个百分比、有线耳机插入、折叠屏开合、省电模式 |
| 通知 | 收到指定应用的通知 |
| 应用 | 某个应用被打开 |
| 健康 | 健身中、今日步数、活动卡路里 |
| 通话短信 | 来电、收到短信、快速拒接 |
| 系统 | 免打扰、声音模式、深色模式、屏幕方向、DeX、车载 Android Auto |
| 日历 | 公共假日、工作日 |
| 闹钟 | 叫醒闹钟响了 |
动作这边有三十多个分组、两百多个具体动作。挑几个值得说的:
- 流程类有条件分支、延迟执行、二次确认弹窗、自定义弹窗
- 显示类细到字体大小、屏幕分辨率与流畅度、缩放、外屏亮度
- 声音类里有分应用音量
- 功能类有打开网址、复制到剪贴板、TTS 朗读文本
- 通知类能朗读通知内容
- 还有获取剪贴板、显示图片、倒数日、调整系统时间
五、「日常程序+」才是真正有想法的部分
这个包没在系统镜像里,是 Galaxy Store 推下来的普通 App,18MB。但它拿到了一堆三星自家签名权限,做出来的是本体没有的东西。
| 能力 | 说明 |
|---|---|
| 触控宏 | 录制一串点击和滑动,触发时按顺序回放。可设重复次数、间隔、开始前延迟 |
| 按钮操作 | 侧键双击长按、音量键、导航键触发例程,会覆盖系统原本的按键行为 |
| 指纹触发 | 指定某根手指解锁时运行例程 |
| 隔空操作 | S-Pen 悬空手势:画圈、上下左右、摇晃 |
| 方向键 | 用手腕动作模拟方向键 |
| 请求 REST API | 例程可以直接发 HTTP 请求 |
| 二维码分享 | 例程导出成 JSON 压成二维码,别人扫码导入 |
| 重复规则扩展 | 每月某日、日期尾数(每月 3 号、13 号、23 号) |
| Just in Routine | 用户自己做的卡片,显示在即时简报里 |
例程上限二十个。这个限制挺有意思,像是故意留的。
六、如果要在小米上做,哪几个值得抄
看完这一圈,我心里的排序是这样的。
条件分支排第一。这是整套系统里最有价值的抽象。现在 RoutineX 只能说「到点做一件事」,加上分支就能表达「连着家里的 Wi-Fi 就静音,否则正常」。存储结构要从一条任务改成任务树,越早改越好。
触控宏排第二。小米上没有同类东西,做成独立应用也有人用。有 root 可以用 input 注入,没 root 就用无障碍手势。
收到指定通知就触发排第三。一个 NotificationListenerService 就够了,不需要 root。典型用法是收到某个人的消息时自动做点事。
延迟执行加二次确认成本最低。延迟就是多排一个闹钟,二次确认就是一个弹窗,但能挡掉很多误触发。
再往后是分应用音量、日出日落触发、每月某日这类。都不是难点,属于有空就补。
至于指纹触发和 S-Pen 隔空,抄不了。Android 不给第三方应用区分具体哪根手指,小米也没有对应的硬件事件。应用配对分屏理论上能做,但各家 ROM 实现差异太大,覆盖率很难保证。
七、最后
拆完之后最大的感受是,三星这套东西的宽度是靠生态堆出来的。它有两百多个动作,是因为它能改系统设置、能读健康数据、能连智能家居、能调自家 AI。这些能力大多是系统本身提供的,它只是把入口接到了例程里。
第三方应用做同样的事,只能在有限的地方用力。所以 RoutineX 不打算对标它的全部,先把时间触发和快捷方式执行做扎实,再往上加两三个高价值的能力,就够了。
分析用的脚本和中间产物我留在本地了,包括类清单、中文资源表和能力目录。要折腾别的系统应用,这套方法能重复用。
发表回复