Skip to content

framework: keep super-island Focus notification across swipe-up-clean#1599

Open
GreenTeodoro839 wants to merge 3 commits into
ReChronoRain:mainfrom
GreenTeodoro839:pr/keep-focus-on-swipe
Open

framework: keep super-island Focus notification across swipe-up-clean#1599
GreenTeodoro839 wants to merge 3 commits into
ReChronoRain:mainfrom
GreenTeodoro839:pr/keep-focus-on-swipe

Conversation

@GreenTeodoro839

Copy link
Copy Markdown

背景

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)   ← 返回 false
  → 外卖通知被清掉
  → SystemUI FocusNotificationController.onNotificationRemoved
  → DynamicIslandWindowViewController.removeDynamicIslandView
  → 超级岛消失

HyperOS 自身在 NotificationManagerServiceImpl.skipClearAll 里已经给可更新 Focus 通知做了清除豁免,但只覆盖 reason 3 / 9 / 11没覆盖 reason 5(PACKAGE_RESTARTED)。这是系统侧设计断点。

修复方式

新增 KeepFocusOnSwipe,在 system_server 里加两个 hook:

  1. ProcessCleanerBase.tryToForceStopPackage — reason 以 "SwipeUpClean" 开头时给目标 pkg 打一个 15s TTL 的短期标记,把"最近任务划掉"和"手动强停 / 应用崩溃"等其他 force-stop 来源区分开。

  2. NotificationManagerServiceImpl.skipClearAll — 当 reason == 5、pkg 命中刚才的标记、且 record 是可更新 Focus 通知时,强制 setResult(true)

「可更新 Focus 通知」的判定完全镜像 HyperOS 自己在 miui.systemui.notification.focus.template.FocusTemplate 里的 gate:

updatable = (param.optBoolean(""updatable"", false)
              || scene == FOOD_DELIVERY
              || scene == CAR_HAILING)
            && hasPermission();

主判定查 Settings.Secure[""updatable_focus_notifs""](SystemUI 的 FocusCoordinator 把所有被系统接受为可更新的 Focus key 都同步在这里);兜底直接读 miui.focus.param JSON,避免 secure setting 有刷新延迟时漏判。

不会影响的场景

  • 用户在通知栏手动划掉通知:不经过 skipClearAll,照旧消失
  • 用户在「设置 → 应用 → 强行停止」强停 App:没有 SwipeUpClean 标记
  • 同一个 App 的普通营销/活动通知:没有 miui.focus.param,照旧清除
  • 普通 Android setLiveUpdate / promoted ongoing:不带 miui.focus.param 且依赖 App 自身后台更新 RemoteViews,划掉后保留无意义
  • 一次性 Focus 场景(verifyCode / timer / smartHomeAlert 等没声明 updatable 的)

UI

新增开关:系统框架 → 其他 → 显示与通知 → 划掉应用后保留超级岛(紧邻已有的"移除上层显示通知")。中英文都加了,默认关。

测试环境

  • 设备:Xiaomi pudding
  • 系统:OS3.0.309.0.WPCCNXM(HyperOS 3 / Android 16 / SDK 36)
  • 框架:LSPosed

签名核对依据均来自该设备 pull 出来的 miui-services.jar / services.jar / MIUISystemUIPlugin.apk,与代码里的反射查找一致。

选项分类

@HookBase(targetPackage = ""system"", minSdk = 36) —— 跟同文件其他 reason-5/通知相关 hook 一致,挂在 SystemFrameworkB

修复 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 验证)。
Copilot AI review requested due to automatic review settings May 25, 2026 14:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 KeepFocusOnSwipe hook with a short-lived “swipe” mark to differentiate swipe-clean from real force-stop flows.
  • Wires the hook into SystemFrameworkB behind 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"));

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Comment on lines +165 to +172
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<>();
@codacy-production

codacy-production Bot commented May 25, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 1 medium

Alerts:
⚠ 1 issue (≤ 0 issues of at least minor severity)

Results:
1 new issue

Category Results
Complexity 1 medium

View in Codacy

🟢 Metrics 59 complexity

Metric Results
Complexity 59

View in Codacy

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants