Skip to content

feat(nofitication): Live Updates 自定义设置(状态栏内容/颜色/图标) - #216

Open
WhiteMoon319 wants to merge 4 commits into
Aliothmoon:mainfrom
WhiteMoon319:style/LiveUpdates
Open

feat(nofitication): Live Updates 自定义设置(状态栏内容/颜色/图标)#216
WhiteMoon319 wants to merge 4 commits into
Aliothmoon:mainfrom
WhiteMoon319:style/LiveUpdates

Conversation

@WhiteMoon319

@WhiteMoon319 WhiteMoon319 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

关联 Issue

无独立 Issue。需求来源:Live Updates 通知此前为固定样式,用户要求可自定义
状态栏 chip 内容、进度条配色与追踪图标。

变更摘要

  • 新增设置二级页(Live Updates),支持启用开关、状态栏显示内容 5 种方案
    (进度+任务名/仅进度/仅任务名/最新日志/不显示)
  • 进度条颜色支持默认语义色、6 个单色预设与自定义色板(HEX 持久化)
  • 进度条图标支持合成玉/MAA/圆点内置方案与自定义图片
    (PNG/JPG/WebP/GIF/SVG/XML,统一解码器 + AndroidSVG 矢量渲染)
  • 自定义图标按「类型+路径」缓存,通知每秒刷新零文件 IO;首次/变更时
    Dispatchers.IO 异步解码,失败自动回退默认图标
  • 配置备份导出/导入时剥离设备本地图标路径,CUSTOM 无文件时归一化为 DEFAULT

验证

  • git diff --check 通过
  • compileDebugKotlin 通过(gradle-9.4.1,-x :app:generateAssetManifest
  • testDebugUnitTest 通过
  • assembleRelease 通过
  • 适配小米 HyperOS(zorn/24117RK2CC),adb connect 已确认;通知为
    setRequestPromotedOngoing 兼容 Android 14+ 的 OIS 展示路径,minSdk 28 / targetSdk 36

截图 / 日志 / 说明

  • 未运行 lintDebug:已知 AGP/K2 UAST 在 Gradle Kotlin 脚本上触发
    findFirCompiledSymbol 内部异常,与本次改动无关,未通过关闭校验规避
  • 通知图标解码与刷新链路改动集中在 TaskExecutionService,测试设备
    状态栏 chip 显示行为已在 HyperOS 上确认

Checklist

Summary by Sourcery

添加可配置的实时更新通知,可自定义状态栏内容、进度颜色和追踪器图标。

新功能:

  • 添加“实时更新”设置页面,用于启用实时通知并自定义状态栏内容、进度颜色和追踪器图标。
  • 支持内置追踪器图标以及用户选择的光栅或 SVG/XML 图像。

错误修复:

  • 防止设备本地的自定义追踪器路径被包含在配置备份中,并在不可用时回退到内置图标。

增强改进:

  • 将实时更新偏好应用到通知内容、进度样式和“正在进行”推广行为上。
  • 缓存自定义追踪器图标,并异步解码它们,以避免在通知刷新期间重复文件 I/O。

构建:

  • 添加 AndroidSVG,用于矢量追踪器图标渲染。
Original summary in English

Summary by Sourcery

Add configurable Live Updates notifications with customizable status-bar content, progress colors, and tracker icons.

New Features:

  • Add a Live Updates settings page for enabling live notifications and customizing status-bar content, progress colors, and tracker icons.
  • Support built-in tracker icons and user-selected raster or SVG/XML images.

Bug Fixes:

  • Prevent device-local custom tracker paths from being included in configuration backups and fall back to a built-in icon when unavailable.

Enhancements:

  • Apply Live Updates preferences to notification content, progress styling, and promoted ongoing behavior.
  • Cache custom tracker icons and decode them asynchronously to avoid repeated file I/O during notification refreshes.

Build:

  • Add AndroidSVG for vector tracker icon rendering.

新功能:

  • 添加 Live Updates 设置页,用于启用实时通知并自定义状态栏内容、进度颜色和追踪器图标。
  • 支持内置和用户选择的追踪器图片,包括光栅图像和 SVG/XML 格式。

错误修复:

  • 防止设备本地的自定义追踪器路径在配置备份间被携带,并在不可用时回退到内置图标。

增强:

  • 将 Live Updates 首选项应用到通知内容、进度样式以及“正在进行”提升行为。
  • 缓存自定义追踪器图标并异步解码,以避免在刷新通知时重复文件 I/O 操作。

构建:

  • 添加 AndroidSVG 依赖,用于矢量图标渲染。
Original summary in English

Summary by Sourcery

添加可配置的实时更新通知,可自定义状态栏内容、进度颜色和追踪器图标。

新功能:

  • 添加“实时更新”设置页面,用于启用实时通知并自定义状态栏内容、进度颜色和追踪器图标。
  • 支持内置追踪器图标以及用户选择的光栅或 SVG/XML 图像。

错误修复:

  • 防止设备本地的自定义追踪器路径被包含在配置备份中,并在不可用时回退到内置图标。

增强改进:

  • 将实时更新偏好应用到通知内容、进度样式和“正在进行”推广行为上。
  • 缓存自定义追踪器图标,并异步解码它们,以避免在通知刷新期间重复文件 I/O。

构建:

  • 添加 AndroidSVG,用于矢量追踪器图标渲染。
Original summary in English

Summary by Sourcery

Add configurable Live Updates notifications with customizable status-bar content, progress colors, and tracker icons.

New Features:

  • Add a Live Updates settings page for enabling live notifications and customizing status-bar content, progress colors, and tracker icons.
  • Support built-in tracker icons and user-selected raster or SVG/XML images.

Bug Fixes:

  • Prevent device-local custom tracker paths from being included in configuration backups and fall back to a built-in icon when unavailable.

Enhancements:

  • Apply Live Updates preferences to notification content, progress styling, and promoted ongoing behavior.
  • Cache custom tracker icons and decode them asynchronously to avoid repeated file I/O during notification refreshes.

Build:

  • Add AndroidSVG for vector tracker icon rendering.

- 7 项代码问题:链式调用缩进、小图缩放、采样解码、预览缓存、未用 import、文件末尾换行、trackerIcon 统一出口
- 重做 Live Updates 设置页排版:开关/内容/颜色/图标分卡片展示,颜色与图标可视化
- 图标默认项更名合成玉
- 状态栏内容 NONE 显式 setShortCriticalText("") 确保 chip 无文本
- TrackerIconDecoder SVG 输入流用 .use 关闭,异常也确保释放 fd
- ConfigBackupManager 导出清空本地图标路径并回退 CUSTOM 到 DEFAULT;导入丢弃路径并归一化
- 自定义图标缓存:主线程只读缓存,每秒刷新零 IO;首次/变更时 Dispatchers.IO 异步解码并主动刷新通知;防并发重复解码与过期结果;服务销毁取消协程
@sourcery-ai

sourcery-ai Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

审阅者指南

添加了专门的 Live Updates(实时更新)通知自定义功能,包括新的设置项、UI 和持久化支持,以及对可配置状态栏文本、进度条颜色和追踪图标(包括自定义图片解码与备份/导入处理)的运行时支持。

自定义 Live Update 追踪图标解码的时序图

sequenceDiagram
    actor User
    participant LiveUpdateSettingsView
    participant AppSettingsManager
    participant TaskExecutionService
    participant TrackerIconDecoder

    User ->> LiveUpdateSettingsView: pick image via ActivityResultContracts.GetContent
    LiveUpdateSettingsView ->> LiveUpdateSettingsView: copy file to filesDir/live_update
    LiveUpdateSettingsView ->> AppSettingsManager: setLiveUpdateCustomTrackerPath(path)

    loop periodic notification refresh
        TaskExecutionService ->> TaskExecutionService: trackerIcon()
        TaskExecutionService ->> AppSettingsManager: liveUpdateTrackerIcon.value
        TaskExecutionService ->> AppSettingsManager: liveUpdateCustomTrackerPath.value
        alt icon type CUSTOM and cache miss
            TaskExecutionService ->> TaskExecutionService: scheduleTrackerIconDecode(key, path)
            TaskExecutionService ->> TrackerIconDecoder: decode(path)
            TrackerIconDecoder -->> TaskExecutionService: Bitmap?
            TaskExecutionService ->> TaskExecutionService: trackerIconCache[key] = Bitmap?
            TaskExecutionService ->> TaskExecutionService: updateNotification(currentSnapshot())
        else other types or cache hit
            TaskExecutionService ->> TaskExecutionService: use cached Bitmap or fallback IconCompat
        end
    end
Loading

文件级改动

Change Details Files
引入 Live Updates 通知偏好和持久化,包括 chip 内容、配色方案、自定义颜色、追踪图标类型和自定义图标路径。
  • 扩展 AppSettings 模型,增加与 Live Updates 相关的字段,以及启用状态、chip 文本模式、配色方案、自定义颜色、追踪图标类型和自定义图标路径等默认值。
  • 在 AppSettingsManager 中将 Live Updates 设置以强类型 StateFlow 和 setter 函数方式暴露,并为 chip 内容、配色方案和追踪图标类型定义枚举。
  • 在 ConfigBackupManager 的导出/导入过程中规范化和清理 Live Updates 自定义图标字段,确保移除设备本地图标路径,并将没有对应文件的 CUSTOM 类型回退到 DEFAULT。
app/src/main/java/com/aliothmoon/maameow/domain/models/AppSettings.kt
app/src/main/java/com/aliothmoon/maameow/data/preferences/AppSettingsManager.kt
app/src/main/java/com/aliothmoon/maameow/data/preferences/ConfigBackupManager.kt
新增 Live Update 设置页面和导航入口,用于配置状态栏内容、配色方案和追踪图标,包括自定义图片选择和预览。
  • 为 Live Update 设置页面定义新的导航路由和 composable 目的地,并在主设置页面的“通知”下暴露该入口。
  • 实现 LiveUpdateSettingsView,用于配置启用 Live Updates、选择 chip 内容模式、在预设配色方案和自定义颜色调色板之间切换,以及选择追踪图标类型。
  • 通过 ActivityResult GetContent 实现自定义追踪图标选择,将选中的文件复制到应用 filesDir/live_update 中,并使用共享解码器解码并展示预览。
app/src/main/java/com/aliothmoon/maameow/constant/Routes.kt
app/src/main/java/com/aliothmoon/maameow/presentation/navigation/AppNavigation.kt
app/src/main/java/com/aliothmoon/maameow/presentation/view/settings/SettingsView.kt
app/src/main/java/com/aliothmoon/maameow/presentation/view/settings/LiveUpdateSettingsView.kt
将 Live Updates 设置接入 TaskExecutionService,用于控制状态栏 chip 文本、进度条颜色、带缓存的追踪图标解码,以及 Live Updates 启用状态。
  • 在 TaskExecutionService 中使用 AppSettingsManager,根据用户选择的 chip 内容模式计算状态栏 shortCritical 文本,包括基于日志的模式以及显式的 NONE。
  • 用可选方案替换固定的进度条颜色,支持语义默认、多种预设方案,以及在运行时解析自定义 HEX 颜色并在失败时回退。
  • 引入自定义追踪图标解析逻辑,使用新的 TrackerIconDecoder、按路径划分的 Bitmap 缓存以及 IO 协程来异步解码图片/SVG,避免阻塞主线程,并在解码成功后刷新通知。
  • 在决定服务是否可以请求“提升的持续 Live Updates”时遵守 liveUpdateEnabled 设置。
app/src/main/java/com/aliothmoon/maameow/domain/service/TaskExecutionService.kt
app/src/main/java/com/aliothmoon/maameow/notification/TrackerIconDecoder.kt
app/src/main/res/drawable/ic_tracker_dot.xml
添加 AndroidSVG 依赖和本地化字符串以支持新功能。
  • 在共享版本目录中注册 androidsvg 库,并将其作为 app 层实现依赖添加。
  • 为 Live Updates 设置中的标签、选项名称、提示文案和新增追踪图标样式提供英文和中文字符串资源。
  • 添加一个新的矢量图资源,用于“点状”追踪图标样式。
gradle/libs.versions.toml
app/build.gradle.kts
app/src/main/res/values-en/strings.xml
app/src/main/res/values/strings.xml
app/src/main/res/drawable/ic_tracker_dot.xml

技巧与命令

与 Sourcery 交互

  • 触发新的审阅: 在 pull request 上评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审阅评论。
  • 从审阅评论生成 GitHub issue: 在某条审阅评论下回复,请求 Sourcery 从该评论创建 issue。你也可以直接回复 @sourcery-ai issue 来从该评论创建 issue。
  • 生成 pull request 标题: 在 pull request 标题中任意位置写上 @sourcery-ai,即可随时生成标题。也可以在 pull request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 pull request 摘要: 在 pull request 正文任意位置写上 @sourcery-ai summary,即可在指定位置生成 PR 摘要。也可以在 pull request 中评论 @sourcery-ai summary 来(重新)生成摘要。
  • 生成审阅者指南: 在 pull request 中评论 @sourcery-ai guide,即可随时(重新)生成审阅者指南。
  • 批量解决所有 Sourcery 评论: 在 pull request 中评论 @sourcery-ai resolve,即可将所有 Sourcery 评论标记为已解决。如果你已经处理完这些评论且不希望再看到它们,这会非常有用。
  • 清除所有 Sourcery 审阅: 在 pull request 中评论 @sourcery-ai dismiss,即可清除所有已存在的 Sourcery 审阅。尤其适用于你想重新开始一次新的审阅——别忘了再评论 @sourcery-ai review 来触发新的审阅!

自定义你的体验

访问你的 dashboard 来:

  • 启用或禁用审阅功能,例如 Sourcery 生成的 pull request 摘要、审阅者指南等。
  • 更改审阅语言。
  • 添加、移除或编辑自定义审阅指令。
  • 调整其他审阅相关设置。

获取帮助

Original review guide in English

Reviewer's Guide

Adds a dedicated Live Updates notification customization feature, including new settings, UI, and persistence, plus runtime support for configurable status bar text, progress bar colors, and tracker icons (including custom image decoding and backup/import handling).

Sequence diagram for custom Live Update tracker icon decoding

sequenceDiagram
    actor User
    participant LiveUpdateSettingsView
    participant AppSettingsManager
    participant TaskExecutionService
    participant TrackerIconDecoder

    User ->> LiveUpdateSettingsView: pick image via ActivityResultContracts.GetContent
    LiveUpdateSettingsView ->> LiveUpdateSettingsView: copy file to filesDir/live_update
    LiveUpdateSettingsView ->> AppSettingsManager: setLiveUpdateCustomTrackerPath(path)

    loop periodic notification refresh
        TaskExecutionService ->> TaskExecutionService: trackerIcon()
        TaskExecutionService ->> AppSettingsManager: liveUpdateTrackerIcon.value
        TaskExecutionService ->> AppSettingsManager: liveUpdateCustomTrackerPath.value
        alt icon type CUSTOM and cache miss
            TaskExecutionService ->> TaskExecutionService: scheduleTrackerIconDecode(key, path)
            TaskExecutionService ->> TrackerIconDecoder: decode(path)
            TrackerIconDecoder -->> TaskExecutionService: Bitmap?
            TaskExecutionService ->> TaskExecutionService: trackerIconCache[key] = Bitmap?
            TaskExecutionService ->> TaskExecutionService: updateNotification(currentSnapshot())
        else other types or cache hit
            TaskExecutionService ->> TaskExecutionService: use cached Bitmap or fallback IconCompat
        end
    end
Loading

File-Level Changes

Change Details Files
Introduce Live Updates notification preferences and persistence, including chip content, color scheme, custom color, tracker icon type, and custom icon path.
  • Extend AppSettings schema with Live Updates-related fields and defaults for enablement, chip text mode, color scheme, custom color, tracker icon type, and custom icon path.
  • Expose Live Updates settings as strongly-typed StateFlows and setter functions in AppSettingsManager, with enums for chip content, color scheme, and tracker icon type.
  • Normalize and sanitize Live Updates custom icon fields in ConfigBackupManager during export/import so device-local icon paths are stripped and CUSTOM types without files fall back to DEFAULT.
app/src/main/java/com/aliothmoon/maameow/domain/models/AppSettings.kt
app/src/main/java/com/aliothmoon/maameow/data/preferences/AppSettingsManager.kt
app/src/main/java/com/aliothmoon/maameow/data/preferences/ConfigBackupManager.kt
Add a new Live Update settings screen and navigation entry to configure status bar content, color scheme, and tracker icon including custom image selection and preview.
  • Define a new navigation route and composable destination for the Live Update settings screen, and expose it from the main Settings view under Notifications.
  • Implement LiveUpdateSettingsView with sections for enabling Live Updates, selecting chip content mode, choosing predefined color schemes or a custom color palette, and picking tracker icon type.
  • Implement custom tracker icon selection via ActivityResult GetContent, copying the chosen file into app filesDir/live_update and showing a decoded preview using the shared decoder.
app/src/main/java/com/aliothmoon/maameow/constant/Routes.kt
app/src/main/java/com/aliothmoon/maameow/presentation/navigation/AppNavigation.kt
app/src/main/java/com/aliothmoon/maameow/presentation/view/settings/SettingsView.kt
app/src/main/java/com/aliothmoon/maameow/presentation/view/settings/LiveUpdateSettingsView.kt
Wire Live Updates settings into TaskExecutionService to control status bar chip text, progress bar color, tracker icon decoding with caching, and Live Updates enablement.
  • Use AppSettingsManager in TaskExecutionService to compute status bar shortCritical text based on user-selected chip content mode, including log-based mode and explicit NONE.
  • Replace fixed progress bar colors with a selectable scheme, supporting semantic default, several presets, and a custom HEX color parsed at runtime with fallback.
  • Introduce custom tracker icon resolution using a new TrackerIconDecoder, a per-path bitmap cache, and an IO coroutine to decode images/SVGs without blocking the main thread, refreshing notifications when decoding succeeds.
  • Respect liveUpdateEnabled when deciding whether the service can request promoted ongoing live updates.
app/src/main/java/com/aliothmoon/maameow/domain/service/TaskExecutionService.kt
app/src/main/java/com/aliothmoon/maameow/notification/TrackerIconDecoder.kt
app/src/main/res/drawable/ic_tracker_dot.xml
Add AndroidSVG dependency and localized strings to support the new feature.
  • Register androidsvg library in the shared versions catalog and add it as an app-level implementation dependency.
  • Provide English and Chinese string resources for Live Updates settings labels, option names, hints, and the new tracker icon style.
  • Add a new vector drawable for the "dot" tracker icon style.
gradle/libs.versions.toml
app/build.gradle.kts
app/src/main/res/values-en/strings.xml
app/src/main/res/values/strings.xml
app/src/main/res/drawable/ic_tracker_dot.xml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - 我发现了 4 个问题,并给出了一些整体反馈:

  • 追踪器图标缓存(trackerIconCache)会在主线程和 IO dispatcher 中同时被访问和修改,但没有做同步处理;建议使用线程安全的数据结构(例如 ConcurrentHashMap),或者把所有缓存的读写都限制在单一 dispatcher 中,以避免数据竞争。
  • LiveUpdateSettingsView 中,TrackerIconDecoder.decode 是在 UI 线程的 remember(customTrackerPath) 中被调用的,但内部会执行文件 IO 和位图解码;可以考虑将这部分工作移到后台 dispatcher 中(例如通过 produceState/LaunchedEffect 配合 Dispatchers.IO),以避免潜在的卡顿。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- 追踪器图标缓存(`trackerIconCache`)会在主线程和 IO dispatcher 中同时被访问和修改,但没有做同步处理;建议使用线程安全的数据结构(例如 `ConcurrentHashMap`),或者把所有缓存的读写都限制在单一 dispatcher 中,以避免数据竞争。
-`LiveUpdateSettingsView` 中,`TrackerIconDecoder.decode` 是在 UI 线程的 `remember(customTrackerPath)` 中被调用的,但内部会执行文件 IO 和位图解码;可以考虑将这部分工作移到后台 dispatcher 中(例如通过 `produceState`/`LaunchedEffect` 配合 `Dispatchers.IO`),以避免潜在的卡顿。

## Individual Comments

### Comment 1
<location path="app/src/main/java/com/aliothmoon/maameow/data/preferences/ConfigBackupManager.kt" line_range="109-110" />
<code_context>
-            shizukuLaunchPackage = shizukuLaunchPackage.ifBlank { OFFICIAL_SHIZUKU_PACKAGE }
+            shizukuLaunchPackage = shizukuLaunchPackage.ifBlank { OFFICIAL_SHIZUKU_PACKAGE },
+            liveUpdateCustomTrackerPath = "",
+            liveUpdateTrackerIcon = if (this.liveUpdateTrackerIcon == "custom" && this.liveUpdateCustomTrackerPath.isBlank())
+                "default" else this.liveUpdateTrackerIcon,
         )

</code_context>
<issue_to_address>
**issue (bug_risk):** 导入时的归一化逻辑可能会在清空路径的同时,仍保留 `liveUpdateTrackerIcon=custom`,这与注释及清理逻辑的意图不符。

在 `normalizedForImport` 中,`liveUpdateCustomTrackerPath` 总是被设为 `""`,但是图标重置条件依然读取的是原对象中的 `this.liveUpdateCustomTrackerPath.isBlank()`。对于跨设备备份场景,如果 `liveUpdateTrackerIcon == "custom"` 且路径原本非空,则该条件仍然为 false,于是图标仍保持为 `"custom"`,即便路径已经被清空。这与「在从其他设备导入自定义图标时回退到内置图标」的设想相矛盾。你可以只基于 `this.liveUpdateTrackerIcon == "custom"` 来判断(反正路径已无条件清空),或者在规范化路径之后再计算新的图标值。
</issue_to_address>

### Comment 2
<location path="app/src/main/java/com/aliothmoon/maameow/domain/service/TaskExecutionService.kt" line_range="94-95" />
<code_context>
     private var progressJob: Job? = null

+    // 自定义图标缓存:key = "custom|$path",value = 解码后的 Bitmap(null 表示解码失败/无效)
+    private val trackerIconCache = mutableMapOf<String, Bitmap?>()
+    private var trackerIconDecodeJob: Job? = null
+
     override fun onBind(intent: Intent?): IBinder? = null
</code_context>
<issue_to_address>
**issue (bug_risk):** 可变的 `trackerIconCache` 会在多个 dispatcher 上被访问,而没有同步措施,这可能导致数据竞争。

该缓存在 `trackerIcon()` 中读取(在主线程上构建通知),并在 `scheduleTrackerIconDecode()` 中通过 `Dispatchers.IO` 写入。由于 `mutableMapOf` 不是线程安全的,并发访问可能会引发隐蔽的数据竞争问题。请考虑要么将所有访问限制到单一 dispatcher(例如始终通过 `withContext(Dispatchers.Main)` 访问),要么使用线程安全机制封装它,比如使用同步/`ConcurrentHashMap` 风格的 Map,或者在所有访问外层包一层 `Mutex`。
</issue_to_address>

### Comment 3
<location path="app/src/main/java/com/aliothmoon/maameow/presentation/view/settings/LiveUpdateSettingsView.kt" line_range="459-461" />
<code_context>
+                                    bottom = MaaDesignTokens.Spacing.md,
+                                )
+                        ) {
+                            val iconBitmap = remember(customTrackerPath) {
+                                if (customTrackerPath.isNotEmpty())
+                                    TrackerIconDecoder.decode(customTrackerPath, targetSize = 72)
+                                else null
+                            }
</code_context>
<issue_to_address>
**suggestion (performance):** 在组合过程中同步解码自定义追踪器图标,会阻塞主线程。

`TrackerIconDecoder.decode` 可能会访问磁盘并解析 SVG,这些都是从可组合函数在主线程上执行的高开销操作。可以考虑将解码移动到后台 dispatcher 中,并以 state 的形式暴露位图(例如使用 `produceState`/`LaunchedEffect` 配合 `withContext(Dispatchers.IO)`),或者复用共享缓存,以避免在重组或路径变化时重复解码和潜在的卡顿。

建议实现:

```
                        ) {
+                            val iconBitmap by produceState<Bitmap?>(initialValue = null, key1 = customTrackerPath) {
+                                value = if (customTrackerPath.isNotEmpty()) {
+                                    withContext(Dispatchers.IO) {
+                                        TrackerIconDecoder.decode(customTrackerPath, targetSize = 72)
+                                    }
+                                } else {
+                                    null
+                                }
+                            }

```

要完整实现该修改,你还需要:
1. 确保在 `LiveUpdateSettingsView.kt` 顶部加入正确的 import:
   - `import androidx.compose.runtime.produceState`
   - `import kotlinx.coroutines.Dispatchers`
   - `import kotlinx.coroutines.withContext`
2. 确认已经导入 `Bitmap`(通常是 `android.graphics.Bitmap`),如果没有,则需要导入;或者如果 `TrackerIconDecoder.decode` 的实际返回类型不是 `Bitmap?`,则需要相应地调整 `produceState` 的泛型类型。
</issue_to_address>

### Comment 4
<location path="app/src/main/java/com/aliothmoon/maameow/domain/service/TaskExecutionService.kt" line_range="300" />
<code_context>
+        AppSettingsManager.LiveUpdateChipContent.PROGRESS -> progressInfo.progressLabel
+        AppSettingsManager.LiveUpdateChipContent.TASK -> activeTaskName
+        AppSettingsManager.LiveUpdateChipContent.LOG -> statusText
+        AppSettingsManager.LiveUpdateChipContent.NONE -> ""
+    }
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 对于 `NONE` 的 chip 内容使用空字符串,可能仍然会创建一个空的 chip,而不是将其隐藏。

对于 `LiveUpdateChipContent.NONE`,你返回的是 `""` 而不是 `null`。根据 `NotificationCompat.ProgressStyle` / `setShortCriticalText` 的具体表现,这仍有可能为一个空 chip 预留空间。如果目标是彻底隐藏该 chip,返回 `null`(并在下游正确处理)会更准确地表达「没有 chip」,也能避免 UI 伪影。

建议实现:

```
        AppSettingsManager.LiveUpdateChipContent.BOTH -> when {
            progressInfo.progressLabel != null && activeTaskName != null ->
                "${progressInfo.progressLabel} $activeTaskName"
            progressInfo.progressLabel != null -> progressInfo.progressLabel
            activeTaskName != null -> activeTaskName
            else -> null
        }
        AppSettingsManager.LiveUpdateChipContent.PROGRESS -> progressInfo.progressLabel
        AppSettingsManager.LiveUpdateChipContent.TASK -> activeTaskName
        AppSettingsManager.LiveUpdateChipContent.LOG -> statusText
        AppSettingsManager.LiveUpdateChipContent.NONE -> null

```

要完整实现这个建议,任何消费该值的代码(很可能是配置 `NotificationCompat` 的地方,例如 `setShortCriticalText` 或类似方法)都应将 `null` 视为「不显示 chip」:当值为 `null` 时,要么跳过调用 setter,要么显式清除该 chip。确保这些调用点现在接受 `String?`,并且不会对 `null` 渲染一个空 chip。
</issue_to_address>

Sourcery 对开源项目免费——如果你觉得这些评论有帮助,欢迎分享 ✨
请帮我变得更有用!你可以在每条评论上点 👍 或 👎,我会根据反馈改进后续的代码审查。
Original comment in English

Hey - I've found 4 issues, and left some high level feedback:

  • Tracker icon cache (trackerIconCache) is accessed and mutated from both main and IO dispatcher without synchronization; consider using a thread-safe structure (e.g., ConcurrentHashMap) or confining all cache reads/writes to a single dispatcher to avoid data races.
  • In LiveUpdateSettingsView, TrackerIconDecoder.decode is called inside remember(customTrackerPath) on the UI thread, but it performs file IO and bitmap decoding; moving this work to a background dispatcher (e.g., via produceState/LaunchedEffect with Dispatchers.IO) would avoid potential jank.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Tracker icon cache (`trackerIconCache`) is accessed and mutated from both main and IO dispatcher without synchronization; consider using a thread-safe structure (e.g., `ConcurrentHashMap`) or confining all cache reads/writes to a single dispatcher to avoid data races.
- In `LiveUpdateSettingsView`, `TrackerIconDecoder.decode` is called inside `remember(customTrackerPath)` on the UI thread, but it performs file IO and bitmap decoding; moving this work to a background dispatcher (e.g., via `produceState`/`LaunchedEffect` with `Dispatchers.IO`) would avoid potential jank.

## Individual Comments

### Comment 1
<location path="app/src/main/java/com/aliothmoon/maameow/data/preferences/ConfigBackupManager.kt" line_range="109-110" />
<code_context>
-            shizukuLaunchPackage = shizukuLaunchPackage.ifBlank { OFFICIAL_SHIZUKU_PACKAGE }
+            shizukuLaunchPackage = shizukuLaunchPackage.ifBlank { OFFICIAL_SHIZUKU_PACKAGE },
+            liveUpdateCustomTrackerPath = "",
+            liveUpdateTrackerIcon = if (this.liveUpdateTrackerIcon == "custom" && this.liveUpdateCustomTrackerPath.isBlank())
+                "default" else this.liveUpdateTrackerIcon,
         )

</code_context>
<issue_to_address>
**issue (bug_risk):** Import normalization may still keep `liveUpdateTrackerIcon=custom` while clearing its path, contrary to the comment and sanitation logic.

In `normalizedForImport`, `liveUpdateCustomTrackerPath` is always set to `""`, but the icon reset condition still reads `this.liveUpdateCustomTrackerPath.isBlank()` from the original object. For a cross-device backup where `liveUpdateTrackerIcon == "custom"` and the path was non-blank, the condition remains false, so the icon stays `"custom"` even though the path is now cleared. That contradicts the intent to fall back to a built-in icon when importing custom icons from another device. You could either base the condition solely on `this.liveUpdateTrackerIcon == "custom"` (given the unconditional path clear) or compute the new icon after normalizing the path.
</issue_to_address>

### Comment 2
<location path="app/src/main/java/com/aliothmoon/maameow/domain/service/TaskExecutionService.kt" line_range="94-95" />
<code_context>
     private var progressJob: Job? = null

+    // 自定义图标缓存:key = "custom|$path",value = 解码后的 Bitmap(null 表示解码失败/无效)
+    private val trackerIconCache = mutableMapOf<String, Bitmap?>()
+    private var trackerIconDecodeJob: Job? = null
+
     override fun onBind(intent: Intent?): IBinder? = null
</code_context>
<issue_to_address>
**issue (bug_risk):** The mutable `trackerIconCache` is accessed from multiple dispatchers without synchronization, which can cause data races.

The cache is read in `trackerIcon()` (notification-building on the main thread) and written in `scheduleTrackerIconDecode()` on `Dispatchers.IO`. Since `mutableMapOf` isn’t thread-safe, concurrent access can cause subtle data races. Please either confine all access to a single dispatcher (e.g., always via `withContext(Dispatchers.Main)`) or wrap it in a thread-safe mechanism such as a synchronized/`ConcurrentHashMap`-style map or a `Mutex` around all access.
</issue_to_address>

### Comment 3
<location path="app/src/main/java/com/aliothmoon/maameow/presentation/view/settings/LiveUpdateSettingsView.kt" line_range="459-461" />
<code_context>
+                                    bottom = MaaDesignTokens.Spacing.md,
+                                )
+                        ) {
+                            val iconBitmap = remember(customTrackerPath) {
+                                if (customTrackerPath.isNotEmpty())
+                                    TrackerIconDecoder.decode(customTrackerPath, targetSize = 72)
+                                else null
+                            }
</code_context>
<issue_to_address>
**suggestion (performance):** Decoding the custom tracker icon synchronously in composition can block the main thread.

`TrackerIconDecoder.decode` may hit disk and parse SVGs, which is expensive work to run from a composable on the main thread. Consider moving the decode to a background dispatcher and exposing the bitmap as state (e.g., `produceState`/`LaunchedEffect` + `withContext(Dispatchers.IO)`), or reusing a shared cache to avoid repeated decoding and potential jank during recomposition or path changes.

Suggested implementation:

```
                        ) {
+                            val iconBitmap by produceState<Bitmap?>(initialValue = null, key1 = customTrackerPath) {
+                                value = if (customTrackerPath.isNotEmpty()) {
+                                    withContext(Dispatchers.IO) {
+                                        TrackerIconDecoder.decode(customTrackerPath, targetSize = 72)
+                                    }
+                                } else {
+                                    null
+                                }
+                            }

```

To fully implement this change, you’ll also need to:
1. Ensure you have the correct imports at the top of `LiveUpdateSettingsView.kt`:
   - `import androidx.compose.runtime.produceState`
   - `import kotlinx.coroutines.Dispatchers`
   - `import kotlinx.coroutines.withContext`
2. Make sure `Bitmap` is imported if it is not already (typically `android.graphics.Bitmap`), or adjust the generic type on `produceState` to match the actual return type of `TrackerIconDecoder.decode` if it’s not `Bitmap?`.
</issue_to_address>

### Comment 4
<location path="app/src/main/java/com/aliothmoon/maameow/domain/service/TaskExecutionService.kt" line_range="300" />
<code_context>
+        AppSettingsManager.LiveUpdateChipContent.PROGRESS -> progressInfo.progressLabel
+        AppSettingsManager.LiveUpdateChipContent.TASK -> activeTaskName
+        AppSettingsManager.LiveUpdateChipContent.LOG -> statusText
+        AppSettingsManager.LiveUpdateChipContent.NONE -> ""
+    }
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Using an empty string for `NONE` chip content may still allocate an empty chip instead of hiding it.

For `LiveUpdateChipContent.NONE` you return `""` instead of `null`. Depending on how `NotificationCompat.ProgressStyle` / `setShortCriticalText` behave, this could still allocate space for an empty chip. If the goal is to hide the chip entirely, returning `null` (and handling that downstream) would more accurately express "no chip" and avoid UI artifacts.

Suggested implementation:

```
        AppSettingsManager.LiveUpdateChipContent.BOTH -> when {
            progressInfo.progressLabel != null && activeTaskName != null ->
                "${progressInfo.progressLabel} $activeTaskName"
            progressInfo.progressLabel != null -> progressInfo.progressLabel
            activeTaskName != null -> activeTaskName
            else -> null
        }
        AppSettingsManager.LiveUpdateChipContent.PROGRESS -> progressInfo.progressLabel
        AppSettingsManager.LiveUpdateChipContent.TASK -> activeTaskName
        AppSettingsManager.LiveUpdateChipContent.LOG -> statusText
        AppSettingsManager.LiveUpdateChipContent.NONE -> null

```

To fully implement the suggestion, any code that consumes this value (likely where `NotificationCompat` is configured, e.g. `setShortCriticalText` or similar) should treat `null` as "no chip": either skip calling the setter when the value is `null`, or explicitly clear the chip in that case. Ensure those call sites now accept a `String?` and do not render an empty chip for `null`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread app/src/main/java/com/aliothmoon/maameow/data/preferences/ConfigBackupManager.kt Outdated
Comment thread app/src/main/java/com/aliothmoon/maameow/domain/service/TaskExecutionService.kt Outdated
AppSettingsManager.LiveUpdateChipContent.PROGRESS -> progressInfo.progressLabel
AppSettingsManager.LiveUpdateChipContent.TASK -> activeTaskName
AppSettingsManager.LiveUpdateChipContent.LOG -> statusText
AppSettingsManager.LiveUpdateChipContent.NONE -> ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): 对于 NONE 的 chip 内容使用空字符串,可能仍然会创建一个空的 chip,而不是将其隐藏。

对于 LiveUpdateChipContent.NONE,你返回的是 "" 而不是 null。根据 NotificationCompat.ProgressStyle / setShortCriticalText 的具体表现,这仍有可能为一个空 chip 预留空间。如果目标是彻底隐藏该 chip,返回 null(并在下游正确处理)会更准确地表达「没有 chip」,也能避免 UI 伪影。

建议实现:

        AppSettingsManager.LiveUpdateChipContent.BOTH -> when {
            progressInfo.progressLabel != null && activeTaskName != null ->
                "${progressInfo.progressLabel} $activeTaskName"
            progressInfo.progressLabel != null -> progressInfo.progressLabel
            activeTaskName != null -> activeTaskName
            else -> null
        }
        AppSettingsManager.LiveUpdateChipContent.PROGRESS -> progressInfo.progressLabel
        AppSettingsManager.LiveUpdateChipContent.TASK -> activeTaskName
        AppSettingsManager.LiveUpdateChipContent.LOG -> statusText
        AppSettingsManager.LiveUpdateChipContent.NONE -> null

要完整实现这个建议,任何消费该值的代码(很可能是配置 NotificationCompat 的地方,例如 setShortCriticalText 或类似方法)都应将 null 视为「不显示 chip」:当值为 null 时,要么跳过调用 setter,要么显式清除该 chip。确保这些调用点现在接受 String?,并且不会对 null 渲染一个空 chip。

Original comment in English

suggestion (bug_risk): Using an empty string for NONE chip content may still allocate an empty chip instead of hiding it.

For LiveUpdateChipContent.NONE you return "" instead of null. Depending on how NotificationCompat.ProgressStyle / setShortCriticalText behave, this could still allocate space for an empty chip. If the goal is to hide the chip entirely, returning null (and handling that downstream) would more accurately express "no chip" and avoid UI artifacts.

Suggested implementation:

        AppSettingsManager.LiveUpdateChipContent.BOTH -> when {
            progressInfo.progressLabel != null && activeTaskName != null ->
                "${progressInfo.progressLabel} $activeTaskName"
            progressInfo.progressLabel != null -> progressInfo.progressLabel
            activeTaskName != null -> activeTaskName
            else -> null
        }
        AppSettingsManager.LiveUpdateChipContent.PROGRESS -> progressInfo.progressLabel
        AppSettingsManager.LiveUpdateChipContent.TASK -> activeTaskName
        AppSettingsManager.LiveUpdateChipContent.LOG -> statusText
        AppSettingsManager.LiveUpdateChipContent.NONE -> null

To fully implement the suggestion, any code that consumes this value (likely where NotificationCompat is configured, e.g. setShortCriticalText or similar) should treat null as "no chip": either skip calling the setter when the value is null, or explicitly clear the chip in that case. Ensure those call sites now accept a String? and do not render an empty chip for null.

- trackerIconCache 改用 ConcurrentHashMap,避免主线程读/IO 写数据竞争
- ConfigBackupManager 导入时 CUSTOM 图标无条件回退 DEFAULT(路径已清空)
- 设置页自定义图标预览改 produceState + Dispatchers.IO 异步解码,避免阻塞主线程
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.

1 participant