Android 开发4 min1 次浏览

让记账住进超级岛:QiNote 的四级通知策略

HyperOS 焦点通知不是想上就能上。QiNote 用能力探测 + Shizuku 特权通道 + Android 16 Live Updates + 常驻通知组成四级降级链,任何设备都有得用,且探测层永不崩溃。

让记账住进超级岛:QiNote 的四级通知策略
目录(展开)

「奇记 QiNote」系列第三篇:记账结果如何"住进" HyperOS 超级岛——焦点通知协议的权限探测、Shizuku 特权通道,以及覆盖所有设备的四级降级策略。

让记账住进超级岛

问题:通知的"最后一厘米"

记账是典型的高频低重要性操作:AI 解析中、成功、失败,都值得让用户知道,但不值得每次都点亮整个通知栏。小米 HyperOS 的"超级岛"(焦点通知 / Focus Notification)正好是这种场景的答案——信息以胶囊形式悬浮在屏幕顶部,实时刷新又不打扰。

问题是,超级岛不是想上就能上的。它有一套白名单机制:普通应用默认无权发送焦点通知。奇记为此设计了一条完整的策略链——优先走官方通道,没权限就借道 Shizuku,再不行退回系统标准通知,确保任何设备上都有得用。

设备能力探测

一切决策的前提是搞清楚"这台设备到底支持什么"。DeviceCapabilityDetector 用四种手段分别探测:

// 1. 是否小米/红米 + 焦点通知协议版本 >= 3(即 HyperOS 3+)
fun getFocusProtocolVersion(): Int =
    Settings.System.getInt(resolver, "notification_focus_protocol", 0)

fun isHyperOS3Plus(): Boolean =
    isXiaomiDevice() && getFocusProtocolVersion() >= 3

// 2. 超级岛系统特性是否开启(反射读系统属性)
method.invoke(null, "persist.sys.feature.island", false)

// 3. 当前应用是否已有焦点通知白名单权限
val uri = Uri.parse("content://miui.statusbar.notification.public")
context.contentResolver.call(uri, "canShowFocus", null, extras)
    ?.getBoolean("canShowFocus", false) ?: false

// 4. Shizuku 是否可用且已授权
Shizuku.pingBinder() && Shizuku.checkSelfPermission() == PERMISSION_GRANTED

这四项探测各自都有 try-catch 兜底并返回安全默认值——厂商定制系统上反射和隐藏 ContentProvider 随时可能失败,探测代码本身绝不能成为崩溃源。

Shizuku 特权通道

当设备支持超级岛、但应用不在白名单时,奇记借道 Shizuku:它以 ADB/shell 特权运行,而 shell 身份天然在焦点通知白名单里。ShizukuWhitelistBypass 的核心只有十几行:

fun notifyAsShell(context: Context, id: Int, channelId: String, n: Notification): Boolean {
    if (!isShizukuReady()) return false
    val shellContext = ShizukuContext(context)  // 以 shell 身份构造上下文
    val nm = shellContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    nm.notify(id, n)
    return true
}

同时注册了 OnBinderDeadListener——Shizuku 进程死亡时通过回调通知上层切换到降级策略,而不是让通知静默消失。

四级策略解析

NotificationStrategyResolver 把上面的能力拼成一条优先级链:

flowchart TD
    A["开始解析"] --> B{"HyperOS 3+ 且超级岛开启?"}
    B -- 否 --> G
    C{"已获焦点通知权限?"} -- 是 --> D["1️⃣ SUPER_ISLAND_DIRECT<br/>直接发焦点通知"]
    C -- 否 --> E{"Shizuku 就绪?"}
    E -- 是 --> F["2️⃣ SUPER_ISLAND_SHIZUKU<br/>以 shell 身份发焦点通知"]
    E -- 否 --> G{"Android 16+?"}
    G -- 是 --> H["3️⃣ LIVE_UPDATES<br/>Live Updates 进度通知"]
    G -- 否 --> I["4️⃣ ONGOING_NOTIFICATION<br/>常驻通知"]

几个工程细节:

  • 结果缓存:resolve() 首次计算后缓存,避免每次发通知都反射读系统属性;设置页开关变化时调用 invalidateCache() 强制重算;
  • 用户意愿优先:即使设备支持超级岛,用户关闭该开关后直接跳到系统通知,不越权;
  • 版本分流:Android 16(API 36)以上走新的 Live Updates 通知样式,以下走传统常驻通知。

状态机与通知样式

通知内容跟随记账状态机流转:PARSING → SUCCESS / FAILED。Android 16+ 上使用进度样式呈现"解析中"的不确定进度条,成功后展示「记账成功 · -¥28.5」与分类信息,失败时附加重试按钮(PendingIntent 直接重发解析请求):

BookkeepingState.PARSING -> builder.setContentTitle("AI记账解析中").setProgress(100, 0, true)
BookkeepingState.SUCCESS -> builder.setContentTitle("记账成功 · $amountDisplay")
BookkeepingState.FAILED  -> { builder.setContentTitle("记账失败"); retryIntent?.let { builder.addAction(...) } }

BookkeepingForegroundService 与 notification_strategy 的组合保证了:无论 App 在前台被记账、还是后台静默解析,状态更新都有统一出口。

小结

这套设计把"能不能上超级岛"从一个二元问题变成了四级连续谱:官方权限 → Shizuku 特权 → Android 16 Live Updates → 传统通知。每一级都独立可测(NotificationStrategyResolver 是纯函数式注入 DeviceCapabilityDetector 的,单测无需真机),而且探测层被刻意做成"永不崩溃"——在碎片化的国产 ROM 环境里,克制比激进更重要。

相 关 文 章

3 篇

已运行 —北京时间 --:--:--

© 2026 风起|永远相信美好的事情即将发生!