Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

| 方式 | 说明 | 是否需要额外授权 |
| ------------------ | -------------------------------------------------------------------- | ----------------- |
| **自动(推荐)** | 根据检测到的 Android 版本和系统 UI(ROM)厂商,自动在下面两种方式中选择最合适的一种 | 视结果而定 |
| **自动(推荐)** | 根据检测到的 Android 版本和系统 UI(ROM)厂商,自动在「系统钩子」和「通知监听」两种方式中选择最合适的一种(不自动选择 Dumpsys Shell) | 视结果而定 |
| **系统钩子** | 通过 Xposed 钩子在 system_server 进程内直接调用 `MediaSessionManager` 获取,零配置 | 否 |
| **通知监听** | 使用标准 `NotificationListenerService` API,在应用自身进程内运行,跨 ROM 兼容性最好 | 是(需在设置中手动授权一次,可点击应用内按钮直达) |
| **Dumpsys Shell** | 通过解析 `dumpsys media_session` 命令输出获取,思路参考自 Magisk 版 Sleepy 脚本,作为兼容性/调试备用方案 | 否(复用系统钩子权限) |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down
178 changes: 113 additions & 65 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,79 +1,127 @@
import java.util.Properties

plugins {
id("com.android.application")
kotlin("android")
}

val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties().apply {
if (keystorePropertiesFile.exists()) {
keystorePropertiesFile.inputStream().use { load(it) }
}
}

android {
namespace = "io.github.recloudstudio.sleepyxposed"
compileSdk = 34

import java.util.Properties
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
id("com.android.application")
kotlin("android")
id("org.jetbrains.kotlin.plugin.compose")
}

val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties().apply {
if (keystorePropertiesFile.exists()) {
keystorePropertiesFile.inputStream().use { load(it) }
}
}

android {
namespace = "io.github.recloudstudio.sleepyxposed"
compileSdk = 36

defaultConfig {
applicationId = "io.github.recloudstudio.sleepyxposed"
minSdk = 24
targetSdk = 34
versionCode = 2
targetSdk = 35
versionCode = 2
versionName = "2.0"
buildConfigField("int", "XPOSED_API", "101")
buildConfigField("String", "MODULE_CHANNEL", "\"release\"")

// Phones only — drop x86 emulator ABIs from the APK
ndk {
abiFilters += listOf("armeabi-v7a", "arm64-v8a")
}
}

signingConfigs {
if (keystorePropertiesFile.exists()) {
create("release") {
storeFile = rootProject.file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["storePassword"] as String
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
}
}
}
signingConfigs {
if (keystorePropertiesFile.exists()) {
create("release") {
storeFile = rootProject.file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["storePassword"] as String
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
}
}
}
buildTypes {
release {
isMinifyEnabled = false
if (keystorePropertiesFile.exists()) {
signingConfig = signingConfigs.getByName("release")
}
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"

buildTypes {
debug {
// Still shrink so debug APKs used for sideload stay small/fast.
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
release {
isMinifyEnabled = true
isShrinkResources = true
if (keystorePropertiesFile.exists()) {
signingConfig = signingConfigs.getByName("release")
}
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

buildFeatures {
compose = true
buildConfig = true
}

packaging {
resources {
merges += "META-INF/xposed/*"
excludes +=
setOf(
"/META-INF/{AL2.0,LGPL2.1}",
"META-INF/*.kotlin_module",
"META-INF/*.version",
"META-INF/LICENSE*",
"META-INF/NOTICE*",
"DebugProbesKt.bin",
"kotlin-tooling-metadata.json"
)
}
}
}

dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")

implementation("com.squareup.okhttp3:okhttp:4.12.0")

compileOnly("io.github.libxposed:api:101.0.1")

kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}

dependencies {
val composeBom = platform("androidx.compose:compose-bom:2025.02.00")
implementation(composeBom)

implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.activity:activity-compose:1.9.3")
// Prefer lighter lifecycle API used via ProcessLifecycle / Activity — compose helper is tiny
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")

implementation("androidx.compose.ui:ui")
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.runtime:runtime")
implementation("androidx.compose.material3:material3")
// IMPORTANT: do NOT add material-icons-extended (tens of MB of icon vectors in DEX)

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")

compileOnly("io.github.libxposed:api:101.0.1")
compileOnly("de.robv.android.xposed:api:82")

testImplementation("junit:junit:4.13.2")
}

debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-tooling-preview")
testImplementation("junit:junit:4.13.2")
}
62 changes: 38 additions & 24 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.

# Keep Xposed API
-keep class de.robv.android.xposed.** { *; }

# Keep our module class
-keep class io.github.recloudstudio.sleepyxposed.** { *; }

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# ---- SleepyXposed R8 rules ----
# Do NOT keep the whole app package; that prevents shrinking Compose / Material.

# libxposed / legacy Xposed entry (compileOnly — names kept for runtime)
-keep class io.github.recloudstudio.sleepyxposed.ModuleMain { *; }
-keep class * extends io.github.libxposed.api.XposedModule { *; }

Comment on lines +5 to +7

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.

# Components declared in the manifest
-keep class io.github.recloudstudio.sleepyxposed.MainActivity { *; }
-keep class io.github.recloudstudio.sleepyxposed.ConfigContentProvider { *; }
-keep class io.github.recloudstudio.sleepyxposed.MediaListenerService { *; }

# Optional hook target for LSPosed (must keep signature)
-keep class io.github.recloudstudio.sleepyxposed.XposedProbe {
public static boolean isModuleActive();
}

# Config model used across processes / JSON
-keepclassmembers class io.github.recloudstudio.sleepyxposed.SleepyConfig { *; }
-keepclassmembers class io.github.recloudstudio.sleepyxposed.MediaMethod { *; }

# Hook / system-server logic must not be stripped (reflection + Xposed)
-keep class io.github.recloudstudio.sleepyxposed.ConfigManager { *; }
-keep class io.github.recloudstudio.sleepyxposed.ForegroundAppMonitor { *; }
-keep class io.github.recloudstudio.sleepyxposed.MediaStatusMonitor { *; }
-keep class io.github.recloudstudio.sleepyxposed.SleepyApiClient { *; }
-keep class io.github.recloudstudio.sleepyxposed.RomDetector { *; }

# Reflection in ConfigManager (XSharedPreferences)
-dontwarn de.robv.android.xposed.**
-dontwarn io.github.libxposed.**

# Compose / Kotlin
-dontwarn org.jetbrains.annotations.**

# Keep line numbers for crash logs (small cost)
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile
26 changes: 16 additions & 10 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,38 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<!-- Package-visibility declarations (targetSdk 30+) so RomDetector.hasPackage() can see
these ROM-identity packages via getPackageInfo. -->
<queries>
<package android:name="com.miui.securitycenter" />
<package android:name="com.coloros.safecenter" />
<package android:name="com.vivo.abe" />
<package android:name="com.huawei.systemmanager" />
</queries>

<application
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:allowBackup="true"
android:theme="@style/Theme.AppCompat.Light.DarkActionBar">
android:hardwareAccelerated="true"
android:theme="@style/Theme.SleepyXposed">

<uses-library
android:name="de.robv.android.xposed"
android:required="false" />

<!-- Main Activity for Configuration -->
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.AppCompat.Light.DarkActionBar">
android:theme="@style/Theme.SleepyXposed"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<!-- Notification listener used for the media-status "Notification Listener" method -->
<service
android:name=".MediaListenerService"
android:label="@string/media_listener_label"
Expand All @@ -37,11 +46,9 @@
</intent-filter>
</service>

<!--
Exposes config to system_server. Private SharedPreferences are SELinux-blocked from
system_server under modern ROMs / libxposed API 101; the provider is queried with the
module app identity so prefs load correctly. Only system UID may read (enforced in code).
-->
<!-- Exported so system_server (via the Xposed hook) can read config and record
heartbeats across processes. Access is enforced in-code: query() requires the
system UID or our own process, and heartbeat calls require system_server only. -->
<provider
android:name=".ConfigContentProvider"
android:authorities="io.github.recloudstudio.sleepyxposed.config"
Expand All @@ -50,7 +57,6 @@
android:directBootAware="true"
android:grantUriPermissions="false" />

<!-- Xposed Module Metadata -->
<meta-data
android:name="xposedmodule"
android:value="true" />
Expand Down
Loading
Loading