framework: keep super-island Focus notification across swipe-up-clean#1599
framework: keep super-island Focus notification across swipe-up-clean#1599GreenTeodoro839 wants to merge 3 commits into
Conversation
修复 HyperOS 3 一个具体问题:外卖订单上超级岛后,如果从最近任务里划掉
外卖 App,岛会立刻消失,即使订单仍在配送中。打车、共享单车等任何走系统
Focus / Live API 的实时状态都有同样问题。
根因
====
最近任务划掉 App 在 MIUI 这条路径上会触发 force-stop 和 PACKAGE_RESTARTED:
RecentsContainer.killProcess
→ ProcessManagerWrapper.doSwapUPClean (policy=7)
→ ProcessSceneCleaner.handleSwipeKill
→ ProcessCleanerBase.killOnce(level=104)
→ ProcessCleanerBase.tryToForceStopPackage(pkg, userId, "SwipeUpClean")
→ ActivityManagerInternal.forceStopPackage
→ 广播 PACKAGE_RESTARTED
→ NotificationManagerService.cancelAllNotificationsInt(..., reason=5)
→ NotificationManagerServiceStub.skipClearAll(record, 5)
HyperOS 自身在 NotificationManagerServiceImpl.skipClearAll 里已经给可更新
Focus 通知做了清除豁免,但只覆盖 reason 3 / 9 / 11,恰好 没有 覆盖
reason 5(PACKAGE_RESTARTED)。SystemUI 收到通知被移除后调用
removeDynamicIslandView,超级岛就掉了。
修复方式
========
双 Hook,都在 system_server:
Hook 1 ProcessCleanerBase.tryToForceStopPackage
在 reason 以 "SwipeUpClean" 开头时给目标 pkg 打一个 15s TTL 的
短期标记,跟手动强停、应用崩溃等其他 force-stop 来源区分开。
Hook 2 NotificationManagerServiceImpl.skipClearAll
当 reason == 5、pkg 命中刚才的标记、且 record 是「可更新 Focus
通知」时,强制 setResult(true) 让通知保留。
「可更新 Focus 通知」的判定完全镜像 HyperOS 自己在 FocusTemplate 里的
gate:
updatable = (param["updatable"] == true
|| scene == foodDelivery
|| scene == carHailing)
&& hasPermission()
主判定查 Settings.Secure["updatable_focus_notifs"](SystemUI 的
FocusCoordinator 把所有被系统接受为可更新的 Focus key 都同步在这里);
兜底直接看 miui.focus.param 的 JSON,避免 secure setting 有刷新延迟的极端
情况。覆盖:
- 美团 / 饿了么外卖
- 滴滴 / 高德 / 美团打车等
- 哈啰 / 美团 / 青桔等共享单车(行程中状态用 updatable:true 登记)
- 第三方导航、健身轨迹、视频通话等任何使用同一套系统 Focus / Live API
的 App
不会影响的场景
==============
- 用户在通知栏手动划掉通知:不经过 skipClearAll,照旧消失
- 用户在「设置 → 应用 → 强行停止」强停 App:没有 SwipeUpClean 标记
- 同一个 App 的普通营销/活动通知:没有 miui.focus.param,照旧清除
- 普通 Android setLiveUpdate / promoted ongoing:不带 miui.focus.param
且依赖 App 自身后台更新 RemoteViews,划掉后保留无意义,照旧清除
- 一次性 Focus 场景(verifyCode / timer / smartHomeAlert 等没声明
updatable 的):照旧清除
UI
==
开关位于「系统框架 → 其他 → 显示与通知 → 划掉应用后保留超级岛」,
紧邻已有的「移除上层显示通知」。中英文都加了。默认关,targetPackage=system,
minSdk=36(HyperOS 3 / Android 16,已在 pudding / OS3.0.309.0.WPCCNXM 验证)。
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a SystemFramework hook to prevent HyperOS “Super Island” (Focus) ongoing notifications from being cleared when the user swipes an app away from recents, and exposes the behavior as a toggle in settings.
Changes:
- Introduces
KeepFocusOnSwipehook with a short-lived “swipe” mark to differentiate swipe-clean from real force-stop flows. - Wires the hook into
SystemFrameworkBbehind a new preference flag. - Adds a new SwitchPreference and localized strings for the toggle.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
library/libhook/src/main/java/com/sevtinge/hyperceiler/libhook/rules/systemframework/others/KeepFocusOnSwipe.java |
Implements dual-hook logic to preserve updatable Focus notifications after swipe-clean. |
library/libhook/src/main/java/com/sevtinge/hyperceiler/libhook/app/SystemFramework/SystemFrameworkB.java |
Registers the new hook behind a preference gate. |
library/core/src/main/res/xml/framework_other.xml |
Adds a settings toggle for the feature. |
library/core/src/main/res/values/strings_app.xml |
Adds EN strings for the new toggle. |
library/core/src/main/res/values-zh-rCN/strings_app.xml |
Adds zh-CN strings for the new toggle. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| initHook(new LinkTurboToast(), PrefsBridge.getBoolean("system_framework_disable_link_turbo_toast")); | ||
| initHook(new AllowUntrustedTouchForU(), PrefsBridge.getBoolean("system_framework_allow_untrusted_touch")); | ||
| initHook(DeleteOnPostNotification.INSTANCE, PrefsBridge.getBoolean("system_other_delete_on_post_notification")); | ||
| initHook(new KeepFocusOnSwipe(), PrefsBridge.getBoolean("system_framework_keep_focus_on_swipe")); |
There was a problem hiding this comment.
False positive — PrefsBridge.wrap() (library/common/.../PrefsBridge.java:126) auto-prepends prefs_key_ when the supplied key is missing it:
private static String wrap(String key) {
return (key != null && !key.startsWith("prefs_key_")) ? "prefs_key_" + key : key;
}So PrefsBridge.getBoolean("system_framework_keep_focus_on_swipe") ends up reading under prefs_key_system_framework_keep_focus_on_swipe, matching the XML key. Every initHook(...) line in SystemFrameworkB follows the same convention — e.g. the line immediately above mine, DeleteOnPostNotification, reads "system_other_delete_on_post_notification" while its XML key is prefs_key_system_other_delete_on_post_notification.
Verified the toggle drives the hook end-to-end on pudding / OS3.0.309.0.WPCCNXM.
| public class KeepFocusOnSwipe extends BaseHook { | ||
|
|
||
| private static final String SWIPE_UP_CLEAN = "SwipeUpClean"; | ||
| private static final int REASON_PACKAGE_CHANGED = 5; |
| Context ctx = getSystemContext(); | ||
| if (ctx == null) return false; | ||
| String raw = Settings.Secure.getString(ctx.getContentResolver(), "updatable_focus_notifs"); | ||
| if (TextUtils.isEmpty(raw)) return false; | ||
| JSONArray arr = new JSONArray(raw); | ||
| for (int i = 0; i < arr.length(); i++) { | ||
| if (TextUtils.equals(key, arr.optString(i, ""))) return true; | ||
| } |
| private static final String SCENE_CAR_HAILING = "carHailing"; | ||
|
|
||
| /** pkg -> mark timestamp (ms). Shared between the two hooks. */ | ||
| private static final ConcurrentHashMap<String, Long> SWIPE_MARKS = new ConcurrentHashMap<>(); |
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| Complexity | 1 medium |
🟢 Metrics 59 complexity
Metric Results Complexity 59
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
- Memoize parsed Settings.Secure["updatable_focus_notifs"] (raw==raw fast path) so a swipe-clean burst doesn't re-read + re-parse for every record processed by skipClearAll. - Prune expired SWIPE_MARKS opportunistically on every mark() so the map stays bounded even when a marked pkg never triggers a follow-up skipClearAll (e.g. swiped while it had no notifications). - Clarify javadoc: the reason==5 constant is REASON_PACKAGE_CHANGED per NotificationListenerService; ACTION_PACKAGE_RESTARTED is the broadcast that triggers it. Avoids the naming inconsistency in the previous comments.
Adds a third hook on NotificationManagerService.cancelNotificationLocked (reason=12 / REASON_GROUP_SUMMARY_CANCELED) so that when the user swipes away the auto-grouped non-Focus notification group of an app, an updatable Focus notification that happens to ride in the same group is not torn down with it. Reuses the existing isUpdatableFocusNotification check; no SwipeMarker needed since this path is user-initiated and per-record. Trim the UI title + summary to one short line covering both scenarios (swipe app away, swipe notification group away). XML key unchanged so no migration needed.
背景
HyperOS 3 上有这样一个具体现象:外卖订单上了超级岛之后,如果用户从最近任务里把外卖 App 划掉,岛会立刻消失,即使订单还在配送中。打车、共享单车(哈啰/美团/青桔)、第三方导航、健身轨迹等任何走系统 Focus / Live API 的实时状态都有同样问题。
根因
最近任务划掉 App 在 MIUI 路径上会走 force-stop 触发 PACKAGE_RESTARTED:
HyperOS 自身在
NotificationManagerServiceImpl.skipClearAll里已经给可更新 Focus 通知做了清除豁免,但只覆盖reason 3 / 9 / 11,没覆盖 reason 5(PACKAGE_RESTARTED)。这是系统侧设计断点。修复方式
新增
KeepFocusOnSwipe,在system_server里加两个 hook:ProcessCleanerBase.tryToForceStopPackage— reason 以"SwipeUpClean"开头时给目标 pkg 打一个 15s TTL 的短期标记,把"最近任务划掉"和"手动强停 / 应用崩溃"等其他 force-stop 来源区分开。NotificationManagerServiceImpl.skipClearAll— 当reason == 5、pkg 命中刚才的标记、且 record 是可更新 Focus 通知时,强制setResult(true)。「可更新 Focus 通知」的判定完全镜像 HyperOS 自己在
miui.systemui.notification.focus.template.FocusTemplate里的 gate:主判定查
Settings.Secure[""updatable_focus_notifs""](SystemUI 的FocusCoordinator把所有被系统接受为可更新的 Focus key 都同步在这里);兜底直接读miui.focus.paramJSON,避免 secure setting 有刷新延迟时漏判。不会影响的场景
skipClearAll,照旧消失miui.focus.param,照旧清除setLiveUpdate/promoted ongoing:不带miui.focus.param且依赖 App 自身后台更新RemoteViews,划掉后保留无意义verifyCode/timer/smartHomeAlert等没声明updatable的)UI
新增开关:系统框架 → 其他 → 显示与通知 → 划掉应用后保留超级岛(紧邻已有的"移除上层显示通知")。中英文都加了,默认关。
测试环境
puddingOS3.0.309.0.WPCCNXM(HyperOS 3 / Android 16 / SDK 36)签名核对依据均来自该设备 pull 出来的
miui-services.jar/services.jar/MIUISystemUIPlugin.apk,与代码里的反射查找一致。选项分类
@HookBase(targetPackage = ""system"", minSdk = 36)—— 跟同文件其他 reason-5/通知相关 hook 一致,挂在SystemFrameworkB。