Skip to content

UI焕新,修复各种bug - #9

Merged
RhenCloud merged 15 commits into
ReCloudStudio:mainfrom
XFJ-YYQF:main
Aug 1, 2026
Merged

UI焕新,修复各种bug#9
RhenCloud merged 15 commits into
ReCloudStudio:mainfrom
XFJ-YYQF:main

Conversation

@XFJ-YYQF

@XFJ-YYQF XFJ-YYQF commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

新的UI,好看吗好看吗?!!!


This change is Reviewable

Summary by CodeRabbit

  • 新功能

    • 全新界面,新增“概览”和“配置”底部导航。
    • 概览页集中展示模块、框架、设备、系统、通知监听及配置状态。
    • 配置页支持服务器、媒体上报和播放方式设置,并提供校验与保存反馈。
    • 新增模块活跃状态与心跳信息展示。
  • 问题修复

    • 优化媒体状态读取、超时处理及失败重试,提升上报稳定性。
  • 文档

    • 明确“自动”媒体模式仅在系统钩子和通知监听之间选择。

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

PR 将传统 XML 配置界面迁移至 Compose,新增概览状态快照、跨进程心跳和配置缓存,并改进媒体采集线程、超时处理及状态重试逻辑。

Changes

媒体上报与应用界面

Layer / File(s) Summary
平台构建与入口基础
build.gradle, app/build.gradle.kts, gradle.properties, gradle/wrapper/*, app/proguard-rules.pro, app/src/main/AndroidManifest.xml
构建升级至 Compose、JVM 17 和 Android SDK 35/36。应用入口改用新主题和 Compose 配置。
配置存储与跨进程心跳
app/src/main/java/io/github/recloudstudio/sleepyxposed/ConfigContentProvider.kt, ConfigManager.kt, HookHeartbeat.kt, ForegroundAppMonitor.kt, XposedProbe.kt
配置支持缓存、公共 JSON 和路径回退。Provider 接收 system UID 心跳。应用保存心跳时间和框架信息。
媒体采集与状态上报
app/src/main/java/io/github/recloudstudio/sleepyxposed/MediaStatusMonitor.kt, MediaListenerService.kt, MediaMethod.kt, RomDetector.kt, README.md
媒体处理迁移至后台线程。dumpsys 读取增加超时和播放状态筛选。失败响应不再阻止后续重试。
运行状态快照
app/src/main/java/io/github/recloudstudio/sleepyxposed/StatusSnapshot.kt
新增状态快照,聚合模块、配置、心跳、设备、系统、ROM、通知监听器和框架信息。
Compose 配置与状态展示
app/src/main/java/io/github/recloudstudio/sleepyxposed/MainActivity.kt, app/src/main/java/io/github/recloudstudio/sleepyxposed/ui/*, app/src/main/res/values*/*
新增 Overview 和 Config 页面、底部导航、配置表单、状态卡片、主题及本地化资源。

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: rhencloud

Sequence Diagram(s)

sequenceDiagram
  participant MainActivity
  participant SleepyApp
  participant StatusSnapshot
  participant OverviewScreen
  MainActivity->>SleepyApp: setContent
  SleepyApp->>StatusSnapshot: collect context
  StatusSnapshot-->>SleepyApp: 返回状态快照
  SleepyApp->>OverviewScreen: 渲染状态快照
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.49% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确概括了本次变更的主要内容,包括 Compose UI 重构和多项问题修复。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 16

🧹 Nitpick comments (14)
app/src/main/res/values-zh-rCN/strings.xml (1)

39-48: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

删除未使用的字符串资源。

以下资源在当前代码库中没有引用:action_backhome_section_modulehome_section_runtimehome_section_systemhome_section_actionsopen_settingsopen_settings_summarystatus_module_hookstatus_activestatus_inactivestatus_app_versionstatus_androidstatus_devicestatus_brand。请从 values/strings.xmlvalues-zh-rCN/strings.xml 中删除。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/res/values-zh-rCN/strings.xml` around lines 39 - 48, Remove the
unused string resources action_back, home_section_module, home_section_runtime,
home_section_system, home_section_actions, open_settings, open_settings_summary,
status_module_hook, status_active, status_inactive, status_app_version,
status_android, status_device, and status_brand from both the default
strings.xml and the Chinese values-zh-rCN strings.xml resources.
app/src/main/java/com/rhencloud/sleepyxposed/ui/SleepyApp.kt (1)

47-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

删除误导性注释,或改用可编译的缓存实现。

Compose BOM 2025.02.00 使用 Material 3 1.3.1。带参数的 NavigationBarItemDefaults.colors@Composable,每次调用都会通过 copy 创建新的 NavigationBarItemColors。不能将它直接放入 remember { ... },因为 remember 的计算块禁止可组合调用。若需要缓存,请在 remember(colorScheme) 中直接构造等价的 NavigationBarItemColors,并保留完整的禁用状态颜色配置。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/rhencloud/sleepyxposed/ui/SleepyApp.kt` around lines 47
- 55, 更新 SleepyApp 中的 navColors 缓存实现:不要在 remember 的计算块内调用可组合的
NavigationBarItemDefaults.colors。改为按当前 colorScheme 使用 remember(colorScheme)
直接构造等价的 NavigationBarItemColors,并完整保留选中、未选中及禁用状态颜色配置;同时删除或修正文档性注释。
app/src/main/java/com/rhencloud/sleepyxposed/ui/ConfigScreen.kt (1)

191-215: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

将点击处理交给 Row,并保留单选组的触摸目标尺寸。

Row 已使用 selectableRole.RadioButton,因此将 RadioButton.onClick 设为 null,避免产生嵌套的无障碍操作节点。设置为 null 后,RadioButton 不再提供最小交互尺寸;当前行只有 4.dp 垂直内边距,触摸高度可能低于 48.dp。为 Row 添加 minimumInteractiveComponentSize() 或至少 48.dp 的高度,并仅将媒体方式行包在 selectableGroup() 中。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/rhencloud/sleepyxposed/ui/ConfigScreen.kt` around lines
191 - 215, Update the media-method container around the MediaMethod.entries loop
to use selectableGroup() only for these radio-option rows, set
RadioButton.onClick to null so Row remains the sole selectable accessibility
node, and add minimumInteractiveComponentSize() or an equivalent minimum 48.dp
height to each selectable Row while preserving its existing selection behavior.
app/src/main/res/values/themes.xml (1)

4-8: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

android:statusBarColorandroid:navigationBarColortargetSdk = 35 下可能不再生效。

targetSdk 已设为 35(Android 15),且 MainActivity 调用了 enableEdgeToEdge()。Android 15 起,应用启用 edge-to-edge 后,android:statusBarColor 被弃用且不再生效,android:navigationBarColor 对手势导航同样不再生效。Line 4-5 设置的颜色值在当前配置下大概率是死配置。这不会造成崩溃,但可能误导后续维护者。可以考虑移除这两行,仅保留 windowLightStatusBar/windowLightNavigationBar,并通过 Compose 侧的 insets 处理来控制系统栏外观。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/res/values/themes.xml` around lines 4 - 8, 移除 themes.xml 中的
android:statusBarColor 和 android:navigationBarColor 配置,保留
android:windowLightStatusBar 与 android:windowLightNavigationBar;继续通过 Compose 的
edge-to-edge/insets 处理控制系统栏外观。
app/proguard-rules.pro (1)

35-38: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

考虑移除已过时的 View 属性保留规则。

-keepclassmembers class * extends android.view.View { void set*(***); *** get*(); } 通常用于保留自定义 View 的 XML 属性访问器。由于应用已全面迁移到 Compose,且当前保留的 Manifest 组件中不包含自定义 View,此规则可能已成为无用配置。如果代码库中确实不再存在自定义 View,可以移除该规则以简化 ProGuard 配置。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/proguard-rules.pro` around lines 35 - 38, 确认代码库中已不存在自定义 View 或依赖 XML
属性访问器后,移除 proguard-rules.pro 中针对 android.view.View 子类 set*/get* 方法的
-keepclassmembers 规则;若仍存在相关自定义 View,则保留该规则。
gradle.properties (1)

6-6: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

确认禁用 Configuration Cache 是否与“优化性能”目标一致。

org.gradle.configuration-cache=false 显式禁用了 Gradle 的 Configuration Cache。该特性是 Gradle 官方推荐的关键构建性能优化之一,禁用它与 PR 描述中的性能优化目标方向相反。如果是因为某个插件(例如 Compose 编译器插件或签名逻辑)尚不兼容 Configuration Cache 而临时禁用,建议在注释中说明原因,方便后续跟进移除限制。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gradle.properties` at line 6, Review the org.gradle.configuration-cache
setting against the PR’s performance goal: remove the explicit false value if
the build supports Configuration Cache; otherwise retain it only with a concise
comment documenting the incompatible plugin or signing logic and the follow-up
needed to re-enable it.
app/src/main/java/com/rhencloud/sleepyxposed/ConfigManager.kt (3)

295-299: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

避免硬编码 /storage/emulated/0

该路径固定指向用户 0。在多用户或工作资料环境下,应用实际的外部存储根目录不同,写入与读取都会落到错误位置。请使用 Environment.getExternalStorageDirectory() 拼接,与第 319-325 行的回退分支保持一致。

♻️ 建议改写
   private fun getPrimaryPublicConfigFile(): File {
     return File(
-            "/storage/emulated/0/Android/media/$MODULE_PACKAGE_NAME/$FALLBACK_DIR/$FALLBACK_FILE_NAME"
+            Environment.getExternalStorageDirectory(),
+            "Android/media/$MODULE_PACKAGE_NAME/$FALLBACK_DIR/$FALLBACK_FILE_NAME"
     )
   }

说明:静态分析给出的路径穿越告警是误报,因为所有路径片段均为编译期常量。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/rhencloud/sleepyxposed/ConfigManager.kt` around lines
295 - 299, 更新 getPrimaryPublicConfigFile,移除硬编码的 /storage/emulated/0,改用
Environment.getExternalStorageDirectory() 作为外部存储根目录拼接
MODULE_PACKAGE_NAME、FALLBACK_DIR 和 FALLBACK_FILE_NAME,并与回退分支保持一致。

Source: Linters/SAST tools


114-145: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

复用 ConfigContentProvider.COLUMNS 中的列名。

此处的列名字符串("server_url""media_method" 等)与 ConfigContentProvider.COLUMNS(第 97-108 行)重复定义。任一侧改名都会导致读取静默返回空值。建议将列名提取为共享常量,两侧引用同一来源。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/rhencloud/sleepyxposed/ConfigManager.kt` around lines
114 - 145, Update loadViaContentProvider and ConfigContentProvider.COLUMNS to
use one shared set of column-name constants instead of repeating string literals
such as server_url and media_method. Ensure the cursor-reading helpers in
loadViaContentProvider reference those shared constants so provider schema
changes cannot leave reads silently empty.

89-101: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

移除重复的 Provider 查询与 JSON 读取。

第 91 行已执行一次 loadViaContentProvider(ctx),第 96 行已执行一次 loadConfigFromJsonFiles(null)。当两者都返回不完整配置时,第 100 行再次执行同样的 IPC 与文件读取。这与该缓存机制"避免重复 IPC + 文件回退链"的目标冲突。请复用首次结果。

♻️ 建议改写
   private fun loadConfigFromXSharedPreferencesUncached(systemContext: Context?): SleepyConfig {
-    systemContext?.let { ctx ->
-      loadViaContentProvider(ctx)?.takeIf { it.hasRequiredFields() }?.let {
-        return it
-      }
-    }
-
-    loadConfigFromJsonFiles(null)?.takeIf { it.hasRequiredFields() }?.let {
-      return it
-    }
-
-    return loadViaContentProvider(systemContext) ?: loadConfigFromJsonFiles(null) ?: SleepyConfig()
+    val provider = loadViaContentProvider(systemContext)
+    if (provider != null && provider.hasRequiredFields()) return provider
+
+    val json = loadConfigFromJsonFiles(null)
+    if (json != null && json.hasRequiredFields()) return json
+
+    return provider ?: json ?: SleepyConfig()
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/rhencloud/sleepyxposed/ConfigManager.kt` around lines
89 - 101, Update loadConfigFromXSharedPreferencesUncached to store and reuse the
results of the initial loadViaContentProvider and loadConfigFromJsonFiles calls,
including incomplete configurations, so the final fallback does not repeat
either provider IPC or JSON file read. Preserve the existing preference order
and required-field validation while returning the first available result or
SleepyConfig().
app/src/main/java/com/rhencloud/sleepyxposed/MediaListenerService.kt (2)

88-101: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

复用 playing.metadata 的取值结果。

第 91、94、95 行分别访问 playing.metadataMediaController.getMetadata() 每次都要跨进程取回一份数据。请提取为局部变量。

♻️ 建议改写
-        val title =
-            playing?.metadata?.getString(MediaMetadata.METADATA_KEY_TITLE)?.takeIf { it.isNotBlank() }
+        val metadata = playing?.metadata
+        val title =
+            metadata?.getString(MediaMetadata.METADATA_KEY_TITLE)?.takeIf { it.isNotBlank() }
         if (playing != null && title != null) {
             val artist =
-                playing.metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST)
-                    ?: playing.metadata?.getString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST) ?: ""
+                metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST)
+                    ?: metadata?.getString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST) ?: ""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/rhencloud/sleepyxposed/MediaListenerService.kt` around
lines 88 - 101, 在媒体状态处理逻辑中提取并复用 playing.metadata 的结果,避免在标题、艺人和专辑艺人读取处重复调用
MediaController.getMetadata()。更新相关代码以使用局部 metadata 变量,同时保持现有标题判断、艺人回退顺序及
status/using 行为不变。

73-77: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

避免在会话回调线程上执行配置文件 I/O。

OnActiveSessionsChangedListener 的回调在主线程执行。ConfigManager.loadConfig 会读取两处 SharedPreferences,并在配置不完整时回退到外部存储上的 JSON 文件读取。媒体会话变化较频繁,这会在主线程上产生反复的磁盘 I/O。

建议把 handleControllers 的整体逻辑投递到后台 HandlerThread,或在服务内缓存配置,并通过 ConfigContentProvider.CONTENT_URIContentObserver 刷新缓存。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/rhencloud/sleepyxposed/MediaListenerService.kt` around
lines 73 - 77, Move the full handleControllers flow, including
ConfigManager.loadConfig(this), off the OnActiveSessionsChangedListener
main-thread callback by dispatching it to a dedicated background HandlerThread.
Preserve the existing configuration validation and controller handling behavior,
and ensure the background thread is started and cleaned up with the service
lifecycle.
app/src/main/java/com/rhencloud/sleepyxposed/HookHeartbeat.kt (1)

95-132: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

在设备解锁前,心跳写入会失败;建议改用设备保护区存储。

ConfigContentProvider 声明了 android:directBootAware="true",因此 provider 在设备首次解锁前即可被调用。此时凭据加密区(CE)不可用,appContext.getSharedPreferences 会抛出异常,并被此处的 catch 吞掉。bootstrap 心跳与框架信息因此丢失,直到下一次节流窗口结束。

ConfigManager 已使用 createDeviceProtectedStorageContext()。建议心跳存储采用同一策略,并让 frameworkInfolastSeenMillisAgo 从同一区域读取。

♻️ 建议改写
+    private fun prefsContext(context: Context): Context =
+        runCatching { context.createDeviceProtectedStorageContext() }.getOrDefault(context)
+
     fun recordPing(appContext: Context, detail: String, extras: Bundle? = null) {
         try {
-            appContext
+            prefsContext(appContext)
                 .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)

recordFrameworkInfoframeworkInfolastSeenMillisAgo 需要做同样替换,否则读写目标不一致。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/rhencloud/sleepyxposed/HookHeartbeat.kt` around lines
95 - 132, Update the heartbeat storage used by recordPing and
recordFrameworkInfo to obtain a device-protected storage context via
createDeviceProtectedStorageContext() before accessing PREFS_NAME. Apply the
same device-protected context in frameworkInfo and lastSeenMillisAgo so all
heartbeat and framework metadata reads and writes use the same storage area and
remain available before first unlock.
app/src/main/AndroidManifest.xml (1)

40-46: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

导出的 provider 仅依赖代码内的 UID 校验,请补充说明。

ConfigContentProvider 声明为 android:exported="true" 且没有 android:permission。任何应用都可以发起 Binder 调用,访问控制完全依赖 ConfigContentProvider.enforceSystemOrSelf()。该实现目前是正确的,因为 system UID 无法通过自定义签名权限授予。

风险在于后续维护:新增的 query/call 分支若忘记调用 enforceSystemOrSelf(),配置(含 secret)会立即对所有应用暴露。请在 manifest 中加入注释,说明访问控制在代码中实现,不可移除。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/AndroidManifest.xml` around lines 40 - 46, 在 AndroidManifest.xml
的 ConfigContentProvider 声明旁添加注释,明确说明该 provider 因需支持 system UID 而保持
exported=true,访问控制由 ConfigContentProvider.enforceSystemOrSelf()
在代码中实现,新增访问分支必须继续执行该校验且不可移除。
app/src/main/java/com/rhencloud/sleepyxposed/MediaMethod.kt (1)

24-26: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

values() 改为 entries

项目的 Android Kotlin 插件版本为 2.3.20。请使用 entries.find { it.name == value },避免每次调用创建数组。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/rhencloud/sleepyxposed/MediaMethod.kt` around lines 24
- 26, Update MediaMethod.Companion.fromString to use the enum entries collection
instead of values(), calling entries.find { it.name == value } and preserving
the existing AUTO fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/com/rhencloud/sleepyxposed/ConfigContentProvider.kt`:
- Around line 60-67: Update enforceSystemOrSelf() for the METHOD_HEARTBEAT path
so UID 0/root is not accepted as a valid caller; require the caller to be
system_server or use another verifiable authentication mechanism before
HookHeartbeat.recordPing updates last_seen_ms. Leave unknown methods delegated
to super.call(...) without additional UID validation.

In `@app/src/main/java/com/rhencloud/sleepyxposed/ConfigManager.kt`:
- Around line 154-157: Update the cache invalidation flow around saveConfig and
ConfigContentProvider.call so the system_server process also invalidates its
cached configuration after a save. Add a provider notification and system-side
handler that invokes loadConfigFromXSharedPreferences(forceRefresh = true),
rather than relying on clearing the app process’s cachedSystemConfig fields;
keep the comments aligned with the actual cross-process behavior.
- Around line 262-280: Update saveConfigToJsonFiles so the globally readable
JSON mirrors exclude the sensitive secret field, while ConfigContentProvider
remains the channel for transferring it. Adjust the JSON fallback parsing and
hasRequiredFields validation to accept mirrors without secret and obtain that
value only through the provider or another private source; preserve validation
for all other required fields.

In `@app/src/main/java/com/rhencloud/sleepyxposed/MainActivity.kt`:
- Around line 14-20: Remove the hardcoded isAppearanceLightStatusBars assignment
from MainActivity’s setup while keeping enableEdgeToEdge() and the existing
SleepyTheme/SleepyApp content unchanged; allow edge-to-edge or theme handling to
determine status-bar icon appearance, and do not modify Scaffold innerPadding
handling.

In `@app/src/main/java/com/rhencloud/sleepyxposed/MediaListenerService.kt`:
- Around line 39-63: Update onListenerConnected to remove any existing
sessionsChangedListener from mediaSessionManager before creating and registering
a new listener, preserving the existing listener reference and cleanup behavior
in onListenerDisconnected.

In `@app/src/main/java/com/rhencloud/sleepyxposed/MediaStatusMonitor.kt`:
- Around line 66-68: Update MediaStatusMonitor initialization and polling so
handler runs on a dedicated HandlerThread rather than context.mainLooper,
keeping poll(), readViaDumpsys(),
ConfigManager.loadConfigFromXSharedPreferences(), and
manager.getActiveSessions(null) off the system_server main thread. In
readViaDumpsys(), add bounded process waiting using TimeUnit and ensure the
process is destroyed and cleaned up on timeout or completion.
- Around line 212-217: Update the dumpsys parsing flow in MediaStatusMonitor
around DUMPSYS_DESCRIPTION_REGEX.find(output) to split the output into
individual session blocks, select a block containing state=PLAYING, and extract
description only from that block. Preserve the existing blank-title and artist
normalization behavior, and return null when no playing session or description
is found.
- Around line 135-137: 两处媒体状态上报需仅在服务器成功响应后更新去重状态:在
app/src/main/java/com/rhencloud/sleepyxposed/MediaStatusMonitor.kt 的 135-137 行,将
lastStatus = status 移入 SleepyApiClient.sendDeviceStatus 的 onResponse 成功分支,并保持
onFailure 与非 2xx 分支不修改 lastStatus;在
app/src/main/java/com/rhencloud/sleepyxposed/MediaListenerService.kt 的 103-104
行对相同上报逻辑执行同样调整。

In `@app/src/main/java/com/rhencloud/sleepyxposed/RomDetector.kt`:
- Around line 41-70: Update recommend so the Recommendation cache is written
only when detectRom receives a non-null, valid context; when context is null,
return the computed recommendation without updating cached. Preserve cached
reuse for later calls and allow a subsequent contextual call to perform
detection and cache the accurate result.
- Around line 116-123: 在 AndroidManifest.xml 中为 RomDetector
查询的四个外部包添加对应的包可见性声明,或将这些查询限制在 system_server 进程中执行,确保两种进程的 ROM 判断一致。更新 hasPackage
使用不读取元数据的查询标志,并在 PackageManager 不再被其他代码使用时移除其导入。

In `@app/src/main/java/com/rhencloud/sleepyxposed/ui/ConfigScreen.kt`:
- Around line 230-237: 将 ConfigScreen 中打开设置和保存配置失败的硬编码英文提示替换为字符串资源:在
values/strings.xml 与 values-zh-rCN/strings.xml 中新增 open_settings_failed 和
config_save_failed,并在对应的异常处理 Toast 中通过 stringResource 或 getString
使用这些资源,确保中文环境显示中文翻译。
- Around line 52-91: Move ConfigManager.loadConfig and RomDetector.recommend out
of remember-time computation into a coroutine using Dispatchers.IO, and expose
their results through Compose state initialized with safe defaults. Update the
field states only after the loaded SleepyConfig is available, preserving the
existing fallback behavior. In the save action around ConfigManager.saveConfig,
launch from rememberCoroutineScope and perform the write inside
withContext(Dispatchers.IO), then update statusMessage or Toast on the UI
thread.

In `@app/src/main/java/com/rhencloud/sleepyxposed/ui/OverviewScreen.kt`:
- Around line 263-272: Update formatHeartbeatAge to treat negative agoMs values
the same as null, returning the never-heard-from placeholder; also append “ago”
to all non-placeholder formatted values so the output matches the documented
format.

In `@app/src/main/java/com/rhencloud/sleepyxposed/ui/SleepyApp.kt`:
- Around line 108-114: 修复 SleepyApp 中切换标签导致 ConfigScreen 未保存输入丢失的问题:将配置状态提升到
SleepyApp,或改用 rememberSaveable,并调整 Tab.Overview/Tab.Config 的组合方式以保留 ConfigScreen
状态。确保用户从配置页切换到概览页再返回时输入内容保持不变。

In `@app/src/main/java/com/rhencloud/sleepyxposed/XposedProbe.kt`:
- Around line 13-17: 为 XposedProbe.isModuleActive()
添加实际调用方,并将其接入应用现有的启动或初始化流程,使探针结果参与运行逻辑;保留当前方法签名及其在发布构建中的可调用性。

In `@README.md`:
- Around line 30-35: Update the 自动(推荐) row description to explicitly state that
it automatically selects the most suitable option between 系统钩子 and 通知监听,
matching the behavior of RomDetector.recommend; do not include Dumpsys Shell as
an automatic choice.

---

Nitpick comments:
In `@app/proguard-rules.pro`:
- Around line 35-38: 确认代码库中已不存在自定义 View 或依赖 XML 属性访问器后,移除 proguard-rules.pro 中针对
android.view.View 子类 set*/get* 方法的 -keepclassmembers 规则;若仍存在相关自定义 View,则保留该规则。

In `@app/src/main/AndroidManifest.xml`:
- Around line 40-46: 在 AndroidManifest.xml 的 ConfigContentProvider 声明旁添加注释,明确说明该
provider 因需支持 system UID 而保持 exported=true,访问控制由
ConfigContentProvider.enforceSystemOrSelf() 在代码中实现,新增访问分支必须继续执行该校验且不可移除。

In `@app/src/main/java/com/rhencloud/sleepyxposed/ConfigManager.kt`:
- Around line 295-299: 更新 getPrimaryPublicConfigFile,移除硬编码的
/storage/emulated/0,改用 Environment.getExternalStorageDirectory() 作为外部存储根目录拼接
MODULE_PACKAGE_NAME、FALLBACK_DIR 和 FALLBACK_FILE_NAME,并与回退分支保持一致。
- Around line 114-145: Update loadViaContentProvider and
ConfigContentProvider.COLUMNS to use one shared set of column-name constants
instead of repeating string literals such as server_url and media_method. Ensure
the cursor-reading helpers in loadViaContentProvider reference those shared
constants so provider schema changes cannot leave reads silently empty.
- Around line 89-101: Update loadConfigFromXSharedPreferencesUncached to store
and reuse the results of the initial loadViaContentProvider and
loadConfigFromJsonFiles calls, including incomplete configurations, so the final
fallback does not repeat either provider IPC or JSON file read. Preserve the
existing preference order and required-field validation while returning the
first available result or SleepyConfig().

In `@app/src/main/java/com/rhencloud/sleepyxposed/HookHeartbeat.kt`:
- Around line 95-132: Update the heartbeat storage used by recordPing and
recordFrameworkInfo to obtain a device-protected storage context via
createDeviceProtectedStorageContext() before accessing PREFS_NAME. Apply the
same device-protected context in frameworkInfo and lastSeenMillisAgo so all
heartbeat and framework metadata reads and writes use the same storage area and
remain available before first unlock.

In `@app/src/main/java/com/rhencloud/sleepyxposed/MediaListenerService.kt`:
- Around line 88-101: 在媒体状态处理逻辑中提取并复用 playing.metadata 的结果,避免在标题、艺人和专辑艺人读取处重复调用
MediaController.getMetadata()。更新相关代码以使用局部 metadata 变量,同时保持现有标题判断、艺人回退顺序及
status/using 行为不变。
- Around line 73-77: Move the full handleControllers flow, including
ConfigManager.loadConfig(this), off the OnActiveSessionsChangedListener
main-thread callback by dispatching it to a dedicated background HandlerThread.
Preserve the existing configuration validation and controller handling behavior,
and ensure the background thread is started and cleaned up with the service
lifecycle.

In `@app/src/main/java/com/rhencloud/sleepyxposed/MediaMethod.kt`:
- Around line 24-26: Update MediaMethod.Companion.fromString to use the enum
entries collection instead of values(), calling entries.find { it.name == value
} and preserving the existing AUTO fallback.

In `@app/src/main/java/com/rhencloud/sleepyxposed/ui/ConfigScreen.kt`:
- Around line 191-215: Update the media-method container around the
MediaMethod.entries loop to use selectableGroup() only for these radio-option
rows, set RadioButton.onClick to null so Row remains the sole selectable
accessibility node, and add minimumInteractiveComponentSize() or an equivalent
minimum 48.dp height to each selectable Row while preserving its existing
selection behavior.

In `@app/src/main/java/com/rhencloud/sleepyxposed/ui/SleepyApp.kt`:
- Around line 47-55: 更新 SleepyApp 中的 navColors 缓存实现:不要在 remember 的计算块内调用可组合的
NavigationBarItemDefaults.colors。改为按当前 colorScheme 使用 remember(colorScheme)
直接构造等价的 NavigationBarItemColors,并完整保留选中、未选中及禁用状态颜色配置;同时删除或修正文档性注释。

In `@app/src/main/res/values-zh-rCN/strings.xml`:
- Around line 39-48: Remove the unused string resources action_back,
home_section_module, home_section_runtime, home_section_system,
home_section_actions, open_settings, open_settings_summary, status_module_hook,
status_active, status_inactive, status_app_version, status_android,
status_device, and status_brand from both the default strings.xml and the
Chinese values-zh-rCN strings.xml resources.

In `@app/src/main/res/values/themes.xml`:
- Around line 4-8: 移除 themes.xml 中的 android:statusBarColor 和
android:navigationBarColor 配置,保留 android:windowLightStatusBar 与
android:windowLightNavigationBar;继续通过 Compose 的 edge-to-edge/insets 处理控制系统栏外观。

In `@gradle.properties`:
- Line 6: Review the org.gradle.configuration-cache setting against the PR’s
performance goal: remove the explicit false value if the build supports
Configuration Cache; otherwise retain it only with a concise comment documenting
the incompatible plugin or signing logic and the follow-up needed to re-enable
it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ee2eb405-c3c8-4be1-af37-772e1fc0117f

📥 Commits

Reviewing files that changed from the base of the PR and between 2507a34 and 7c15067.

📒 Files selected for processing (29)
  • README.md
  • app/build.gradle.kts
  • app/proguard-rules.pro
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/com/rhencloud/sleepyxposed/ConfigContentProvider.kt
  • app/src/main/java/com/rhencloud/sleepyxposed/ConfigManager.kt
  • app/src/main/java/com/rhencloud/sleepyxposed/ForegroundAppMonitor.kt
  • app/src/main/java/com/rhencloud/sleepyxposed/HookHeartbeat.kt
  • app/src/main/java/com/rhencloud/sleepyxposed/MainActivity.kt
  • app/src/main/java/com/rhencloud/sleepyxposed/MediaListenerService.kt
  • app/src/main/java/com/rhencloud/sleepyxposed/MediaMethod.kt
  • app/src/main/java/com/rhencloud/sleepyxposed/MediaStatusMonitor.kt
  • app/src/main/java/com/rhencloud/sleepyxposed/ModuleMain.kt
  • app/src/main/java/com/rhencloud/sleepyxposed/RomDetector.kt
  • app/src/main/java/com/rhencloud/sleepyxposed/StatusSnapshot.kt
  • app/src/main/java/com/rhencloud/sleepyxposed/XposedProbe.kt
  • app/src/main/java/com/rhencloud/sleepyxposed/ui/AppTheme.kt
  • app/src/main/java/com/rhencloud/sleepyxposed/ui/ConfigScreen.kt
  • app/src/main/java/com/rhencloud/sleepyxposed/ui/OverviewScreen.kt
  • app/src/main/java/com/rhencloud/sleepyxposed/ui/SleepyApp.kt
  • app/src/main/res/layout/activity_main.xml
  • app/src/main/res/values-zh-rCN/strings.xml
  • app/src/main/res/values/colors.xml
  • app/src/main/res/values/strings.xml
  • app/src/main/res/values/themes.xml
  • build.gradle
  • gradle.properties
  • gradle/wrapper/gradle-wrapper.properties
  • settings.gradle.kts
💤 Files with no reviewable changes (2)
  • settings.gradle.kts
  • app/src/main/res/layout/activity_main.xml

Comment on lines +154 to +157
// Invalidate the system-side cache immediately so a Save takes effect without waiting out
// the TTL window.
cachedSystemConfig = null
cachedSystemConfigAt = 0L

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

跨进程静态缓存无法通过此处清除。

cachedSystemConfig 是进程内静态字段。saveConfig 在应用进程执行,只清除应用进程的副本。loadConfigFromXSharedPreferences 的实际调用方在 system_server 进程,其缓存副本不受影响,仍需等满 SYSTEM_CACHE_TTL_MS。当前注释描述与运行行为不一致。

建议修正注释,或在 ConfigContentProvider.call 中新增一个使系统侧缓存失效的方法,由 system_server 侧在收到通知后调用 loadConfigFromXSharedPreferences(forceRefresh = true)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/rhencloud/sleepyxposed/ConfigManager.kt` around lines
154 - 157, Update the cache invalidation flow around saveConfig and
ConfigContentProvider.call so the system_server process also invalidates its
cached configuration after a save. Add a provider notification and system-side
handler that invokes loadConfigFromXSharedPreferences(forceRefresh = true),
rather than relying on clearing the app process’s cachedSystemConfig fields;
keep the comments aligned with the actual cross-process behavior.

Comment on lines +262 to +280
private fun saveConfigToJsonFiles(context: Context, config: SleepyConfig): Boolean {
val json = configToJson(config)
var any = false
for (file in getAllJsonCandidates(context)) {
try {
if (!file.exists()) {
continue
val parent = file.parentFile
if (parent != null && !parent.exists()) {
parent.mkdirs()
}
file.writeText(json)
// Best-effort world-readable so system_server can open without app identity.
file.setReadable(true, false)
parent?.setReadable(true, false)
parent?.setExecutable(true, false)
any = true
} catch (_: Exception) {}
}
return any
}

@coderabbitai coderabbitai Bot Aug 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

不要把 secret 写入全局可读的 JSON 文件。

saveConfigToJsonFiles 将完整配置(含 secret 服务器密钥)写入 /storage/emulated/0/Android/media/...,并调用 file.setReadable(true, false)parent?.setReadable(true, false),使任意 UID 可读。任何取得媒体/存储读取权限的第三方应用都能读取该密钥,并伪造上报请求。

建议方案:

  • 依赖已有的 ConfigContentProvider 通道传递敏感字段,JSON 镜像仅保留非敏感字段。
  • 如果 JSON 镜像必须包含 secret,请对 secret 做加密存储,密钥保留在应用私有区。
🔒 示例:JSON 镜像剔除敏感字段
-  private fun configToJson(config: SleepyConfig): String {
+  /** 公共镜像不包含 secret;secret 只经 ConfigContentProvider 传递。 */
+  private fun configToJson(config: SleepyConfig): String {
     return JSONObject()
             .apply {
               put(KEY_SERVER_URL, config.serverUrl)
-              put(KEY_SECRET, config.secret)
               put(KEY_DEVICE_ID, config.deviceId)

注意:hasRequiredFields() 要求 secret 非空,因此剔除该字段后需要同步调整 JSON 回退路径的完整性判定。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/rhencloud/sleepyxposed/ConfigManager.kt` around lines
262 - 280, Update saveConfigToJsonFiles so the globally readable JSON mirrors
exclude the sensitive secret field, while ConfigContentProvider remains the
channel for transferring it. Adjust the JSON fallback parsing and
hasRequiredFields validation to accept mirrors without secret and obtain that
value only through the provider or another private source; preserve validation
for all other required fields.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

谁这么闲得慌去偷这服务器密钥

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@XFJ-YYQF,这不是攻击者必须专门盯上该项目的问题。

公开 JSON 中的 secret 可被有存储访问能力的应用批量扫描、复制或随备份泄露。拿到它的任何人都可以伪造该设备的上报请求。风险取决于该密钥是否可用于认证,而不是是否有人“专门去偷”。

如果该密钥不具备认证价值,或后端已限制其权限和有效期,可以接受这个风险。否则,建议不要将 secret 写入公共 JSON。ConfigContentProvider 已可作为传递敏感字段的私有通道。

(ᵕ—ᴗ—)🐇

You are interacting with an AI system.

Comment on lines +108 to +114
// Only the active tab is composed.
Box(Modifier.padding(padding)) {
when (tab) {
Tab.Overview -> OverviewScreen(snapshot = snapshot)
Tab.Config -> ConfigScreen()
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

切换标签会丢失未保存的配置输入。

when (tab) 只组合当前标签。ConfigScreen 内的状态使用 remember,离开组合后即被丢弃。用户在配置页输入内容后切到概览页再返回,输入内容会全部清空。请把配置状态提升到 SleepyApp,或在 ConfigScreen 中改用 rememberSaveable 并保留两个页面的组合。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/rhencloud/sleepyxposed/ui/SleepyApp.kt` around lines
108 - 114, 修复 SleepyApp 中切换标签导致 ConfigScreen 未保存输入丢失的问题:将配置状态提升到 SleepyApp,或改用
rememberSaveable,并调整 Tab.Overview/Tab.Config 的组合方式以保留 ConfigScreen
状态。确保用户从配置页切换到概览页再返回时输入内容保持不变。

Comment on lines +13 to +17
object XposedProbe {
@JvmStatic
@Suppress("FunctionOnlyReturningConstant")
fun isModuleActive(): Boolean = false
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 查找 XposedProbe 的引用与 keep 规则
rg -n --glob '!**/build/**' 'XposedProbe|isModuleActive'
fd -H 'proguard-rules.pro' --exec cat -n {}
fd -H 'build.gradle.kts' --exec rg -n 'minify|proguard|isShrinkResources' {}

Repository: ReCloudStudio/SleepyXposed

Length of output: 164


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(XposedProbe\.kt|proguard-rules\.pro|build\.gradle\.kts|AndroidManifest\.xml)$'
printf '%s\n' '--- probe references ---'
rg -n --hidden --glob '!**/build/**' --glob '!**/.git/**' 'XposedProbe|isModuleActive|HookHeartbeat|isRecentlyActive' . || true
printf '%s\n' '--- relevant build settings ---'
rg -n --hidden --glob '!**/build/**' --glob '!**/.git/**' 'minifyEnabled|isMinifyEnabled|minify|proguardFiles|shrinkResources|consumerProguardFiles' . || true

Repository: ReCloudStudio/SleepyXposed

Length of output: 2627


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- app/build.gradle.kts ---'
cat -n app/build.gradle.kts
printf '%s\n' '--- app/proguard-rules.pro ---'
cat -n app/proguard-rules.pro
printf '%s\n' '--- XposedProbe.kt ---'
cat -n app/src/main/java/com/rhencloud/sleepyxposed/XposedProbe.kt
printf '%s\n' '--- references including documentation ---'
rg -n --hidden --glob '!**/build/**' --glob '!**/.git/**' -C 3 'XposedProbe|isModuleActive' . || true

Repository: ReCloudStudio/SleepyXposed

Length of output: 9092


🏁 Script executed:

#!/bin/bash
python3 - <<'PY'
from pathlib import Path
import subprocess

files = subprocess.check_output(["git", "ls-files"], text=True).splitlines()
needle = "isModuleActive"
hits = []
for name in files:
    path = Path(name)
    if path.suffix not in {".kt", ".java", ".xml", ".pro", ".kts", ".md"}:
        continue
    text = path.read_text(errors="replace")
    for lineno, line in enumerate(text.splitlines(), 1):
        if needle in line or "XposedProbe" in line:
            hits.append((name, lineno, line.strip()))

print("all matching tracked-source references:")
for hit in hits:
    print(f"{hit[0]}:{hit[1]}:{hit[2]}")

runtime_calls = [
    h for h in hits
    if h[0].endswith((".kt", ".java"))
    and "fun isModuleActive" not in h[2]
    and "object XposedProbe" not in h[2]
]
print(f"\nnon-declaration JVM-source references: {len(runtime_calls)}")
for hit in runtime_calls:
    print(f"{hit[0]}:{hit[1]}:{hit[2]}")

gradle = Path("app/build.gradle.kts").read_text()
rules = Path("app/proguard-rules.pro").read_text()
print("\nrelease minification:", 'release {' in gradle and 'isMinifyEnabled = true' in gradle)
print("release applies app/proguard-rules.pro:",
      gradle.count('"proguard-rules.pro"') >= 2)
print("probe keep rule present:",
      "-keep class com.rhencloud.sleepyxposed.XposedProbe" in rules
      and "public static boolean isModuleActive();" in rules)
PY

Repository: ReCloudStudio/SleepyXposed

Length of output: 679


XposedProbe.isModuleActive() 添加实际调用方。

发布构建已启用 R8,且 app/proguard-rules.pro 已保留该方法。当前源码没有调用方,因此探针不会影响应用运行逻辑。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/rhencloud/sleepyxposed/XposedProbe.kt` around lines 13
- 17, 为 XposedProbe.isModuleActive()
添加实际调用方,并将其接入应用现有的启动或初始化流程,使探针结果参与运行逻辑;保留当前方法签名及其在发布构建中的可调用性。

Comment thread README.md
- restrict heartbeat method to system_server only
- exclude secret from world-readable JSON config mirror
- run media monitor/listener work off the main thread
- bound dumpsys waits and parse only the PLAYING session block
- keep unsaved config edits across tab switches
- remove unused strings, dead theme items and View keep rule
- add manifest package visibility queries for ROM detection
Adopt main's package rename to io.github.recloudstudio.sleepyxposed while
keeping the PR's Compose UI, media status reporting, heartbeat and all
CodeRabbit fixes. Merges main's legacy XSharedPreferences/prefs-XML
fallbacks into ConfigManager.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/io/github/recloudstudio/sleepyxposed/MediaStatusMonitor.kt (1)

140-164: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

为异步上报维护进行中的状态和请求顺序。

两处实现只在 HTTP 成功回调中更新 lastStatus。在回调完成前,下一次轮询或媒体会话回调仍会发送相同状态。不同请求的回调也可以乱序完成,并覆盖较新的 lastStatus

  • app/src/main/java/io/github/recloudstudio/sleepyxposed/MediaStatusMonitor.kt#L140-L164: 在发送前记录进行中的状态或请求序号。仅在回调仍对应最新请求时更新已送达状态。
  • app/src/main/java/io/github/recloudstudio/sleepyxposed/MediaListenerService.kt#L123-L146: 使用相同的进行中状态和顺序控制。将回调结果切回后台 Handler 后再修改状态。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/io/github/recloudstudio/sleepyxposed/MediaStatusMonitor.kt`
around lines 140 - 164, 异步状态上报在回调完成前会重复发送,且乱序回调可能覆盖较新的状态。更新
MediaStatusMonitor.kt 的状态发送逻辑,在发送前记录进行中的状态或请求序号,并仅在回调仍对应最新请求且 HTTP 成功时更新
lastStatus;同时在 MediaListenerService.kt 的对应发送逻辑中采用相同的顺序控制,并将回调结果切回后台 Handler
后再修改状态。
🧹 Nitpick comments (3)
app/src/main/java/io/github/recloudstudio/sleepyxposed/StatusSnapshot.kt (2)

123-139: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

startsWith("$pkg/") 的匹配范围过宽。

第 134 行把本包下任意组件条目都判定为已启用。当前包内只有 MediaListenerService,所以结果正确。如果以后新增第二个 NotificationListenerService,该判断会返回错误结果。建议仅比较 cn.flattenToString()cn.flattenToShortString()

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/io/github/recloudstudio/sleepyxposed/StatusSnapshot.kt`
around lines 123 - 139, Update isNotificationListenerEnabled to match only the
target MediaListenerService component: compare each entry against
cn.flattenToString() or cn.flattenToShortString(), and remove the broad "$pkg/"
prefix check.

37-81: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

建议在文档中标明 collect 必须在后台线程调用。

collect 会执行磁盘与 Settings.Secure 读取(第 39、51、62、65 行)。当前唯一调用方 SleepyApp.refresh 使用 Dispatchers.Defaultapp/src/main/java/io/github/recloudstudio/sleepyxposed/ui/SleepyApp.kt 第 57 行),因此现在不会阻塞主线程。为防止将来在主线程直接调用,请在 KDoc 中显式说明该约束。阻塞 I/O 更适合 Dispatchers.IO

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/io/github/recloudstudio/sleepyxposed/StatusSnapshot.kt`
around lines 37 - 81, 为 StatusSnapshot.collect 添加 KDoc,明确说明该方法会执行磁盘和
Settings.Secure 读取,必须在后台线程调用,并建议使用 Dispatchers.IO;保留现有实现和调用行为不变。
app/src/main/java/io/github/recloudstudio/sleepyxposed/ui/OverviewScreen.kt (1)

264-272: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

心跳时间单位为硬编码英文。

第 268-270 行返回 "12s ago" 一类的英文文本。项目已提供 values-zh-rCN/strings.xml,其他文案都使用字符串资源。请为秒、分、小时三种单位新增字符串资源,使中文环境显示中文单位。负值处理与格式说明现在一致。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/io/github/recloudstudio/sleepyxposed/ui/OverviewScreen.kt`
around lines 264 - 272, Update formatHeartbeatAge in OverviewScreen to replace
the hardcoded “s ago”, “m ago”, and “h ago” text with string resources. Add
resources for the seconds, minutes, and hours formats in the default and
values-zh-rCN strings.xml files, then format the calculated values through those
resources while preserving the existing null/negative handling and thresholds.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/proguard-rules.pro`:
- Around line 5-7: Update the ProGuard rules alongside the existing ModuleMain
keep rule to explicitly keep io.github.recloudstudio.sleepyxposed.LegacyEntry
and all its members, ensuring the legacy Xposed entry declared in xposed_init is
neither renamed nor removed.

In `@app/src/main/java/io/github/recloudstudio/sleepyxposed/ConfigManager.kt`:
- Around line 98-104: 调整 ConfigManager 中的配置加载优先级:不要在
loadConfigFromJsonFiles(null) 返回仅含公共字段的 JSON 后立即返回;应先尝试
loadViaLegacyXSharedPreferences() 和
loadConfigFromPrefsXmlFiles(),优先返回包含必要认证字段的配置。仅在这些可认证配置源不可用时,才将公共 JSON 用于不需要
secret 的回退路径。

In
`@app/src/main/java/io/github/recloudstudio/sleepyxposed/MediaListenerService.kt`:
- Around line 43-47: 在 MediaListenerService 的 onListenerConnected() 中创建新
HandlerThread 前,先移除 backgroundHandler 上的旧回调并停止现有 backgroundThread;然后再初始化并启动新的线程与
Handler,避免重复连接时旧任务继续运行。

In
`@app/src/main/java/io/github/recloudstudio/sleepyxposed/MediaStatusMonitor.kt`:
- Around line 218-243: 在读取 dumpsys 输出的流程中,调整 process.waitFor 调用前的逻辑,通过独立读取任务持续排空
process.inputStream,避免子进程因管道缓冲区满而无法退出;发生超时时先销毁
process,再等待或取消读取任务,并确保正常完成时复用已收集的输出进行后续 playingBlock 和 description 解析。

---

Outside diff comments:
In
`@app/src/main/java/io/github/recloudstudio/sleepyxposed/MediaStatusMonitor.kt`:
- Around line 140-164: 异步状态上报在回调完成前会重复发送,且乱序回调可能覆盖较新的状态。更新 MediaStatusMonitor.kt
的状态发送逻辑,在发送前记录进行中的状态或请求序号,并仅在回调仍对应最新请求且 HTTP 成功时更新 lastStatus;同时在
MediaListenerService.kt 的对应发送逻辑中采用相同的顺序控制,并将回调结果切回后台 Handler 后再修改状态。

---

Nitpick comments:
In `@app/src/main/java/io/github/recloudstudio/sleepyxposed/StatusSnapshot.kt`:
- Around line 123-139: Update isNotificationListenerEnabled to match only the
target MediaListenerService component: compare each entry against
cn.flattenToString() or cn.flattenToShortString(), and remove the broad "$pkg/"
prefix check.
- Around line 37-81: 为 StatusSnapshot.collect 添加 KDoc,明确说明该方法会执行磁盘和
Settings.Secure 读取,必须在后台线程调用,并建议使用 Dispatchers.IO;保留现有实现和调用行为不变。

In `@app/src/main/java/io/github/recloudstudio/sleepyxposed/ui/OverviewScreen.kt`:
- Around line 264-272: Update formatHeartbeatAge in OverviewScreen to replace
the hardcoded “s ago”, “m ago”, and “h ago” text with string resources. Add
resources for the seconds, minutes, and hours formats in the default and
values-zh-rCN strings.xml files, then format the calculated values through those
resources while preserving the existing null/negative handling and thresholds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a65bc18c-e79a-41fc-ad87-2666d0ed7def

📥 Commits

Reviewing files that changed from the base of the PR and between 7c15067 and 9ef7749.

📒 Files selected for processing (23)
  • README.md
  • app/build.gradle.kts
  • app/proguard-rules.pro
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/io/github/recloudstudio/sleepyxposed/ConfigContentProvider.kt
  • app/src/main/java/io/github/recloudstudio/sleepyxposed/ConfigManager.kt
  • app/src/main/java/io/github/recloudstudio/sleepyxposed/ForegroundAppMonitor.kt
  • app/src/main/java/io/github/recloudstudio/sleepyxposed/HookHeartbeat.kt
  • app/src/main/java/io/github/recloudstudio/sleepyxposed/MainActivity.kt
  • app/src/main/java/io/github/recloudstudio/sleepyxposed/MediaListenerService.kt
  • app/src/main/java/io/github/recloudstudio/sleepyxposed/MediaMethod.kt
  • app/src/main/java/io/github/recloudstudio/sleepyxposed/MediaStatusMonitor.kt
  • app/src/main/java/io/github/recloudstudio/sleepyxposed/RomDetector.kt
  • app/src/main/java/io/github/recloudstudio/sleepyxposed/StatusSnapshot.kt
  • app/src/main/java/io/github/recloudstudio/sleepyxposed/XposedProbe.kt
  • app/src/main/java/io/github/recloudstudio/sleepyxposed/ui/AppTheme.kt
  • app/src/main/java/io/github/recloudstudio/sleepyxposed/ui/ConfigScreen.kt
  • app/src/main/java/io/github/recloudstudio/sleepyxposed/ui/OverviewScreen.kt
  • app/src/main/java/io/github/recloudstudio/sleepyxposed/ui/SleepyApp.kt
  • app/src/main/res/values-zh-rCN/strings.xml
  • app/src/main/res/values/strings.xml
  • app/src/main/res/values/themes.xml
  • gradle.properties
💤 Files with no reviewable changes (1)
  • app/src/main/res/values/themes.xml
🚧 Files skipped from review as they are similar to previous changes (5)
  • app/src/main/res/values/strings.xml
  • gradle.properties
  • README.md
  • app/build.gradle.kts
  • app/src/main/res/values-zh-rCN/strings.xml

Comment thread app/proguard-rules.pro
Comment on lines +5 to +7
-keep class io.github.recloudstudio.sleepyxposed.ModuleMain { *; }
-keep class * extends io.github.libxposed.api.XposedModule { *; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 3 \
  'ModuleMain|XposedProbe|XposedModule|xposed_init' \
  app/src app/proguard-rules.pro || true

rg -n -C 3 \
  'android:name="[^"]*(MainActivity|ConfigContentProvider|MediaListenerService)' \
  app/src || true

Repository: ReCloudStudio/SleepyXposed

Length of output: 9128


🏁 Script executed:

set -euo pipefail

printf '%s\n' '== APK and build artifacts =='
find . -type f \( -iname '*.apk' -o -iname '*.aab' \) -print

printf '%s\n' '== Xposed-related declarations and release configuration =='
rg -n -C 4 \
  'java_init\.list|xposed_init|ModuleMain|XposedModule|XposedProbe|proguardFiles|minifyEnabled|shrinkResources|release' \
  app build.gradle* settings.gradle* gradle.properties 2>/dev/null || true

printf '%s\n' '== Resource and manifest files =='
git ls-files 'app/src/main/resources/**' 'app/src/main/AndroidManifest.xml' 'app/proguard-rules.pro'

Repository: ReCloudStudio/SleepyXposed

Length of output: 11774


🏁 Script executed:

set -euo pipefail

printf '%s\n' '== Xposed metadata =='
for f in app/src/main/resources/META-INF/xposed/java_init.list \
         app/src/main/resources/META-INF/xposed/module.prop \
         app/src/main/resources/META-INF/xposed/scope.list; do
  echo "--- $f"
  cat -n "$f"
done

printf '%s\n' '== Manifest =='
cat -n app/src/main/AndroidManifest.xml

printf '%s\n' '== All Xposed declarations in tracked files =='
git grep -n -E 'java_init|xposed_init|XposedModule|ModuleMain|XposedProbe|META-INF/xposed|xposed' -- \
  ':!*.md' ':!*.lock' || true

Repository: ReCloudStudio/SleepyXposed

Length of output: 16113


🏁 Script executed:

set -euo pipefail

printf '%s\n' '== ProGuard rules =='
cat -n app/proguard-rules.pro

printf '%s\n' '== Legacy Xposed entry =='
cat -n app/src/main/java/io/github/recloudstudio/sleepyxposed/LegacyEntry.kt

printf '%s\n' '== Resource packaging configuration =='
sed -n '70,90p' app/build.gradle.kts

printf '%s\n' '== LegacyEntry keep coverage =='
rg -n -C 2 \
  'LegacyEntry|IXposedHookLoadPackage|implements| -keep' \
  app/proguard-rules.pro app/src/main/java app/src/main/resources app/src/main/assets || true

Repository: ReCloudStudio/SleepyXposed

Length of output: 5082


保留 LegacyEntry 的 Xposed 入口

app/src/main/assets/xposed_init 声明了 io.github.recloudstudio.sleepyxposed.LegacyEntry,但现有规则未保留该类。LegacyEntry 实现的是 IXposedHookLoadPackage,不匹配 XposedModule 子类规则。增加 -keep class io.github.recloudstudio.sleepyxposed.LegacyEntry { *; },否则 R8 可能重命名或移除该类,导致传统 Xposed 无法加载入口。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/proguard-rules.pro` around lines 5 - 7, Update the ProGuard rules
alongside the existing ModuleMain keep rule to explicitly keep
io.github.recloudstudio.sleepyxposed.LegacyEntry and all its members, ensuring
the legacy Xposed entry declared in xposed_init is neither renamed nor removed.

Comment on lines +98 to +104
val json = loadConfigFromJsonFiles(null)
if (json != null && json.hasRequiredPublicFields()) return json

loadViaLegacyXSharedPreferences()?.takeIf { it.hasRequiredFields() }?.let { return it }
loadConfigFromPrefsXmlFiles()?.let { return it }

return loadViaContentProvider(systemContext)
?: loadConfigFromJsonFiles(null)
?: loadViaLegacyXSharedPreferences()
?: loadConfigFromPrefsXmlFiles()
?: SleepyConfig()
return provider ?: json ?: SleepyConfig()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

在无密钥 JSON 回退前读取可认证的配置源。

第 99 行会立即返回不含 secret 的公共 JSON。第 101-102 行的旧配置源不会执行。

MediaStatusMonitorapp/src/main/java/io/github/recloudstudio/sleepyxposed/MediaStatusMonitor.kt 第 111-115 行要求 secret。当 Provider 不可用但旧配置源可读时,媒体上报仍会被跳过。

请先尝试 loadViaLegacyXSharedPreferences()loadConfigFromPrefsXmlFiles()。仅在不需要认证字段的调用路径中使用公共 JSON。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/io/github/recloudstudio/sleepyxposed/ConfigManager.kt`
around lines 98 - 104, 调整 ConfigManager 中的配置加载优先级:不要在
loadConfigFromJsonFiles(null) 返回仅含公共字段的 JSON 后立即返回;应先尝试
loadViaLegacyXSharedPreferences() 和
loadConfigFromPrefsXmlFiles(),优先返回包含必要认证字段的配置。仅在这些可认证配置源不可用时,才将公共 JSON 用于不需要
secret 的回退路径。

Comment on lines 43 to +47
override fun onListenerConnected() {
super.onListenerConnected()
try {
backgroundThread = HandlerThread(TAG).also { it.start() }
backgroundHandler = Handler(backgroundThread!!.looper)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

停止旧的后台线程后再创建新线程。

如果系统再次调用 onListenerConnected(),第 46 行会创建新线程并覆盖 backgroundThread。旧线程不会在 onListenerDisconnected() 中停止。

旧线程中的任务会继续运行,并会增加重复媒体上报和线程泄漏风险。请在创建新线程前移除旧回调并停止旧线程。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/main/java/io/github/recloudstudio/sleepyxposed/MediaListenerService.kt`
around lines 43 - 47, 在 MediaListenerService 的 onListenerConnected() 中创建新
HandlerThread 前,先移除 backgroundHandler 上的旧回调并停止现有 backgroundThread;然后再初始化并启动新的线程与
Handler,避免重复连接时旧任务继续运行。

Comment on lines +218 to +243
if (!process.waitFor(DUMPSYS_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
log("$TAG: dumpsys media_session timed out")
return null
}
val output = BufferedReader(InputStreamReader(process.inputStream)).use { it.readText() }

// dumpsys prints one block per media session; the first description= in the whole
// dump may belong to a paused session. Select only the block that is actually
// PLAYING and extract its description, so we never report the wrong track.
val playingBlock =
output
.split(Regex("(?m)^\\s*#\\d+:\\s*MediaSession"))
.firstOrNull { block ->
block.contains("state=PLAYING") || block.contains("state=3")
}
?: return null

val match = DUMPSYS_DESCRIPTION_REGEX.find(output) ?: return null
val match = DUMPSYS_DESCRIPTION_REGEX.find(playingBlock) ?: return null
val title = match.groupValues[1].trim()
val artist = match.groupValues[2].trim().let { if (it == "null") "" else it }
if (title.isBlank()) null else MediaInfo(title, artist)
} catch (e: Exception) {
log("$TAG: dumpsys read failed: ${e.message}")
null
} finally {
process?.destroy()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

在等待进程结束前并发读取 stdout。

第 218 行先等待 dumpsys 退出。第 222 行才读取 stdout。

如果输出填满子进程管道缓冲区,dumpsys 会等待写入空间且无法退出。第 218 行随后超时,并将正常的媒体状态误判为不可用。

请在等待期间使用独立读取任务持续排空 process.inputStream。超时时先销毁进程,再等待或取消读取任务。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/io/github/recloudstudio/sleepyxposed/MediaStatusMonitor.kt`
around lines 218 - 243, 在读取 dumpsys 输出的流程中,调整 process.waitFor
调用前的逻辑,通过独立读取任务持续排空 process.inputStream,避免子进程因管道缓冲区满而无法退出;发生超时时先销毁
process,再等待或取消读取任务,并确保正常完成时复用已收集的输出进行后续 playingBlock 和 description 解析。

@RhenCloud
RhenCloud merged commit 870eeea into ReCloudStudio:main Aug 1, 2026
3 checks passed
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