diff --git a/app/src/main/java/btm/m/os4/systemuihook/HyperSystemUiModule.kt b/app/src/main/java/btm/m/os4/systemuihook/HyperSystemUiModule.kt index a18d5d5..7f9d449 100644 --- a/app/src/main/java/btm/m/os4/systemuihook/HyperSystemUiModule.kt +++ b/app/src/main/java/btm/m/os4/systemuihook/HyperSystemUiModule.kt @@ -1589,11 +1589,23 @@ class HyperSystemUiModule : XposedModule() { } } + private fun isStandaloneHyperMusicCoverInstalled(context: Context): Boolean = runCatching { + context.packageManager.getPackageInfo("com.github.zyl6932.HyperMusicCover", 0) + true + }.getOrDefault(false) + private fun sendHyperMusicCoverMediaCardCommand( context: Context, hideArtwork: Boolean, centerText: Boolean, ) { + if (isStandaloneHyperMusicCoverInstalled(context)) { + btm.m.os4.systemuihook.HookModuleLogger.log( + this, Log.INFO, TAG, + "Standalone HyperMusicCover detected; skipping HyperChanger media-card command", + ) + return + } runCatching { context.sendBroadcast( Intent(HYPER_MUSIC_COVER_ACTION) @@ -1608,6 +1620,13 @@ class HyperSystemUiModule : XposedModule() { } private fun sendHyperMusicCoverCommand(context: Context, op: String, on: Boolean) { + if (isStandaloneHyperMusicCoverInstalled(context)) { + btm.m.os4.systemuihook.HookModuleLogger.log( + this, Log.INFO, TAG, + "Standalone HyperMusicCover detected; skipping HyperChanger command op=$op on=$on", + ) + return + } runCatching { context.sendBroadcast( Intent(HYPER_MUSIC_COVER_ACTION) diff --git a/app/src/main/java/btm/m/os4/systemuihook/MainActivity.kt b/app/src/main/java/btm/m/os4/systemuihook/MainActivity.kt index fa633cf..e421df8 100644 --- a/app/src/main/java/btm/m/os4/systemuihook/MainActivity.kt +++ b/app/src/main/java/btm/m/os4/systemuihook/MainActivity.kt @@ -516,6 +516,13 @@ private fun suggestedScopesForHookChange(before: HookSettings, after: HookSettin return scopes } +private const val STANDALONE_HMC_PACKAGE = "com.github.zyl6932.HyperMusicCover" + +private fun isStandaloneHyperMusicCoverInstalled(context: Context): Boolean = runCatching { + context.packageManager.getPackageInfo(STANDALONE_HMC_PACKAGE, 0) + true +}.getOrDefault(false) + @Composable private fun Root( hooks: HookSettingsStore, @@ -8721,6 +8728,14 @@ private fun Lock( ), ) { p, scroll -> val context = LocalContext.current + val standaloneHmcInstalled = remember(context) { isStandaloneHyperMusicCoverInstalled(context) } + LaunchedEffect(standaloneHmcInstalled, s.lockscreenMusicLockscreenEnabled) { + if (standaloneHmcInstalled && s.lockscreenMusicLockscreenEnabled) { + // HMC and HC both hook SystemUI. Disable HC's stored toggle when the standalone + // module is installed so it cannot silently reactivate after a later restart. + update { it.copy(lockscreenMusicLockscreenEnabled = false) } + } + } val shortcutGestureStore = remember(context) { LockscreenShortcutBackgroundSettingsStore(context) } val shortcutGestureService by HookApplication.service.collectAsStateWithLifecycle() var shortcutGestureMode by remember { mutableIntStateOf(shortcutGestureStore.settings.gestureMode) } @@ -9088,9 +9103,17 @@ OverlayDropdownPreference( "\u9700\u8981\u540c\u65f6\u91cd\u542f\u201c\u7cfb\u7edf\u754c\u9762\u201d\u548c\u201c\u58c1\u7eb8\u201d\u5e94\u7528", "\u9700\u8981\u540c\u65f6\u91cd\u542f\u201c\u7cfb\u7edf\u754c\u9762\u201d\u548c\u201c\u58c1\u7eb8\u201d\u5e94\u7528", ), - checked = s.lockscreenMusicLockscreenEnabled, + checked = s.lockscreenMusicLockscreenEnabled && !isStandaloneHyperMusicCoverInstalled(context), onCheckedChange = { value -> - update { it.copy(lockscreenMusicLockscreenEnabled = value) } + if (value && isStandaloneHyperMusicCoverInstalled(context)) { + Toast.makeText( + context, + "检测到独立版 HyperMusicCover 已安装。为避免两个模块同时 Hook SystemUI,请先在 LSPosed 中停用 HMC,再开启 HC 音乐锁屏。", + Toast.LENGTH_LONG, + ).show() + } else { + update { it.copy(lockscreenMusicLockscreenEnabled = value) } + } }, ) AnimatedVisibility( diff --git a/app/src/main/java/btm/m/os4/systemuihook/ModuleSafetyPage.kt b/app/src/main/java/btm/m/os4/systemuihook/ModuleSafetyPage.kt index 552a49a..d72ccda 100644 --- a/app/src/main/java/btm/m/os4/systemuihook/ModuleSafetyPage.kt +++ b/app/src/main/java/btm/m/os4/systemuihook/ModuleSafetyPage.kt @@ -36,6 +36,7 @@ internal fun ModuleSafetyPage(openLogs: () -> Unit, back: () -> Unit) { val exporter = rememberModuleLogExporter() val nativeFailure = tr("safetyNativeFailed", "设置已保存,但桌面原生配置同步失败。请授予超级用户权限后重试,确认成功后再重启设备。") val syncFailure = tr("safetySyncFailed", "设置已保存到本地,但框架同步失败。请恢复 LSPosed 连接后重试。") + fun update(change: (ModuleSafetySettings) -> ModuleSafetySettings) { if (busy) return val result = runCatching { store.update(service, change) } @@ -52,76 +53,137 @@ internal fun ModuleSafetyPage(openLogs: () -> Unit, back: () -> Unit) { busy = false } } + LaunchedEffect(service) { service?.let { if (runCatching { store.syncRemote(it) }.isFailure) status = syncFailure settings = store.settings } } + AppPage(tr("moduleSafety", "模块安全"), back, header = { - SoftGlassHeaderTabs(listOf(tr("safetyOptions", "选项"), tr("safetyInformation", "信息")), selectedTab) { selectedTab = it } + SoftGlassHeaderTabs(listOf(tr("safetyOptions", "选项"), tr("safetyInformation", "信息")), selectedTab) { + selectedTab = it + } }) { padding, scroll -> AppList(padding, scroll) { if (selectedTab == 0) { item { Group(tr("safetySwitches", "模块开关")) { - SwitchPreference(title = tr("safetyMaster", "模块总开关"), + SwitchPreference( + title = tr("safetyMaster", "模块总开关"), summary = tr("safetyMasterHint", "关闭以后禁用所有 Hook,至少软重启设备以生效"), - checked = settings.enabled, enabled = !busy, - onCheckedChange = { value -> update { it.copy(enabled = value) } }) - SwitchPreference(title = tr("safetySystemUi", "系统界面安全模式"), + checked = settings.enabled, + enabled = !busy, + onCheckedChange = { value -> update { it.copy(enabled = value) } } + ) + SwitchPreference( + title = tr("safetySystemUi", "系统界面安全模式"), summary = tr("safetySystemUiHint", "禁用对系统界面的 Hook,重启后生效"), - checked = settings.systemUiSafe, enabled = !busy, - onCheckedChange = { value -> update { it.copy(systemUiSafe = value) } }) - SwitchPreference(title = tr("safetyPartial", "部分安全模式"), + checked = settings.systemUiSafe, + enabled = !busy, + onCheckedChange = { value -> update { it.copy(systemUiSafe = value) } } + ) + SwitchPreference( + title = tr("safetyPartial", "部分安全模式"), summary = tr("safetyPartialHint", "停用某个部分的模块功能,重启后生效"), - checked = settings.partialSafe, enabled = !busy, - onCheckedChange = { value -> if (value) sectionsDialog = true else update { it.copy(partialSafe = false) } }) - if (settings.partialSafe) ArrowPreference(tr("safetyChooseSections", "选择停用部分"), - summary = settings.sections.joinToString("、") { safetySectionTitle(it) }, - enabled = !busy, onClick = { sectionsDialog = true }) + checked = settings.partialSafe, + enabled = !busy, + onCheckedChange = { value -> + if (value) sectionsDialog = true else update { it.copy(partialSafe = false) } + } + ) + if (settings.partialSafe) { + ArrowPreference( + tr("safetyChooseSections", "选择停用部分"), + summary = settings.sections.joinToString("、") { safetySectionTitle(it) }, + enabled = !busy, + onClick = { sectionsDialog = true } + ) + } ModuleHotReloadPreferences(enabled = !busy) } } item { Group(tr("safetyLogging", "日志输出")) { - SwitchPreference(title = tr("safetyLogging", "日志输出"), checked = settings.logEnabled, enabled = !busy, - onCheckedChange = { value -> update { it.copy(logEnabled = value) } }) - OverlayDropdownPreference(tr("safetyLogOutputLevel", "日志输出等级"), + SwitchPreference( + title = tr("safetyLogging", "日志输出"), + checked = settings.logEnabled, + enabled = !busy, + onCheckedChange = { value -> update { it.copy(logEnabled = value) } } + ) + OverlayDropdownPreference( + tr("safetyLogOutputLevel", "日志输出等级"), listOf(tr("safetyLogNormal", "一般"), tr("safetyLogDetailed", "详细")), - if (settings.detailedLogs) 1 else 0, enabled = !busy && settings.logEnabled, - onSelectedIndexChange = { value -> update { it.copy(detailedLogs = value == 1) } }) + if (settings.detailedLogs) 1 else 0, + enabled = !busy && settings.logEnabled, + onSelectedIndexChange = { value -> update { it.copy(detailedLogs = value == 1) } } + ) ArrowPreference(tr("safetyViewLogs", "查看日志"), onClick = openLogs) - ArrowPreference(tr("safetyExportLogs", "导出日志"), enabled = !exporter.busy, onClick = { exporter.export(false) }) + ArrowPreference( + tr("safetyExportLogs", "导出日志"), + enabled = !exporter.busy, + onClick = { exporter.export(false) } + ) val days = listOf(3, 7, 14, 30, 120) - OverlayDropdownPreference(tr("safetyRetention", "定时清理日志"), days.map { tr("safetyDays", "%d 天").format(it) }, - days.indexOf(settings.retentionDays), enabled = !busy, - onSelectedIndexChange = { index -> update { it.copy(retentionDays = days[index]) } }) - ArrowPreference(tr("safetyClearAll", "立即清空日志"), enabled = !busy, onClick = { clearDialog = true }) + OverlayDropdownPreference( + tr("safetyRetention", "定时清理日志"), + days.map { tr("safetyDays", "%d 天").format(it) }, + days.indexOf(settings.retentionDays), + enabled = !busy, + onSelectedIndexChange = { index -> update { it.copy(retentionDays = days[index]) } } + ) + ArrowPreference( + tr("safetyClearAll", "立即清空日志"), + enabled = !busy, + onClick = { clearDialog = true } + ) } } status?.let { value -> item { SafetyNote(value) } } - if (status != null) item { - Card(Modifier.fillMaxWidth(), cornerRadius = 22.5.dp) { - ArrowPreference(tr("safetyRetrySync", "重试同步"), enabled = !busy, onClick = { update { it } }) + if (status != null) { + item { + Card(Modifier.fillMaxWidth(), cornerRadius = 22.5.dp) { + ArrowPreference( + tr("safetyRetrySync", "重试同步"), + enabled = !busy, + onClick = { update { it } } + ) + } } } exporter.status?.let { value -> item { SafetyNote(value) } } - if (service == null) item { SafetyNote(tr("safetyOffline", "LSPosed 未连接,设置将保存在本地并在连接后同步。")) } - } else item { ModuleSafetyInformation() } + if (service == null) { + item { SafetyNote(tr("safetyOffline", "LSPosed 未连接,设置将保存在本地并在连接后同步。")) } + } + } else { + item { ModuleSafetyInformation() } + } } } - if (sectionsDialog) SafetySectionsDialog(settings.sections, { sectionsDialog = false }) { selected -> - sectionsDialog = false - update { it.copy(partialSafe = true, sections = selected) } + + if (sectionsDialog) { + SafetySectionsDialog(settings.sections, { sectionsDialog = false }) { selected -> + sectionsDialog = false + update { it.copy(partialSafe = true, sections = selected) } + } } - if (clearDialog) ModuleLogClearDialog({ clearDialog = false }) { - clearDialog = false - busy = true - scope.launch { - val result = withContext(Dispatchers.IO) { runCatching { ModuleLogRepository(context).clear() } } - status = if (result.isSuccess) tr("safetyLogsCleared", "日志已清空") else tr("safetyLogsClearFailed", "无法清空日志,请重试") - busy = false + + if (clearDialog) { + ModuleLogClearDialog({ clearDialog = false }) { + clearDialog = false + busy = true + scope.launch { + val result = withContext(Dispatchers.IO) { + runCatching { ModuleLogRepository(context).clear() } + } + status = if (result.isSuccess) { + tr("safetyLogsCleared", "日志已清空") + } else { + tr("safetyLogsClearFailed", "无法清空日志,请重试") + } + busy = false + } } } } @@ -129,32 +191,54 @@ internal fun ModuleSafetyPage(openLogs: () -> Unit, back: () -> Unit) { internal fun safetySectionTitle(section: SafetySection) = tr("safetySection${section.name}", section.title) @Composable -private fun SafetySectionsDialog(initial: Set, dismiss: () -> Unit, save: (Set) -> Unit) { +private fun SafetySectionsDialog( + initial: Set, + dismiss: () -> Unit, + save: (Set) -> Unit +) { var selected by remember { mutableStateOf(initial) } WindowDialog(show = true, title = tr("safetyPartial", "部分安全模式"), onDismissRequest = dismiss) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Column(Modifier.heightIn(max = 380.dp).verticalScroll(rememberScrollState())) { SafetySection.entries.forEach { section -> - val toggle = { selected = if (section in selected) selected - section else selected + section } - Row(Modifier.fillMaxWidth().clickable(onClick = toggle).padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically) { - Checkbox(state = if (section in selected) ToggleableState.On else ToggleableState.Off, onClick = toggle) + val toggle = { + selected = if (section in selected) selected - section else selected + section + } + Row( + Modifier.fillMaxWidth().clickable(onClick = toggle).padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + state = if (section in selected) ToggleableState.On else ToggleableState.Off, + onClick = toggle + ) Text(safetySectionTitle(section), Modifier.padding(start = 12.dp)) } } } Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { - GlassDialogButton({ selected = SafetySection.entries.toSet() }, Modifier.weight(1f)) { Text(tr("全选", "全选")) } - GlassDialogButton({ save(selected) }, Modifier.weight(1f), colors = ButtonDefaults.buttonColorsPrimary()) { Text(tr("保存", "保存")) } + GlassDialogButton({ selected = SafetySection.entries.toSet() }, Modifier.weight(1f)) { + Text(tr("全选", "全选")) + } + GlassDialogButton({ save(selected) }, Modifier.weight(1f), colors = ButtonDefaults.buttonColorsPrimary()) { + Text(tr("保存", "保存")) + } + } + GlassDialogButton(dismiss, Modifier.fillMaxWidth()) { + Text(tr("取消", "取消")) } - GlassDialogButton(dismiss, Modifier.fillMaxWidth()) { Text(tr("取消", "取消")) } } } } @Composable internal fun SafetyNote(text: String) { - Text(text, Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), - style = MiuixTheme.textStyles.body2, color = MiuixTheme.colorScheme.onSurfaceVariantSummary) + Text( + text, + Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), + style = MiuixTheme.textStyles.body2, + color = MiuixTheme.colorScheme.onSurfaceVariantSummary + ) } @Composable @@ -163,33 +247,85 @@ private fun ModuleSafetyInformation() { val service by HookApplication.service.collectAsStateWithLifecycle() var root by remember { mutableStateOf(null) } var zygisk by remember { mutableStateOf(null) } + var lsposedVersion by remember { mutableStateOf(null) } + LaunchedEffect(Unit) { - withContext(Dispatchers.IO) { + val detection = withContext(Dispatchers.IO) { val probe = runCatching { ModuleRoot.run("id -u", 1024) }.getOrNull() - root = probe?.let { it.code == 0 && it.output.trim() == "0" } ?: false - if (root == true) zygisk = runCatching { - ModuleRoot.run(""" - for directory in /data/adb/modules/*; do - [ -f "${'$'}directory/module.prop" ] || continue - [ ! -f "${'$'}directory/disable" ] || continue - case "${'$'}directory" in *zygisk*|*rezygisk*) - name=''; version='' - while IFS='=' read -r key value; do - case "${'$'}key" in name) name="${'$'}value";; version) version="${'$'}value";; esac - done < "${'$'}directory/module.prop" - printf '%s %s\n' "${'$'}name" "${'$'}version";; - esac - done - """.trimIndent(), 8192).output.trim().takeIf { it.isNotBlank() } - }.getOrNull() + val hasRoot = probe?.let { it.code == 0 && it.output.trim() == "0" } ?: false + var zygiskInfo: String? = null + var lsposedInfo: String? = null + + if (hasRoot) { + zygiskInfo = runCatching { + ModuleRoot.run( + """ + for directory in /data/adb/modules/*; do + [ -f "${'$'}directory/module.prop" ] || continue + [ ! -f "${'$'}directory/disable" ] || continue + case "${'$'}directory" in *zygisk*|*rezygisk*) + name=''; version='' + while IFS='=' read -r key value; do + case "${'$'}key" in name) name="${'$'}value";; version) version="${'$'}value";; esac + done < "${'$'}directory/module.prop" + printf '%s %s\n' "${'$'}name" "${'$'}version";; + esac + done + """.trimIndent(), + 8192 + ).output.trim().takeIf { it.isNotBlank() } + }.getOrNull() + + lsposedInfo = runCatching { + ModuleRoot.run( + """ + for directory in /data/adb/modules/*; do + [ -f "${'$'}directory/module.prop" ] || continue + [ -f "${'$'}directory/disable" ] && continue + id='' + name='' + version='' + while IFS='=' read -r key value; do + case "${'$'}key" in + id) id="${'$'}value" ;; + name) name="${'$'}value" ;; + version) version="${'$'}value" ;; + esac + done < "${'$'}directory/module.prop" + case "${'$'}id:${'$'}name" in + *lsposed*|*LSPosed*|*LSPOSED*) + printf '%s\n' "${'$'}version" + break + ;; + esac + done + """.trimIndent(), + 8192 + ).output.trim().takeIf { it.isNotBlank() } + }.getOrNull() + } + + Triple(hasRoot, zygiskInfo, lsposedInfo) } + root = detection.first + zygisk = detection.second + lsposedVersion = detection.third } + val unknown = tr("未检测到", "未检测到") Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Group(tr("safetyFrameworkRoot", "框架与超级用户")) { Column(Modifier.fillMaxWidth().clickable { openLsposedManager(context) }) { - DeviceInfoItem(if (service != null) tr("safetyLsposedConnected", "LSPosed 已连接") else tr("safetyLsposedDisconnected", "LSPosed 未连接"), - tr("safetyApi", "API %s").format(runCatching { service?.apiVersion?.toString() }.getOrNull() ?: unknown)) + DeviceInfoItem( + if (service != null) { + tr("safetyLsposedConnected", "LSPosed 已连接") + } else { + tr("safetyLsposedDisconnected", "LSPosed 未连接") + }, + tr("safetyApi", "API %s").format( + runCatching { service?.apiVersion?.toString() }.getOrNull() ?: unknown + ) + "\n" + (lsposedVersion ?: unknown) + ) } DeviceInfoItem(when (root) { true -> tr("safetyRootGranted", "超级用户 已授权") @@ -199,6 +335,7 @@ private fun ModuleSafetyInformation() { } Group(tr("safetyDeviceInformation", "设备信息")) { DeviceInfoItem(OsCompatibility.systemProperty("ro.product.marketname").ifBlank { Build.MODEL }, tr("safetyDeviceModel", "设备型号")) + DeviceInfoItem(OsCompatibility.systemProperty("ro.product.device").ifBlank { Build.DEVICE },tr("设备代号", "设备代号")) DeviceInfoItem(tr("safetyAndroidValue", "%s (SDK %d)").format(Build.VERSION.RELEASE, Build.VERSION.SDK_INT), tr("safetyAndroidVersion", "Android 版本")) DeviceInfoItem(OsCompatibility.systemProperty("ro.mi.os.version.incremental").removePrefix("OS").ifBlank { unknown }, tr("safetyHyperOsBuildVersion", "HyperOS 构建版本号")) DeviceInfoItem(Build.SUPPORTED_ABIS.joinToString(", "), tr("safetyArchitecture", "系统架构")) diff --git a/app/src/main/java/btm/m/os4/systemuihook/hypermusiccover/Main.java b/app/src/main/java/btm/m/os4/systemuihook/hypermusiccover/Main.java index 975eeef..5f98d85 100644 --- a/app/src/main/java/btm/m/os4/systemuihook/hypermusiccover/Main.java +++ b/app/src/main/java/btm/m/os4/systemuihook/hypermusiccover/Main.java @@ -1761,6 +1761,13 @@ public void onReceive(Context c, Intent i) { } else if ("pushart".equals(op)) { boolean on = i.getBooleanExtra("on", true); if (i.hasExtra("bias")) sBias = clamp01(i.getFloatExtra("bias", sBias)); + if (!on) { + // An explicit OFF from the app must also stop automatic card-following. + // Otherwise the next metadata/card update reaches onMediaUpdate() while + // sAuto is still true and immediately enters cover mode again, making the + // user's OFF switch appear ineffective (especially with HMC also active). + setAuto(false); + } sTrackKey = on ? trackKey(pickController(c)) : ""; setCoverEnabled(on, anim, false); } else if ("wphello".equals(op)) {