From 5fe98409dc52506f7a5cc1bb8f94d6f5c849b7f0 Mon Sep 17 00:00:00 2001 From: Wang Han <416810799@qq.com> Date: Sun, 30 Aug 2026 21:09:17 +0800 Subject: [PATCH 01/34] refactor(kernel, ksud, manager): Switch webview zygote umount to allowlist (https://github.com/tiann/KernelSU/pull/3666) [cherry-picked upstream commit https://github.com/tiann/KernelSU/commit/3497a56261564ee7fca494e3bfa0fac80118a34d] Co-Authored-By: AlexLiuDev233 --- kernel/feature/kernel_umount.c | 29 +----- kernel/feature/kernel_umount.h | 1 - kernel/hook/setuid_hook.c | 15 +-- kernel/policy/allowlist.c | 3 - manager/app/src/main/cpp/jni.c | 8 -- manager/app/src/main/cpp/ksu.c | 16 ---- manager/app/src/main/cpp/ksu.h | 5 - .../java/com/resukisu/resukisu/Natives.kt | 3 - .../resukisu/data/kernel/KernelRepository.kt | 5 - .../data/packageinfo/SuperUserRepository.kt | 49 +++++++++- .../settings/SettingsPlatformRepository.kt | 3 - .../com/resukisu/resukisu/di/AppModules.kt | 2 - .../domain/model/InstalledAppGroup.kt | 29 +++++- .../resukisu/domain/model/KernelState.kt | 1 - .../resukisu/domain/model/SettingsPlatform.kt | 1 - .../resukisu/domain/usecase/KernelUseCases.kt | 4 - .../resukisu/resukisu/ui/screen/AppProfile.kt | 95 +++++++++++-------- .../resukisu/ui/screen/main/SettingsPage.kt | 22 ----- .../resukisu/ui/screen/main/SuperUserPage.kt | 8 +- .../ui/viewmodel/AppProfileViewModel.kt | 38 +++++--- .../ui/viewmodel/SettingsViewModel.kt | 17 ---- .../ui/viewmodel/SuperUserViewModel.kt | 4 +- .../app/src/main/res/values-fr/strings.xml | 6 +- .../app/src/main/res/values-hu/strings.xml | 2 - .../src/main/res/values-pt-rBR/strings.xml | 2 - .../app/src/main/res/values-ru/strings.xml | 3 - .../app/src/main/res/values-tr/strings.xml | 2 - .../app/src/main/res/values-uk/strings.xml | 2 - .../src/main/res/values-zh-rCN/strings.xml | 2 - .../src/main/res/values-zh-rHK/strings.xml | 2 - manager/app/src/main/res/values/strings.xml | 2 - uapi/feature.h | 1 - userspace/ksud/src/android/cli.rs | 4 +- userspace/ksud/src/android/feature.rs | 9 -- 34 files changed, 165 insertions(+), 230 deletions(-) diff --git a/kernel/feature/kernel_umount.c b/kernel/feature/kernel_umount.c index c7bcfd767..78ed9522d 100644 --- a/kernel/feature/kernel_umount.c +++ b/kernel/feature/kernel_umount.c @@ -2,7 +2,6 @@ #include #include #include -#include #include #include #include @@ -31,7 +30,6 @@ #include "feature/sucompat.h" static bool ksu_kernel_umount_enabled = true; -bool ksu_webview_zygote_umount_enabled = false; static int kernel_umount_feature_get(u64 *value) { @@ -54,27 +52,6 @@ static const struct ksu_feature_handler kernel_umount_handler = { .set_handler = kernel_umount_feature_set, }; -static int webview_zygote_umount_feature_get(u64 *value) -{ - *value = ksu_webview_zygote_umount_enabled ? 1 : 0; - return 0; -} - -static int webview_zygote_umount_feature_set(u64 value) -{ - bool enable = value != 0; - ksu_webview_zygote_umount_enabled = enable; - pr_info("webview_zygote_umount: set to %d\n", enable); - return 0; -} - -static const struct ksu_feature_handler webview_zygote_umount_handler = { - .feature_id = KSU_FEATURE_WEBVIEW_ZYGOTE_UMOUNT, - .name = "webview_zygote_umount", - .get_handler = webview_zygote_umount_feature_get, - .set_handler = webview_zygote_umount_feature_set, -}; - #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 9, 0) || defined(KSU_HAS_PATH_UMOUNT) extern int path_umount(struct path *path, int flags); static void ksu_umount_mnt(const char *mnt, struct path *path, int flags) @@ -152,7 +129,7 @@ int ksu_handle_umount(uid_t old_uid, uid_t new_uid) // 1. Normal app: zygote -> appuid // 2. Isolated process forked from zygote: zygote -> isolated_process // 3. App zygote forked from zygote: zygote -> appuid - // 4. Webview zygote forked from zygote: zygote -> webview_zygote (controlled by feature policy) + // 4. Webview zygote forked from zygote: zygote -> webview_zygote // 5. Isolated process forked from app zygote: appuid -> isolated_process (already handled by 3) // 6. Isolated process forked from webview zygote (already handled by 4) if (!is_appuid(new_uid) && new_uid != WEBVIEW_ZYGOTE_UID && !is_isolated_process(new_uid)) { @@ -205,13 +182,9 @@ void __init ksu_kernel_umount_init(void) if (ksu_register_feature_handler(&kernel_umount_handler)) { pr_err("Failed to register kernel_umount feature handler\n"); } - if (ksu_register_feature_handler(&webview_zygote_umount_handler)) { - pr_err("Failed to register webview_zygote_umount feature handler\n"); - } } void __exit ksu_kernel_umount_exit(void) { - ksu_unregister_feature_handler(KSU_FEATURE_WEBVIEW_ZYGOTE_UMOUNT); ksu_unregister_feature_handler(KSU_FEATURE_KERNEL_UMOUNT); } diff --git a/kernel/feature/kernel_umount.h b/kernel/feature/kernel_umount.h index 355f8ca5c..d76044a04 100644 --- a/kernel/feature/kernel_umount.h +++ b/kernel/feature/kernel_umount.h @@ -7,7 +7,6 @@ void ksu_kernel_umount_init(void); void ksu_kernel_umount_exit(void); -extern bool ksu_webview_zygote_umount_enabled; // Handler function to be called from setresuid hook int ksu_handle_umount(uid_t old_uid, uid_t new_uid); diff --git a/kernel/hook/setuid_hook.c b/kernel/hook/setuid_hook.c index 070b2fbcf..7eb2de3f5 100644 --- a/kernel/hook/setuid_hook.c +++ b/kernel/hook/setuid_hook.c @@ -93,21 +93,8 @@ static int handle_zygote_next_setresuid(uid_t new_uid) goto do_susfs_work; } - // manager NEVER use zygote next! - - // we should not umount for webview zygote - if (unlikely(new_uid == WEBVIEW_ZYGOTE_UID)) { - if (ksu_webview_zygote_umount_enabled) { - susfs_set_current_proc_no_su(); - susfs_set_current_proc_umounted(); - susfs_set_current_proc_umounted_for_zygote_next(); - goto do_susfs_work; - } - susfs_set_current_proc_no_su(); - return 0; - } - // Check if spawned process is normal user app and needs to be umounted + // Now app_profile for webview_zygote is available in KernelSU manager if (likely(is_appuid(new_uid) && ksu_uid_should_umount(new_uid))) { susfs_set_current_proc_no_su(); susfs_set_current_proc_umounted(); diff --git a/kernel/policy/allowlist.c b/kernel/policy/allowlist.c index c3284d8af..55377ab60 100644 --- a/kernel/policy/allowlist.c +++ b/kernel/policy/allowlist.c @@ -316,9 +316,6 @@ bool ksu_uid_should_umount(uid_t uid) // we should not umount on manager! return false; } - if (unlikely(uid == WEBVIEW_ZYGOTE_UID)) { - return ksu_webview_zygote_umount_enabled; - } #ifdef CONFIG_KSU_DISABLE_POLICY return !__ksu_is_allow_uid(uid); #else diff --git a/manager/app/src/main/cpp/jni.c b/manager/app/src/main/cpp/jni.c index 1818d3fa2..d7f710c9f 100644 --- a/manager/app/src/main/cpp/jni.c +++ b/manager/app/src/main/cpp/jni.c @@ -514,14 +514,6 @@ NativeBridge(setKernelUmountEnabled, jboolean, jboolean enabled) { return set_kernel_umount_enabled(enabled); } -NativeBridgeNP(isWebViewZygoteUmountEnabled, jboolean) { - return is_webview_zygote_umount_enabled(); -} - -NativeBridge(setWebViewZygoteUmountEnabled, jboolean, jboolean enabled) { - return set_webview_zygote_umount_enabled(enabled); -} - NativeBridgeNP(isSelinuxHideEnabled, jboolean) { return is_selinux_hide_enabled(); } diff --git a/manager/app/src/main/cpp/ksu.c b/manager/app/src/main/cpp/ksu.c index 2a664932a..4dbe60679 100644 --- a/manager/app/src/main/cpp/ksu.c +++ b/manager/app/src/main/cpp/ksu.c @@ -234,22 +234,6 @@ bool is_kernel_umount_enabled() { return value != 0; } -bool set_webview_zygote_umount_enabled(bool enabled) { - return set_feature(KSU_FEATURE_WEBVIEW_ZYGOTE_UMOUNT, enabled ? 1 : 0); -} - -bool is_webview_zygote_umount_enabled() { - uint64_t value = 0; - bool supported = false; - if (!get_feature(KSU_FEATURE_WEBVIEW_ZYGOTE_UMOUNT, &value, &supported)) { - return false; - } - if (!supported) { - return false; - } - return value != 0; -} - int set_selinux_hide_enabled(bool enabled) { if (!set_feature(KSU_FEATURE_SELINUX_HIDE, enabled ? 1 : 0)) { return -errno; diff --git a/manager/app/src/main/cpp/ksu.h b/manager/app/src/main/cpp/ksu.h index 38f45305f..c93ff2648 100644 --- a/manager/app/src/main/cpp/ksu.h +++ b/manager/app/src/main/cpp/ksu.h @@ -59,11 +59,6 @@ bool set_sulog_enabled(bool enabled); bool set_kernel_umount_enabled(bool enabled); bool is_kernel_umount_enabled(); -// WebView zygote umount -bool set_webview_zygote_umount_enabled(bool enabled); - -bool is_webview_zygote_umount_enabled(); - // SELinux hide int set_selinux_hide_enabled(bool enabled); diff --git a/manager/app/src/main/java/com/resukisu/resukisu/Natives.kt b/manager/app/src/main/java/com/resukisu/resukisu/Natives.kt index 8581f0098..ed264316d 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/Natives.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/Natives.kt @@ -144,9 +144,6 @@ object Natives { external fun isKernelUmountEnabled(): Boolean external fun setKernelUmountEnabled(enabled: Boolean): Boolean - external fun isWebViewZygoteUmountEnabled(): Boolean - external fun setWebViewZygoteUmountEnabled(enabled: Boolean): Boolean - /** * SELinux hide can be disabled temporarily. * 0: disabled diff --git a/manager/app/src/main/java/com/resukisu/resukisu/data/kernel/KernelRepository.kt b/manager/app/src/main/java/com/resukisu/resukisu/data/kernel/KernelRepository.kt index f4b6181f3..db811e5e3 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/data/kernel/KernelRepository.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/data/kernel/KernelRepository.kt @@ -67,7 +67,6 @@ class KernelRepository( suspend fun getFeatureSettings(): KernelFeatureSettings = withContext(Dispatchers.IO) { KernelFeatureSettings( suEnabled = runCatching { Natives.isSuEnabled() }.getOrDefault(false), - webViewZygoteUmountEnabled = runCatching { Natives.isWebViewZygoteUmountEnabled() }.getOrDefault(false), kernelUmountEnabled = runCatching { Natives.isKernelUmountEnabled() }.getOrDefault(false), suLogEnabled = runCatching { Natives.isSuLogEnabled() }.getOrDefault(false), selinuxHideEnabled = runCatching { Natives.isSelinuxHideEnabled() }.getOrDefault(false), @@ -89,10 +88,6 @@ class KernelRepository( Natives.setSuLogEnabled(enabled) } - suspend fun setWebviewZygoteUmountEnabled(enabled: Boolean): Boolean = saveFeature { - Natives.setWebViewZygoteUmountEnabled(enabled) - } - suspend fun setSelinuxHideEnabled(enabled: Boolean): Int = withContext(Dispatchers.IO) { Natives.setSelinuxHideEnabled(enabled).also { ksuCliRepository.execKsud("feature save", true) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/data/packageinfo/SuperUserRepository.kt b/manager/app/src/main/java/com/resukisu/resukisu/data/packageinfo/SuperUserRepository.kt index 7b79ff711..7d9ff71f5 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/data/packageinfo/SuperUserRepository.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/data/packageinfo/SuperUserRepository.kt @@ -13,6 +13,8 @@ import com.resukisu.resukisu.domain.model.AllowlistRestoreResult import com.resukisu.resukisu.domain.model.InstalledApp import com.resukisu.resukisu.domain.model.InstalledAppGroup import com.resukisu.resukisu.domain.model.SuperUserState +import com.resukisu.resukisu.domain.model.WEBVIEW_ZYGOTE_PROFILE_KEY +import com.resukisu.resukisu.domain.model.WEBVIEW_ZYGOTE_UID import com.topjohnwu.superuser.io.SuFile import com.topjohnwu.superuser.io.SuFileInputStream import kotlinx.coroutines.CancellationException @@ -43,7 +45,7 @@ class SuperUserRepository( ) { source, profiles -> source.copy( groups = source.groups.map { group -> - val snapshot = profiles[AppProfileKey(group.primaryPackageName, group.uid)] + val snapshot = profiles[AppProfileKey(group.profileKey, group.uid)] ?: return@map group group.copy( profile = snapshot.profile, @@ -64,9 +66,10 @@ class SuperUserRepository( val packages = cache.packages.value val groups = withContext(Dispatchers.IO) { val packageManager = application.packageManager - packages.mapNotNull { info -> + val apps = packages.mapNotNull { info -> val applicationInfo = info.applicationInfo ?: return@mapNotNull null if (info.packageName == application.packageName) return@mapNotNull null + if (applicationInfo.uid == WEBVIEW_ZYGOTE_UID) return@mapNotNull null InstalledApp( packageName = info.packageName, label = applicationInfo.loadLabel(packageManager).toString(), @@ -74,7 +77,8 @@ class SuperUserRepository( isSystem = applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0, firstInstallTime = info.firstInstallTime, ) - }.groupBy(InstalledApp::uid).map { (uid, uidApps) -> + } + val normalGroups = apps.groupBy(InstalledApp::uid).map { (uid, uidApps) -> val sorted = uidApps.sortedBy(InstalledApp::label) val primary = sorted.first() val profile = profileRepository.getProfileSnapshot(primary.packageName, uid) @@ -87,6 +91,29 @@ class SuperUserRepository( shouldUmount = profile.shouldUmount, ) } + // WebView Zygote is a single system UID, not a per-user package. + val webviewProfile = profileRepository.getProfileSnapshot( + WEBVIEW_ZYGOTE_PROFILE_KEY, + WEBVIEW_ZYGOTE_UID, + ) + val webviewGroup = InstalledAppGroup( + uid = WEBVIEW_ZYGOTE_UID, + primaryPackageName = WEBVIEW_ZYGOTE_PROFILE_KEY, + apps = listOf( + InstalledApp( + packageName = WEBVIEW_ZYGOTE_PROFILE_KEY, + label = "WebView Zygote", + uid = WEBVIEW_ZYGOTE_UID, + isSystem = true, + profileKey = WEBVIEW_ZYGOTE_PROFILE_KEY, + special = true, + ) + ), + profile = webviewProfile.profile, + userName = profileRepository.getUserName(WEBVIEW_ZYGOTE_UID), + shouldUmount = webviewProfile.shouldUmount, + ) + normalGroups + webviewGroup } mutableState.value = SuperUserState( groups = groups, @@ -143,6 +170,22 @@ class SuperUserRepository( suspend fun getAppGroup(uid: Int, primaryPackageName: String): InstalledAppGroup = withContext(Dispatchers.IO) { + if (uid == WEBVIEW_ZYGOTE_UID) { + return@withContext InstalledAppGroup( + uid = WEBVIEW_ZYGOTE_UID, + primaryPackageName = WEBVIEW_ZYGOTE_PROFILE_KEY, + apps = listOf( + InstalledApp( + packageName = WEBVIEW_ZYGOTE_PROFILE_KEY, + label = "WebView Zygote", + uid = WEBVIEW_ZYGOTE_UID, + isSystem = true, + profileKey = WEBVIEW_ZYGOTE_PROFILE_KEY, + special = true, + ) + ), + ) + } val packageManager = application.packageManager val cached = cache.packages.value val packages = (cached.ifEmpty { installedPackages(packageManager) }) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/data/settings/SettingsPlatformRepository.kt b/manager/app/src/main/java/com/resukisu/resukisu/data/settings/SettingsPlatformRepository.kt index 7736797ca..0468db6ec 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/data/settings/SettingsPlatformRepository.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/data/settings/SettingsPlatformRepository.kt @@ -217,9 +217,6 @@ class SettingsPlatformRepository( selinuxHideStatus = runCatching { ksuCliRepository.getFeatureStatus("selinux_hide") }.getOrDefault(""), - webViewZygoteUmountStatus = runCatching { - ksuCliRepository.getFeatureStatus("webview_zygote_umount") - }.getOrDefault(""), ) } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/di/AppModules.kt b/manager/app/src/main/java/com/resukisu/resukisu/di/AppModules.kt index 93dbd276f..f4783ca01 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/di/AppModules.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/di/AppModules.kt @@ -131,7 +131,6 @@ import com.resukisu.resukisu.domain.usecase.SetSelinuxHideEnabledUseCase import com.resukisu.resukisu.domain.usecase.SetStringPreferenceUseCase import com.resukisu.resukisu.domain.usecase.SetStringSetPreferenceUseCase import com.resukisu.resukisu.domain.usecase.SetSuEnabledUseCase -import com.resukisu.resukisu.domain.usecase.SetWebViewZygoteUmountEnabledUseCase import com.resukisu.resukisu.domain.usecase.StartKernelFlashUseCase import com.resukisu.resukisu.domain.usecase.SuSFSConfigUseCase import com.resukisu.resukisu.domain.usecase.TakeModuleUriPermissionUseCase @@ -320,7 +319,6 @@ val useCaseModule = module { factoryOf(::ConfigureSuLogUseCase) factoryOf(::SetSelinuxHideEnabledUseCase) factoryOf(::SetDefaultUmountModulesUseCase) - factoryOf(::SetWebViewZygoteUmountEnabledUseCase) factoryOf(::IsLateLoadModeUseCase) factoryOf(::GetAppProfileUseCase) factoryOf(::SetAppProfileUseCase) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/InstalledAppGroup.kt b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/InstalledAppGroup.kt index 363100749..5826a2290 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/InstalledAppGroup.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/InstalledAppGroup.kt @@ -1,12 +1,23 @@ package com.resukisu.resukisu.domain.model +const val WEBVIEW_ZYGOTE_UID = 1053 +const val WEBVIEW_ZYGOTE_PROFILE_KEY = "webview_zygote" + data class InstalledApp( val packageName: String, val label: String, val uid: Int, val isSystem: Boolean = false, val firstInstallTime: Long = 0L, -) + val profileKey: String = packageName, + val special: Boolean = false, +) { + val displayIdentifier: String + get() = if (special) profileKey else packageName + + val isWebViewZygote: Boolean + get() = special && uid == WEBVIEW_ZYGOTE_UID +} data class InstalledAppGroup( val uid: Int, @@ -19,15 +30,27 @@ data class InstalledAppGroup( val mainApp: InstalledApp get() = apps.first { it.packageName == primaryPackageName } + val profileKey: String + get() = mainApp.profileKey + + val isWebViewZygote: Boolean + get() = mainApp.isWebViewZygote + val packageNames: List get() = apps.map(InstalledApp::packageName) val allowSu: Boolean - get() = profile?.allowSu == true + get() = !isWebViewZygote && profile?.allowSu == true val hasCustomProfile: Boolean get() = profile?.let { - if (it.allowSu) !it.rootUseDefault else !it.nonRootUseDefault + if (isWebViewZygote) { + !it.nonRootUseDefault + } else if (it.allowSu) { + !it.rootUseDefault + } else { + !it.nonRootUseDefault + } } ?: false val isRecentlyInstalled: Boolean diff --git a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/KernelState.kt b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/KernelState.kt index 8e8730b58..b3092f2c5 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/KernelState.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/KernelState.kt @@ -39,7 +39,6 @@ data class KernelStatus( data class KernelFeatureSettings( val suEnabled: Boolean, val kernelUmountEnabled: Boolean, - val webViewZygoteUmountEnabled: Boolean, val suLogEnabled: Boolean, val selinuxHideEnabled: Boolean, val defaultUmountModules: Boolean, diff --git a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/SettingsPlatform.kt b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/SettingsPlatform.kt index 58d167332..037580080 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/SettingsPlatform.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/SettingsPlatform.kt @@ -33,7 +33,6 @@ data class PlatformFeatureStatus( val adbRootEnabled: Boolean = false, val sulogStatus: String = "", val selinuxHideStatus: String = "", - val webViewZygoteUmountStatus: String = "", ) sealed interface AppearanceSetting { diff --git a/manager/app/src/main/java/com/resukisu/resukisu/domain/usecase/KernelUseCases.kt b/manager/app/src/main/java/com/resukisu/resukisu/domain/usecase/KernelUseCases.kt index 4fff334c1..3dd248307 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/domain/usecase/KernelUseCases.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/domain/usecase/KernelUseCases.kt @@ -34,10 +34,6 @@ class SetDefaultUmountModulesUseCase(private val repository: KernelRepository) { suspend operator fun invoke(enabled: Boolean) = repository.setDefaultUmountModules(enabled) } -class SetWebViewZygoteUmountEnabledUseCase(private val repository: KernelRepository) { - suspend operator fun invoke(enabled: Boolean) = repository.setWebviewZygoteUmountEnabled(enabled) -} - class IsLateLoadModeUseCase(private val repository: KernelRepository) { operator fun invoke() = repository.isLateLoadMode() } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/AppProfile.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/AppProfile.kt index c59b73c7b..48a1a41fe 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/AppProfile.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/AppProfile.kt @@ -118,6 +118,7 @@ fun AppProfileScreen( val uiState by viewModel.state.collectAsStateWithLifecycle() val appGroup = uiState.appGroup val appLabel = appGroup?.mainApp?.label ?: packageName + val isSpecial = appGroup?.isWebViewZygote == true val failToUpdateAppProfile = stringResource(R.string.failed_to_update_app_profile).format( appLabel ) @@ -167,7 +168,7 @@ fun AppProfileScreen( topBar = { TopBar( title = appGroup.mainApp.label, - packageName = packageName, + packageName = appGroup.mainApp.displayIdentifier, colors = TopAppBarDefaults.topAppBarColors( containerColor = cardColor, scrolledContainerColor = cardColor @@ -188,9 +189,10 @@ fun AppProfileScreen( .blurSource(), topPadding = paddingValues.calculateTopPadding(), appGroup = appGroup, + isSpecial = isSpecial, appIcon = { PackageIcon( - packageName = appGroup.mainApp.packageName, + packageName = if (isSpecial) "android" else appGroup.mainApp.packageName, contentDescription = appGroup.mainApp.label, modifier = Modifier .padding(4.dp) @@ -233,6 +235,7 @@ private fun AppProfileInner( modifier: Modifier = Modifier, topPadding: Dp, appGroup: InstalledAppGroup, + isSpecial: Boolean = false, appIcon: @Composable () -> Unit, profile: AppProfile, defaultUmountModules: Boolean = profile.umountModules, @@ -245,7 +248,7 @@ private fun AppProfileInner( ) { val cardConfig: CardConfig = koinInject() val themeConfig: ThemeConfig = koinInject() - val isRootGranted = profile.allowSu + val isRootGranted = !isSpecial && profile.allowSu val affectedApplicationsTitle = stringResource(R.string.affected_applications) LazyColumn(modifier = modifier) { @@ -254,48 +257,62 @@ private fun AppProfileInner( } item { - SettingsDropdownWidget( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - title = appGroup.mainApp.label, - description = appGroup.mainApp.packageName, - iconPlaceholder = false, - leadingContent = { - appIcon() - }, - choice = -1, - data = listOf( - stringResource(id = R.string.launch_app), - stringResource(id = R.string.force_stop_app), - stringResource(id = R.string.restart_app) + if (isSpecial) { + SettingsBaseWidget( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + title = appGroup.mainApp.label, + description = appGroup.mainApp.displayIdentifier, + iconPlaceholder = false, + leadingContent = { + appIcon() + }, ) - ) { choice -> - when (choice) { - 0 -> onControlApp(AppControlAction.LAUNCH) - 1 -> onControlApp(AppControlAction.FORCE_STOP) - 2 -> onControlApp(AppControlAction.RESTART) - else -> throw IllegalStateException("Illegal choice: $choice") + } else { + SettingsDropdownWidget( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + title = appGroup.mainApp.label, + description = appGroup.mainApp.displayIdentifier, + iconPlaceholder = false, + leadingContent = { + appIcon() + }, + choice = -1, + data = listOf( + stringResource(id = R.string.launch_app), + stringResource(id = R.string.force_stop_app), + stringResource(id = R.string.restart_app) + ) + ) { choice -> + when (choice) { + 0 -> onControlApp(AppControlAction.LAUNCH) + 1 -> onControlApp(AppControlAction.FORCE_STOP) + 2 -> onControlApp(AppControlAction.RESTART) + else -> throw IllegalStateException("Illegal choice: $choice") + } } } } - item { - Surface( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - shape = RoundedCornerShape(16.dp), - color = MaterialTheme.colorScheme.surfaceBright.copy( - alpha = cardConfig.cardAlpha - ), - contentColor = MaterialTheme.colorScheme.onSurface, - ) - { - SettingsSwitchWidget( - icon = Icons.TwoTone.Security, - title = stringResource(id = R.string.superuser), - checked = isRootGranted, - onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) }, + if (!isSpecial) { + item { + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.surfaceBright.copy( + alpha = cardConfig.cardAlpha + ), + contentColor = MaterialTheme.colorScheme.onSurface, ) + { + SettingsSwitchWidget( + icon = Icons.TwoTone.Security, + title = stringResource(id = R.string.superuser), + checked = isRootGranted, + onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) }, + ) + } } } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt index 0834e6414..7251e035e 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt @@ -318,28 +318,6 @@ fun SettingsPage(bottomPadding: Dp) { ) } - item { - val webViewUmountSummary = when (uiState.webViewZygoteUmountStatus) { - "unsupported" -> stringResource(id = R.string.feature_status_unsupported_summary) - "managed" -> stringResource(id = R.string.feature_status_managed_summary) - else -> stringResource(id = R.string.settings_webview_zygote_umount_summary) - } - SettingsSwitchWidget( - icon = Icons.TwoTone.Language, - title = stringResource(id = R.string.settings_webview_zygote_umount), - description = webViewUmountSummary, - enabled = uiState.webViewZygoteUmountStatus == "supported", - checked = uiState.isWebViewZygoteUmountEnabled, - onCheckedChange = { checked -> - settingsViewModel.dispatch( - SettingsUiAction.SetWebViewZygoteUmountEnabled( - checked - ) - ) - }, - ) - } - item { val selinuxHideSummary = when (uiState.selinuxHideStatus) { "unsupported" -> stringResource(id = R.string.feature_status_unsupported_summary) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SuperUserPage.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SuperUserPage.kt index c827f007c..837bde05a 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SuperUserPage.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SuperUserPage.kt @@ -363,13 +363,13 @@ private fun SuperUserContent( } lazySegmentColumn( items = uiState.appGroupList, - key = { _, appGroup -> "${appGroup.uid}-${appGroup.mainApp.packageName}" }, + key = { _, appGroup -> "${appGroup.uid}-${appGroup.profileKey}" }, contentType = { _, _ -> "AppGroupItem" } ) { _, appGroup -> AppGroupItem( appGroup = appGroup ) { - navigator.push(Route.AppProfile(appGroup.uid, appGroup.mainApp.packageName)) + navigator.push(Route.AppProfile(appGroup.uid, appGroup.profileKey)) } } @@ -477,7 +477,7 @@ private fun AppGroupItem( description = if (appGroup.apps.size > 1) { stringResource(R.string.group_contains_apps, appGroup.apps.size) } else { - mainApp.packageName + mainApp.displayIdentifier }, descriptionColumnContent = { Spacer(modifier = Modifier.height(5.dp)) @@ -525,7 +525,7 @@ private fun AppGroupItem( }, leadingContent = { PackageIcon( - packageName = mainApp.packageName, + packageName = if (appGroup.isWebViewZygote) "android" else mainApp.packageName, contentDescription = mainApp.label, modifier = Modifier .padding(4.dp) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/AppProfileViewModel.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/AppProfileViewModel.kt index 163ed8744..53138e507 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/AppProfileViewModel.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/AppProfileViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import com.resukisu.resukisu.domain.model.AppControlAction import com.resukisu.resukisu.domain.model.AppProfile import com.resukisu.resukisu.domain.model.InstalledAppGroup +import com.resukisu.resukisu.domain.model.WEBVIEW_ZYGOTE_UID import com.resukisu.resukisu.domain.usecase.ControlAppUseCase import com.resukisu.resukisu.domain.usecase.GetAppProfileUseCase import com.resukisu.resukisu.domain.usecase.GetAppSepolicyUseCase @@ -76,7 +77,10 @@ class AppProfileViewModel( mutableState.update { it.copy(isLoading = true) } runCatching { val profile = getProfile(packageName, uid) - val loadedProfile = if (profile.allowSu) { + val isSpecial = uid == WEBVIEW_ZYGOTE_UID + val loadedProfile = if (isSpecial) { + profile.copy(allowSu = false) + } else if (profile.allowSu) { profile.copy( rules = runCatching { getSepolicy(packageName) } .getOrDefault(profile.rules) @@ -105,29 +109,37 @@ class AppProfileViewModel( is AppProfileUiAction.Save -> { val previous = mutableState.value.profile - mutableState.update { it.copy(profile = action.profile) } + val isSpecial = uid == WEBVIEW_ZYGOTE_UID + val profileToSave = if (isSpecial) { + action.profile.copy(allowSu = false) + } else { + action.profile + } + mutableState.update { it.copy(profile = profileToSave) } viewModelScope.launch { saveMutex.withLock { - val sepolicyKey = action.profile.rootTemplate ?: action.profile.name - if (action.profile.allowSu && !action.profile.rootUseDefault && - action.profile.rules.isNotEmpty() && - !setSepolicy(sepolicyKey, action.profile.rules) - ) { - rollbackIfCurrent(action.profile, previous) - mutableEvents.emit(AppProfileUiEvent.SepolicyUpdateFailed) - return@withLock + if (!isSpecial) { + val sepolicyKey = profileToSave.rootTemplate ?: profileToSave.name + if (profileToSave.allowSu && !profileToSave.rootUseDefault && + profileToSave.rules.isNotEmpty() && + !setSepolicy(sepolicyKey, profileToSave.rules) + ) { + rollbackIfCurrent(profileToSave, previous) + mutableEvents.emit(AppProfileUiEvent.SepolicyUpdateFailed) + return@withLock + } } - runCatching { setProfile(action.profile) } + runCatching { setProfile(profileToSave) } .onSuccess { saved -> if (saved) { mutableEvents.tryEmit(AppProfileUiEvent.Saved) } else { - rollbackIfCurrent(action.profile, previous) + rollbackIfCurrent(profileToSave, previous) mutableEvents.tryEmit(AppProfileUiEvent.Error()) } } .onFailure { - rollbackIfCurrent(action.profile, previous) + rollbackIfCurrent(profileToSave, previous) mutableEvents.tryEmit(AppProfileUiEvent.Error(it)) } } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/SettingsViewModel.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/SettingsViewModel.kt index 644990c8b..9276eff49 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/SettingsViewModel.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/SettingsViewModel.kt @@ -17,7 +17,6 @@ import com.resukisu.resukisu.domain.usecase.SetDefaultUmountModulesUseCase import com.resukisu.resukisu.domain.usecase.SetKernelUmountEnabledUseCase import com.resukisu.resukisu.domain.usecase.SetSelinuxHideEnabledUseCase import com.resukisu.resukisu.domain.usecase.SetSuEnabledUseCase -import com.resukisu.resukisu.domain.usecase.SetWebViewZygoteUmountEnabledUseCase import com.resukisu.resukisu.domain.usecase.UpdateAppearanceUseCase import com.resukisu.resukisu.domain.usecase.UpdatePlatformSettingUseCase import kotlinx.coroutines.flow.MutableSharedFlow @@ -97,8 +96,6 @@ data class SettingsUiState( val isSuLogEnabled: Boolean = false, val selinuxHideStatus: String = "", val isSelinuxHideEnabled: Boolean = false, - val webViewZygoteUmountStatus: String = "", - val isWebViewZygoteUmountEnabled: Boolean = false, val defaultUmountModules: Boolean = false, val useBuiltinMonoFont: Boolean = false, ) @@ -139,7 +136,6 @@ sealed interface SettingsUiAction { data class SetAdbRoot(val enabled: Boolean) : SettingsUiAction data class SetSuLog(val enabled: Boolean) : SettingsUiAction data class SetDefaultUmountModules(val enabled: Boolean) : SettingsUiAction - data class SetWebViewZygoteUmountEnabled(val enabled: Boolean) : SettingsUiAction } sealed interface SettingsUiEvent { @@ -159,7 +155,6 @@ class SettingsViewModel( private val setSuLogEnabled: ConfigureSuLogUseCase, private val setSelinuxHideEnabled: SetSelinuxHideEnabledUseCase, private val setDefaultUmountModules: SetDefaultUmountModulesUseCase, - private val setWebViewZygoteUmountEnabled: SetWebViewZygoteUmountEnabledUseCase, ) : ViewModel() { private val mutableState = MutableStateFlow(SettingsUiState()) val state: StateFlow = mutableState.asStateFlow() @@ -199,8 +194,6 @@ fun initialize() { isSuLogEnabled = features.suLogEnabled, selinuxHideStatus = platform.selinuxHideStatus, isSelinuxHideEnabled = features.selinuxHideEnabled, - webViewZygoteUmountStatus = platform.webViewZygoteUmountStatus, - isWebViewZygoteUmountEnabled = features.webViewZygoteUmountEnabled, defaultUmountModules = features.defaultUmountModules, ) } @@ -408,15 +401,6 @@ fun initialize() { } } - fun handleWebViewZygoteUmountChange(checked: Boolean) { - viewModelScope.launch { - if (setWebViewZygoteUmountEnabled(checked)) { - mutableState.update { it.copy( isWebViewZygoteUmountEnabled = checked) } - } - } - } - - fun dispatch(action: SettingsUiAction) { when (action) { SettingsUiAction.Initialize -> initialize() @@ -458,7 +442,6 @@ fun dispatch(action: SettingsUiAction) { is SettingsUiAction.SetSuLog -> handleSuLogChange(action.enabled) is SettingsUiAction.SetDefaultUmountModules -> handleDefaultUmountModulesChange(action.enabled) - is SettingsUiAction.SetWebViewZygoteUmountEnabled -> handleWebViewZygoteUmountChange(action.enabled) } } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/SuperUserViewModel.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/SuperUserViewModel.kt index 879173bd3..ae5ad7eda 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/SuperUserViewModel.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/SuperUserViewModel.kt @@ -181,12 +181,12 @@ class SuperUserViewModel( .filter { group -> group.apps.any { app -> app.label.contains(search, true) || - app.packageName.contains(search, true) || + app.displayIdentifier.contains(search, true) || transliterateText(app.label).contains(search, true) } } .filter { group -> - group.uid == 2000 || showSystemApps || group.apps.any { !it.isSystem } + group.isWebViewZygote || group.uid == 2000 || showSystemApps || group.apps.any { !it.isSystem } } .sortedWith { first, second -> val priority = groupPriority(first).compareTo(groupPriority(second)) diff --git a/manager/app/src/main/res/values-fr/strings.xml b/manager/app/src/main/res/values-fr/strings.xml index 268a80185..2d19f3b50 100644 --- a/manager/app/src/main/res/values-fr/strings.xml +++ b/manager/app/src/main/res/values-fr/strings.xml @@ -535,8 +535,6 @@ Importer Configuration par défaut Supprime la configuration SuSFS actuelle et restaure la configuration par défaut - Remarques importantes :\n• Les chemins cible et redirigé doivent exister avant l’ajout d’une entrée\n• les permissions SELinux pour les deux chemins doivent être configurées.\n• La redirection affecte uniquement les processus correspondant au schéma d\'UID sélectionné. - Empêche les fuites d\'informations du processus WebView, mais peut causer le dysfonctionnement de certains modules. Redémarrer pour appliquer les modifications - Démontage pour WebView - + Remarques importantes :\n• Les chemins cible et redirigé doivent exister avant l’ajout d’une entrée\n• les permissions SELinux pour les deux chemins doivent être configurées.\n• La redirection affecte uniquement les processus correspondant au schéma d\'UID sélectionné. + diff --git a/manager/app/src/main/res/values-hu/strings.xml b/manager/app/src/main/res/values-hu/strings.xml index 41891d914..fb2f74f1c 100644 --- a/manager/app/src/main/res/values-hu/strings.xml +++ b/manager/app/src/main/res/values-hu/strings.xml @@ -536,7 +536,5 @@ SuSFS Menedzser Beépített SuSFS Menedzser engedélyezése vagy letiltása. Valamely harmadik féltől származó modullal összeférhetetlen lehet. Beépített SuSFS Menedzser letiltva. Engedélyezd, vagy telepíts egy harmadik féltől származó modult a SuSFS funkciók használatához. - WebView leválasztás - Megakadályozza az információszivárgást a WebView folyamatból, de modulok működését zavarhatja. Az alkalmazáshoz újraindítás szükséges diff --git a/manager/app/src/main/res/values-pt-rBR/strings.xml b/manager/app/src/main/res/values-pt-rBR/strings.xml index a773477c7..1103b724d 100644 --- a/manager/app/src/main/res/values-pt-rBR/strings.xml +++ b/manager/app/src/main/res/values-pt-rBR/strings.xml @@ -222,8 +222,6 @@ Versão do Android Modelo do dispositivo Não é permitido conceder privilégios de superusuário a %s - Desmontar para WebView - Impede o vazamento de informações do processo WebView, mas pode causar problemas em alguns módulos. Reinicie o sistema para aplicar as alterações O kernel não suporta essa funcionalidade Desativar até a reinicialização Desativar sempre diff --git a/manager/app/src/main/res/values-ru/strings.xml b/manager/app/src/main/res/values-ru/strings.xml index 03c52d104..78e589416 100644 --- a/manager/app/src/main/res/values-ru/strings.xml +++ b/manager/app/src/main/res/values-ru/strings.xml @@ -536,7 +536,4 @@ менеджер SUSFS Включите встроенный менеджер SUSFS. Может конфликтовать со сторонними модулями. Встроенный менеджер SUSFS отключен. Включите его или установите сторонний модуль для использования функций SUSFS. - Отключать монтирование для WebView - Предотвращает утечку информации из процесса WebView, но может нарушить работу модулей. Перезагрузите для применения - diff --git a/manager/app/src/main/res/values-tr/strings.xml b/manager/app/src/main/res/values-tr/strings.xml index 0ecb78aea..4312a8faa 100644 --- a/manager/app/src/main/res/values-tr/strings.xml +++ b/manager/app/src/main/res/values-tr/strings.xml @@ -532,8 +532,6 @@ Veri Yok Yönlendirmeyi Aç - WebView bağlamasını kaldır - WebView işleminden bilgi sızıntılarını önler ancak bazı modüllerin çalışmasını bozabilir. Uygulamak için yeniden başlatın Yerleşik eş aralıklı yazı tipini kullan Günlükleri görüntülerken sistemin eş aralıklı yazı tipiyle ilgili sorunları önlemek için yerleşik JetBrains Mono yazı tipini kullanır SUSFS Yöneticisi diff --git a/manager/app/src/main/res/values-uk/strings.xml b/manager/app/src/main/res/values-uk/strings.xml index 9ad647f06..42cb578b3 100644 --- a/manager/app/src/main/res/values-uk/strings.xml +++ b/manager/app/src/main/res/values-uk/strings.xml @@ -536,7 +536,5 @@ Увімкнути вбудований менеджер SUSFS. Може конфліктувати зі сторонніми модулями. Менеджер SUSFS Використовувати вбудований моноширинний шрифт - Запобігає витоку інформації з процесу WebView, але це може порушити роботу модулів. Перезавантажте, щоб застосувати зміни. - Розмонтувати WebView diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index 9ad38e9d1..7775230ca 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -222,8 +222,6 @@ 允许通过 /system/bin/su 获取 Root 权限。 内核处理卸载模块 在内核给需要的应用卸载模块 - 为 WebView 卸载模块 - 防止 WebView 进程泄露信息,但可能会导致模块失效。重启后生效 内核不支持此功能 禁用直到下次重启 始终禁用 diff --git a/manager/app/src/main/res/values-zh-rHK/strings.xml b/manager/app/src/main/res/values-zh-rHK/strings.xml index a8c780703..50740a9e0 100644 --- a/manager/app/src/main/res/values-zh-rHK/strings.xml +++ b/manager/app/src/main/res/values-zh-rHK/strings.xml @@ -534,6 +534,4 @@ SUSFS 管理器 啟用內置 SUSFS 管理器,可能與第三方模組衝突。 內置 SUSFS 管理器已停用。請啟用它或安裝第三方模組以使用 SUSFS 功能。 - 為 WebView 解除掛載 - 防止 WebView 程序洩露資訊,但可能會令部分模組失效。重新啟動以套用變更 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index ba222504e..519fb022c 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -225,8 +225,6 @@ Allow root access via /system/bin/su, in new processes. Module unmounting Unmount modules from kernel in App Profile - Unmount for WebView - Prevent information leaks from WebView process but may break modules. Reboot to apply Kernel does not support this feature Disable until Reboot Always disable diff --git a/uapi/feature.h b/uapi/feature.h index 1049457c2..df2f68c15 100644 --- a/uapi/feature.h +++ b/uapi/feature.h @@ -7,7 +7,6 @@ enum ksu_feature_id { KSU_FEATURE_SULOG = 2, KSU_FEATURE_ADB_ROOT = 3, KSU_FEATURE_SELINUX_HIDE = 4, - KSU_FEATURE_WEBVIEW_ZYGOTE_UMOUNT = 5, KSU_FEATURE_MAX }; diff --git a/userspace/ksud/src/android/cli.rs b/userspace/ksud/src/android/cli.rs index fe48bcd1a..3a5458c58 100755 --- a/userspace/ksud/src/android/cli.rs +++ b/userspace/ksud/src/android/cli.rs @@ -461,7 +461,7 @@ enum Profile { enum Feature { /// Get feature value and support status Get { - /// Feature ID or name (su_compat, kernel_umount, sulog, adb_root, selinux_hide, webview_zygote_umount) + /// Feature ID or name (su_compat, kernel_umount, sulog, adb_root, selinux_hide) id: String, /// Read from config file #[arg(long, default_value_t = false)] @@ -481,7 +481,7 @@ enum Feature { /// Check feature status (supported/unsupported/managed) Check { - /// Feature ID or name (su_compat, kernel_umount, sulog, adb_root, selinux_hide, webview_zygote_umount) + /// Feature ID or name (su_compat, kernel_umount, sulog, adb_root, selinux_hide) id: String, }, diff --git a/userspace/ksud/src/android/feature.rs b/userspace/ksud/src/android/feature.rs index ef6eb6b4f..5d8964eea 100644 --- a/userspace/ksud/src/android/feature.rs +++ b/userspace/ksud/src/android/feature.rs @@ -26,7 +26,6 @@ pub enum FeatureId { Sulog = 2, AdbRoot = 3, SelinuxHide = 4, - WebviewZygoteUmount = 5, } impl FeatureId { @@ -37,7 +36,6 @@ impl FeatureId { 2 => Some(Self::Sulog), 3 => Some(Self::AdbRoot), 4 => Some(Self::SelinuxHide), - 5 => Some(Self::WebviewZygoteUmount), _ => None, } } @@ -49,7 +47,6 @@ impl FeatureId { Self::Sulog => "sulog", Self::AdbRoot => "adb_root", Self::SelinuxHide => "selinux_hide", - Self::WebviewZygoteUmount => "webview_zygote_umount", } } @@ -68,9 +65,6 @@ impl FeatureId { Self::SelinuxHide => { "SELinux Hide - sanitize /sys/fs/selinux access results for app UIDs" } - Self::WebviewZygoteUmount => { - "WebView Zygote Umount - unmount modules from WebView zygote and its isolated children" - } } } } @@ -82,7 +76,6 @@ fn parse_feature_id(name: &str) -> Result { "sulog" | "2" => Ok(FeatureId::Sulog), "adb_root" | "3" => Ok(FeatureId::AdbRoot), "selinux_hide" | "4" => Ok(FeatureId::SelinuxHide), - "webview_zygote_umount" | "5" => Ok(FeatureId::WebviewZygoteUmount), _ => bail!("Unknown feature: {name}"), } } @@ -329,7 +322,6 @@ pub fn list_features() { FeatureId::Sulog, FeatureId::AdbRoot, FeatureId::SelinuxHide, - FeatureId::WebviewZygoteUmount, ]; for feature_id in &all_features { @@ -393,7 +385,6 @@ pub fn save_config() -> Result<()> { FeatureId::Sulog, FeatureId::AdbRoot, FeatureId::SelinuxHide, - FeatureId::WebviewZygoteUmount, ]; for feature_id in &all_features { From f5992bfce3b292c69989ae8f41e49769563d2db9 Mon Sep 17 00:00:00 2001 From: Wang Han <416810799@qq.com> Date: Fri, 4 Sep 2026 11:58:54 +0800 Subject: [PATCH 02/34] fix(kernel): Preserve a ebitmap's size for each policy type (https://github.com/tiann/KernelSU/pull/3680) Analyzed-by: 5ec1cff <56485584+5ec1cff@users.noreply.github.com> [cherry-picked upstream commit https://github.com/tiann/KernelSU/commit/32ce89ffcc91935e86b8579e843228b033b21382] --- kernel/selinux/sepolicy.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/kernel/selinux/sepolicy.c b/kernel/selinux/sepolicy.c index 533477b5c..d032e2a56 100644 --- a/kernel/selinux/sepolicy.c +++ b/kernel/selinux/sepolicy.c @@ -1151,12 +1151,17 @@ int ksu_dup_policydb(struct policydb *old_db, struct policydb *new_db) int len = 0; ksu_lock_sepolicy_legacy(); - len = old_db->len; + + // Some device policy db seems not marking type itself in type_attr_map_array + // policydb_read() adds each type to its own attribute map, so old_db->policydb.len may be smaller + // preserve one ebitmap entry for this condition to avoid trigger -EINVAL + len = old_db->len + (size_t)old_db->p_types.nprim * (sizeof(u32) + sizeof(u64)); + ksu_unlock_sepolicy_legacy(); data = vmalloc(len); if (!data) { - pr_err("alloc policy len %d\n", len); + pr_err("alloc policy buffer len %d\n", len); ret = -ENOMEM; goto out_free_data; } @@ -1171,6 +1176,7 @@ int ksu_dup_policydb(struct policydb *old_db, struct policydb *new_db) ksu_unlock_sepolicy_legacy(); goto out_free_data; } + len -= fp.len; ksu_unlock_sepolicy_legacy(); // https://android-review.googlesource.com/c/kernel/common/+/3009995 @@ -1204,7 +1210,7 @@ int ksu_dup_policydb(struct policydb *old_db, struct policydb *new_db) goto out_free_data; } - new_db->len = old_db->len; + new_db->len = len; vfree(data); ret = len; From fdd18805bdac7d05aec4db2ef7a4a548037e3857 Mon Sep 17 00:00:00 2001 From: Wang Han <416810799@qq.com> Date: Sun, 6 Sep 2026 12:59:35 +0800 Subject: [PATCH 03/34] feat(ksud): Implement SIGSYS handler (https://github.com/tiann/KernelSU/pull/3677) [cherry-picked upstream commit https://github.com/tiann/KernelSU/commit/35feb58872b6f88ac1b488ce46a68892a017d62d] Co-Authored-By: 5ec1cff Co-Authored-By: AlexLiuDev233 --- userspace/ksud/src/android/cli.rs | 4 +- userspace/ksud/src/android/ksucalls.rs | 113 ++++++++++++++++++++----- userspace/ksud/src/android/sulog.rs | 20 ++--- 3 files changed, 105 insertions(+), 32 deletions(-) diff --git a/userspace/ksud/src/android/cli.rs b/userspace/ksud/src/android/cli.rs index 3a5458c58..6f2bce33d 100755 --- a/userspace/ksud/src/android/cli.rs +++ b/userspace/ksud/src/android/cli.rs @@ -572,6 +572,8 @@ pub fn run() -> Result<()> { .with_tag("KernelSU"), ); + ksucalls::setup_sigsys_handler(); + // the kernel executes su with argv[0] = "su" and replace it with us let arg0 = std::env::args().next().unwrap_or_default(); if arg0 == "su" || arg0.ends_with("/su") { @@ -871,7 +873,7 @@ pub fn run() -> Result<()> { Kernel::Umount { command } => match command { UmountOp::Add { mnt, flags } => ksucalls::umount_list_add(&mnt, flags), UmountOp::Del { mnt } => ksucalls::umount_list_del(&mnt), - UmountOp::Wipe => ksucalls::umount_list_wipe().map_err(Into::into), + UmountOp::Wipe => ksucalls::umount_list_wipe(), UmountOp::List => { let list = ksucalls::umount_list_list()?; println!("{}", serde_json::to_string(&list)?); diff --git a/userspace/ksud/src/android/ksucalls.rs b/userspace/ksud/src/android/ksucalls.rs index 8cf217fd6..78e5c3347 100644 --- a/userspace/ksud/src/android/ksucalls.rs +++ b/userspace/ksud/src/android/ksucalls.rs @@ -1,10 +1,75 @@ #![allow(clippy::unreadable_literal)] -use anyhow::bail; +use anyhow::{Result, bail}; -use std::{fs, os::fd::RawFd, sync::OnceLock}; +use std::{cell::Cell, fs, os::fd::RawFd, sync::OnceLock}; use crate::{android::uapi, defs::MountInfo}; +// sigsys handler +std::thread_local! { + static SVC_IN_FLIGHT: Cell = const { Cell::new(false) }; + static SIGSYS_OCCURRED: Cell = const { Cell::new(false) }; +} + +const SYS_SECCOMP: libc::c_int = 1; + +fn with_svc_call(call: F) -> R +where + F: FnOnce() -> R, +{ + SVC_IN_FLIGHT.with(|in_flight| in_flight.set(true)); + let result = call(); + SVC_IN_FLIGHT.with(|in_flight| in_flight.set(false)); + result +} + +fn take_sigsys_occurred() -> bool { + SIGSYS_OCCURRED.with(|occurred| occurred.replace(false)) +} + +extern "C" fn sigsys_handler( + _sig: libc::c_int, + info: *mut libc::siginfo_t, + ctx: *mut libc::c_void, +) { + unsafe { + if info.is_null() || ctx.is_null() || (*info).si_code != SYS_SECCOMP { + return; + } + if SVC_IN_FLIGHT.with(Cell::get) { + SIGSYS_OCCURRED.with(|occurred| occurred.set(true)); + } + + let ucontext = ctx.cast::(); + #[cfg(target_arch = "aarch64")] + { + (*ucontext).uc_mcontext.regs[0] = (-libc::EPERM) as u64; + } + #[cfg(target_arch = "arm")] + { + (*ucontext).uc_mcontext.arm_r0 = (-libc::EPERM) as u32; + } + #[cfg(target_arch = "x86_64")] + { + let rax = libc::REG_RAX as usize; + (*ucontext).uc_mcontext.gregs[rax] = i64::from(-libc::EPERM); + } + } +} + +pub fn setup_sigsys_handler() { + unsafe { + let mut sa: libc::sigaction = std::mem::zeroed(); + sa.sa_flags = libc::SA_SIGINFO; + sa.sa_sigaction = sigsys_handler as *const () as usize; + libc::sigemptyset(std::ptr::addr_of_mut!(sa.sa_mask)); + if libc::sigaction(libc::SIGSYS, std::ptr::addr_of!(sa), std::ptr::null_mut()) != 0 { + let error = std::io::Error::last_os_error(); + log::warn!("Failed to set SIGSYS handler: {error}"); + } + } +} + // Global driver fd cache static DRIVER_FD: OnceLock = OnceLock::new(); static INFO_CACHE: OnceLock = OnceLock::new(); @@ -32,15 +97,19 @@ fn init_driver_fd() -> Option { let fd = scan_driver_fd(); if fd.is_none() { let mut fd = -1; - unsafe { + with_svc_call(|| unsafe { libc::syscall( libc::SYS_reboot, uapi::KSU_INSTALL_MAGIC1_RUST, uapi::KSU_INSTALL_MAGIC2_RUST, 0, &mut fd, - ); - }; + ) + }); + if take_sigsys_occurred() { + eprintln!("KernelSU driver install syscall was blocked by seccomp"); + log::error!("KernelSU driver install syscall was blocked by seccomp"); + } if fd >= 0 { Some(fd) } else { None } } else { fd @@ -48,17 +117,19 @@ fn init_driver_fd() -> Option { } // ioctl wrapper using libc -pub fn ksuctl(request: u32, arg: *mut T) -> std::io::Result { +pub fn ksuctl(request: u32, arg: *mut T) -> Result { use std::io; let fd = *DRIVER_FD.get_or_init(|| init_driver_fd().unwrap_or(-1)); + if fd < 0 { + bail!("could not retrieve kernelsu driver fd") + } unsafe { let ret = libc::ioctl(fd as libc::c_int, request as i32, arg); if ret < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(ret) + bail!("ioctl failed: {}", io::Error::last_os_error()); } + Ok(ret) } } @@ -140,7 +211,7 @@ pub fn get_full_version() -> String { } } -pub fn grant_root() -> std::io::Result<()> { +pub fn grant_root() -> Result<()> { ksuctl(uapi::KSU_IOCTL_GRANT_ROOT_RUST, std::ptr::null_mut::())?; Ok(()) } @@ -168,7 +239,7 @@ pub fn check_kernel_safemode() -> bool { cmd.in_safe_mode != 0 } -pub fn set_sepolicy(payload: *const u8, payload_len: u64) -> std::io::Result { +pub fn set_sepolicy(payload: *const u8, payload_len: u64) -> Result { let mut ioctl_cmd = uapi::ksu_set_sepolicy_cmd { data_len: payload_len, data: payload as u64, @@ -179,7 +250,7 @@ pub fn set_sepolicy(payload: *const u8, payload_len: u64) -> std::io::Result std::io::Result<(u64, bool)> { +pub fn get_feature(feature_id: u32) -> Result<(u64, bool)> { let mut cmd = uapi::ksu_get_feature_cmd { feature_id, value: 0, @@ -190,13 +261,13 @@ pub fn get_feature(feature_id: u32) -> std::io::Result<(u64, bool)> { } /// Set feature value in kernel -pub fn set_feature(feature_id: u32, value: u64) -> std::io::Result<()> { +pub fn set_feature(feature_id: u32, value: u64) -> Result<()> { let mut cmd = uapi::ksu_set_feature_cmd { feature_id, value }; ksuctl(uapi::KSU_IOCTL_SET_FEATURE_RUST, &raw mut cmd)?; Ok(()) } -pub fn get_wrapped_fd(fd: RawFd) -> std::io::Result { +pub fn get_wrapped_fd(fd: RawFd) -> Result { let mut cmd = uapi::ksu_get_wrapper_fd_cmd { fd: fd as u32, flags: 0, @@ -205,14 +276,14 @@ pub fn get_wrapped_fd(fd: RawFd) -> std::io::Result { Ok(result) } -pub fn get_sulog_fd() -> std::io::Result { +pub fn get_sulog_fd() -> Result { let mut cmd = uapi::ksu_get_sulog_fd_cmd { flags: 0 }; let result = ksuctl(uapi::KSU_IOCTL_GET_SULOG_FD, &raw mut cmd)?; Ok(result) } /// Get mark status for a process (pid=0 returns total marked count) -pub fn mark_get(pid: i32) -> std::io::Result { +pub fn mark_get(pid: i32) -> Result { let mut cmd = uapi::ksu_manage_mark_cmd { operation: uapi::KSU_MARK_GET_RUST, pid, @@ -223,7 +294,7 @@ pub fn mark_get(pid: i32) -> std::io::Result { } /// Mark a process (pid=0 marks all processes) -pub fn mark_set(pid: i32) -> std::io::Result<()> { +pub fn mark_set(pid: i32) -> Result<()> { let mut cmd = uapi::ksu_manage_mark_cmd { operation: uapi::KSU_MARK_MARK_RUST, pid, @@ -234,7 +305,7 @@ pub fn mark_set(pid: i32) -> std::io::Result<()> { } /// Unmark a process (pid=0 unmarks all processes) -pub fn mark_unset(pid: i32) -> std::io::Result<()> { +pub fn mark_unset(pid: i32) -> Result<()> { let mut cmd = uapi::ksu_manage_mark_cmd { operation: uapi::KSU_MARK_UNMARK_RUST, pid, @@ -245,7 +316,7 @@ pub fn mark_unset(pid: i32) -> std::io::Result<()> { } /// Refresh mark for all running processes -pub fn mark_refresh() -> std::io::Result<()> { +pub fn mark_refresh() -> Result<()> { let mut cmd = uapi::ksu_manage_mark_cmd { operation: uapi::KSU_MARK_REFRESH_RUST, pid: 0, @@ -265,7 +336,7 @@ pub fn nuke_ext4_sysfs(mnt: &str) -> anyhow::Result<()> { } /// Wipe all entries from umount list -pub fn umount_list_wipe() -> std::io::Result<()> { +pub fn umount_list_wipe() -> Result<()> { let mut cmd = uapi::ksu_manage_try_umount_cmd { arg: 0, flags: 0, @@ -300,7 +371,7 @@ pub fn umount_list_del(path: &str) -> anyhow::Result<()> { } /// Set current process's process group to init_group (pgid = 0) -pub fn set_init_pgrp() -> std::io::Result<()> { +pub fn set_init_pgrp() -> Result<()> { ksuctl( uapi::KSU_IOCTL_SET_INIT_PGRP_RUST, std::ptr::null_mut::(), diff --git a/userspace/ksud/src/android/sulog.rs b/userspace/ksud/src/android/sulog.rs index a7ca03937..3b959416b 100644 --- a/userspace/ksud/src/android/sulog.rs +++ b/userspace/ksud/src/android/sulog.rs @@ -641,22 +641,22 @@ fn handle_readable(fd: RawFd, writer: &mut DailyLogWriter) -> Result } } -pub fn open_sulog_fd() -> io::Result { +pub fn open_sulog_fd() -> Result { let fd = ksucalls::get_sulog_fd()?; - let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) }; + let fd = unsafe { OwnedFd::from_raw_fd(fd) }; + let flags = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETFL) }; if flags < 0 { - let err = io::Error::last_os_error(); - let _ = unsafe { libc::close(fd) }; - return Err(err); + bail!("open_sulog_fd: get flags: {}", io::Error::last_os_error()); } - if unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) } < 0 { - let err = io::Error::last_os_error(); - let _ = unsafe { libc::close(fd) }; - return Err(err); + if unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_SETFL, flags | libc::O_NONBLOCK) } < 0 { + bail!( + "open_sulog_fd: set cloexec flags: {}", + io::Error::last_os_error() + ) } - Ok(unsafe { OwnedFd::from_raw_fd(fd) }) + Ok(fd) } fn write_session_marker( From f1ae2cebb3503236cfde52fb3caf8e69f1c083a1 Mon Sep 17 00:00:00 2001 From: Nullptr Date: Sun, 6 Sep 2026 13:52:49 +0800 Subject: [PATCH 04/34] kernel, uapi, ksud: make fd wrapper work with app profile (https://github.com/tiann/KernelSU/pull/3679) [cherry-picked from upstream commit https://github.com/tiann/KernelSU/commit/c72f294e09536222d450237e4c1f0271bfe145ff] Co-authored-by: 5ec1cff Signed-off-by: AlexLiuDev233 --- kernel/feature/sucompat.c | 68 ++++++++++++++++++++---- kernel/feature/sucompat.h | 13 +++++ kernel/hook/lsm_hook_magic.h | 9 +++- kernel/hook/lsm_hooks.c | 29 ++++++++++- kernel/infra/file_wrapper.c | 17 +++++- kernel/supercall/dispatch.c | 11 ++-- kernel/supercall/internal.h | 3 +- kernel/supercall/supercall.c | 71 ++++++++++++++++++-------- kernel/supercall/supercall.h | 5 ++ kernel/tools/kernel_compat.mk | 10 ++++ kernel/tools/susfs_compat.mk | 8 +++ uapi/supercall.h | 3 +- userspace/ksud/Cargo.lock | 66 ------------------------ userspace/ksud/Cargo.toml | 1 - userspace/ksud/src/android/ksucalls.rs | 30 ++++++++--- userspace/ksud/src/android/su.rs | 33 +++++++++--- userspace/ksud/src/android/unload.rs | 4 +- 17 files changed, 255 insertions(+), 126 deletions(-) diff --git a/kernel/feature/sucompat.c b/kernel/feature/sucompat.c index 4fd64343d..244d26d0b 100644 --- a/kernel/feature/sucompat.c +++ b/kernel/feature/sucompat.c @@ -33,6 +33,7 @@ #include "runtime/ksud.h" #include "feature/sucompat.h" #include "policy/app_profile.h" +#include "supercall/supercall.h" #ifdef CONFIG_KSU_TRACEPOINT_HOOK #include "hook/syscall_hook.h" #else @@ -235,6 +236,7 @@ static long ksu_handle_execve_sucompat_common_internal(const char __user **filen char path[sizeof(su_path) + 1]; long ret, orig_regs[5]; unsigned long addr; + int su_fd = -1; int tmp_fd; struct file *ksud_file; const struct cred *old_cred; @@ -309,6 +311,13 @@ static long ksu_handle_execve_sucompat_common_internal(const char __user **filen regs->__PT_PARM3_REG = orig_regs[2]; regs->__PT_SYSCALL_PARM4_REG = orig_regs[3]; regs->__PT_PARM5_REG = orig_regs[4]; + } else { + // Only grant the scoped driver capability after the selected root + // profile has been applied successfully. + su_fd = ksu_install_su_fd(); + if (su_fd < 0) { + pr_warn("install su session fd failed: %d\n", su_fd); + } } return ret; @@ -343,19 +352,19 @@ static inline int do_ksu_handle_execveat_sucompat(int *fd, const char *filename, // Yep, maybe someusers love turn off sucompat <- idk how they managed to keep using it // But for mostly users, sucompat is enabled, so unlikely here if (!static_branch_unlikely(&ksu_su_compat_enabled)) { - return 0; + return -EINVAL; } #else if (!ksu_su_compat_enabled) { - return 0; + return -EINVAL; } #endif if (!is_allowed) - return 0; + return -EINVAL; if (likely(memcmp(filename, su_path, sizeof(su_path)))) - return 0; + return -EINVAL; pr_info("do_execveat_common su found\n"); @@ -375,6 +384,29 @@ static inline int do_ksu_handle_execveat_sucompat(int *fd, const char *filename, memcpy((void *)filename, ksud_path, sizeof(ksud_path)); out: ksu_sulog_emit_pending(pending_sucompat, 0, GFP_KERNEL); +#ifdef CONFIG_KSU_MANUAL_HOOK + // flag for post execve hook, mostly: bprm_committed_creds LSM hooks + // no need care in susfs, susfs completed everything + set_thread_flag(TIF_PROC_IN_KSU_EXECVE); +#endif + return 0; +} + +// fd, filename, argv, envp, flags and retval were NOT provided in bprm_committed_creds (KSU_COMPAT_NO_POST_EXECVE_HOOK)! +int ksu_handle_post_execve(int *fd, const char *filename, void *argv, void *envp, int *flags, int *retval) +{ +#ifdef CONFIG_KSU_MANUAL_HOOK + if (likely(!test_thread_flag(TIF_PROC_IN_KSU_EXECVE))) { + return -EINVAL; + } +#endif +#ifndef KSU_COMPAT_HAS_SUSFS_INSTALL_SU_FD_DIRECT_CALL + ksu_install_su_fd(); +#endif + // #ifdef KSU_COMPAT_NO_POST_EXECVE_HOOK + // return 0; + // #endif + // TODO Implement tmpfd of ksud when KSU_COMPAT_NO_POST_EXECVE_HOOK is not defined return 0; } @@ -412,7 +444,7 @@ int ksu_handle_execve(int *fd, const char *filename, void *argv, void *envp, int #ifndef CONFIG_KSU_TRACEPOINT_HOOK if (ksu_is_current_proc_unprivillege()) { - return 0; + return -EINVAL; } #endif @@ -424,7 +456,7 @@ int ksu_handle_execve(int *fd, const char *filename, void *argv, void *envp, int } if (*fd != AT_FDCWD || *flags != 0) { - return 0; + return -EINVAL; } skip_check: @@ -453,21 +485,29 @@ int ksu_handle_execve(int *fd, const char *filename, void *argv, void *envp, int return ret; } -// old hook, link to ksu_handle_execve int ksu_handle_execveat(int *fd, struct filename **filename_ptr, void *argv, void *envp, int *flags) { struct filename *filename; filename = *filename_ptr; if (IS_ERR(filename)) { - return 0; + return -EINVAL; } return ksu_handle_execve(fd, filename->name, argv, envp, flags); } -// because simonpunk, he do check in hook side -// and call ksu_handle_execveat_sucompat -// we need unpack filename* in here, and pass it to ksu_handle_execveat +int ksu_handle_post_execveat(int *fd, struct filename **filename_ptr, void *argv, void *envp, int *flags, int *retval) +{ + struct filename *filename; + filename = *filename_ptr; + if (IS_ERR(filename)) { + return -EINVAL; + } + + return ksu_handle_post_execve(fd, filename->name, argv, envp, flags, retval); +} + +// compat for check in hook #ifdef CONFIG_KSU_SUSFS int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr, void *argv, void *envp, int *flags) { @@ -479,6 +519,12 @@ int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr, void * return ksu_handle_execveat(fd, filename_ptr, argv, envp, flags); } + +int ksu_handle_post_execveat_sucompat(int *fd, struct filename **filename_ptr, void *argv, void *envp, int *flags, + int *retval) +{ + return ksu_handle_post_execveat(fd, filename_ptr, argv, envp, flags, retval); +} #endif #endif diff --git a/kernel/feature/sucompat.h b/kernel/feature/sucompat.h index 0c6365c0b..13fcfbeb0 100644 --- a/kernel/feature/sucompat.h +++ b/kernel/feature/sucompat.h @@ -20,6 +20,7 @@ int ksu_handle_stat(int *dfd, struct filename **filename, int *flags); #else int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode, int *__unused_flags); int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags); +int ksu_handle_post_execve(int *fd, const char *filename, void *argv, void *envp, int *flags, int *retval); #endif // #ifdef CONFIG_KSU_SUSFS #ifdef CONFIG_KSU_TRACEPOINT_HOOK @@ -47,12 +48,24 @@ long ksu_handle_execveat_sucompat_internal(const char __user **filename_user, in #define ksu_clear_current_proc_unprivillege susfs_clear_current_proc_no_su #else // manual hook +// we have a huge number spare TIFs can use +// https://elixir.bootlin.com/linux/v7.2.2/source/arch/arm64/include/asm/thread_info.h#L90 +// https://elixir.bootlin.com/linux/v7.2.2/source/arch/arm/include/asm/thread_info.h#L154 +// https://elixir.bootlin.com/linux/v7.2.2/source/arch/x86/include/asm/thread_info.h#L103 +// 23 - 31 is spare in arm32 (9 tifs) +// 32 - 63 is spare in arm64 (32 tifs) +// 28 - 31 is spare in x86 (4 tifs) +// 28 - 63 is spare in x86-64 (36 tifs) + // 63 already used as TIF_KSU_DISABLE_ESCAPE_WITH_ROOT (64bit) // 31 already used as TIF_KSU_DISABLE_ESCAPE_WITH_ROOT (32bit) +// TIF_PROC_IN_KSU_EXECVE may reuse in future? because it only useful when current->in_execve=1 #ifdef CONFIG_64BIT #define TIF_PROC_NON_PRIVILEGE 62 +#define TIF_PROC_IN_KSU_EXECVE 61 #else #define TIF_PROC_NON_PRIVILEGE 30 +#define TIF_PROC_IN_KSU_EXECVE 29 #endif static inline bool ksu_is_current_proc_unprivillege(void) diff --git a/kernel/hook/lsm_hook_magic.h b/kernel/hook/lsm_hook_magic.h index c98aec693..905f5df31 100644 --- a/kernel/hook/lsm_hook_magic.h +++ b/kernel/hook/lsm_hook_magic.h @@ -31,12 +31,17 @@ struct ksu_lsm_hook { int offset; }; +// clang-format off #define KSU_LSM_HOOK_INIT(member, target_symbol, replacement_fn, off) \ { \ - .head_name = #member, .target_name = target_symbol, .head_offset = offsetof(KSU_LSM_HOOK_HEADS_TYPE, member), \ - .hook_offset = offsetof(struct security_hook_list, hook.member), .replacement = (void *)(replacement_fn), \ + .head_name = #member, \ + .target_name = target_symbol, \ + .head_offset = offsetof(KSU_LSM_HOOK_HEADS_TYPE, member), \ + .hook_offset = offsetof(struct security_hook_list, hook.member), \ + .replacement = (void *)(replacement_fn), \ .offset = off, \ } +// clang-format on // This API implements runtime patching of existing LSM hook slots. It is a // workaround for out-of-tree modules, not the normal LSM registration path via diff --git a/kernel/hook/lsm_hooks.c b/kernel/hook/lsm_hooks.c index e07ed438c..16088e643 100644 --- a/kernel/hook/lsm_hooks.c +++ b/kernel/hook/lsm_hooks.c @@ -68,6 +68,21 @@ static int ksu_inode_rename(struct inode *old_inode, struct dentry *old_dentry, return 0; } +#ifdef KSU_COMPAT_NO_POST_EXECVE_HOOK +#include +#include "feature/sucompat.h" + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 7, 0) || \ + defined(KSU_COMPAT_CONSTIFY_BPRM_PARAMETER_IN_SECURITY_BPRM_COMMITTED_CREDS) +static void ksu_handle_bprm_committed_creds(const struct linux_binprm *bprm) +#else +static void ksu_handle_bprm_committed_creds(struct linux_binprm *bprm) +#endif +{ + ksu_handle_post_execve(NULL, NULL, NULL, NULL, NULL, NULL); +} +#endif + #if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 2, 0) || defined(KSU_COMPAT_HAS_LIST_OF_LSM_HOOKS) #include @@ -80,6 +95,10 @@ static struct security_hook_list ksu_hooks[] = { #ifdef CONFIG_KSU_MANUAL_HOOK_AUTO_INITRC_HOOK LSM_HOOK_INIT(file_permission, ksu_file_permission), #endif + +#ifdef KSU_COMPAT_NO_POST_EXECVE_HOOK + LSM_HOOK_INIT(bprm_committed_creds, ksu_handle_bprm_committed_creds), +#endif }; void __init ksu_lsm_hook_built_in_init(void) @@ -112,6 +131,12 @@ void __init ksu_lsm_hook_built_in_init(void) #define IF_CONFIG_KSU_MANUAL_HOOK_AUTO_INITRC_HOOK(x) #endif +#ifdef KSU_COMPAT_NO_POST_EXECVE_HOOK +#define IF_KSU_COMPAT_NO_POST_EXECVE_HOOK(x) x +#else +#define IF_KSU_COMPAT_NO_POST_EXECVE_HOOK(x) +#endif + #define LSM_HOOK_LIST(HOOK_ITEM) \ HOOK_ITEM(inode_rename, ksu_inode_rename, \ (struct inode * old_inode, struct dentry * old_dentry, struct inode * new_inode, \ @@ -121,7 +146,9 @@ void __init ksu_lsm_hook_built_in_init(void) (struct cred * new, const struct cred *old, int flags), \ (new, old, flags))) \ IF_CONFIG_KSU_MANUAL_HOOK_AUTO_INITRC_HOOK( \ - HOOK_ITEM(file_permission, ksu_file_permission, (struct file * file, int mask), (file, mask))) + HOOK_ITEM(file_permission, ksu_file_permission, (struct file * file, int mask), (file, mask))) \ + IF_KSU_COMPAT_NO_POST_EXECVE_HOOK( \ + HOOK_ITEM(bprm_committed_creds, ksu_handle_bprm_committed_creds, (struct linux_binprm * bprm), (bprm))) #define STRIP_PARENS(...) __VA_ARGS__ diff --git a/kernel/infra/file_wrapper.c b/kernel/infra/file_wrapper.c index 8fa9ee483..710eebfc6 100644 --- a/kernel/infra/file_wrapper.c +++ b/kernel/infra/file_wrapper.c @@ -17,6 +17,7 @@ #include "objsec.h" +#include "ksu.h" #include "klog.h" // IWYU pragma: keep #include "selinux/selinux.h" #include "runtime/ksud_boot.h" @@ -584,6 +585,8 @@ struct file *ksu_anon_inode_create_getfile_compat(const char *name, const struct int ksu_install_file_wrapper(int fd) { int out_fd, ret; + const struct cred *old_cred; + struct file *wrapper_file; struct file *orig_file = fget(fd); if (!orig_file) { return -EBADF; @@ -601,8 +604,18 @@ int ksu_install_file_wrapper(int fd) goto out_put_fd; } - struct file *wrapper_file = ksu_anon_inode_create_getfile_compat("[ksu_fdwrapper]", &file_wrapper_data->ops, - file_wrapper_data, orig_file->f_flags, NULL); + /* + * security_inode_init_security_anon() checks FILE__CREATE against the + * current SELinux domain. A custom root profile may have already moved + * this task into a restricted domain (for example shell), so create the + * private wrapper inode with KernelSU's authorized credentials. The file + * is not published until the caller's credentials have been restored and + * its inode has been relabeled below. + */ + old_cred = override_creds(ksu_cred); + wrapper_file = ksu_anon_inode_create_getfile_compat("[ksu_fdwrapper]", &file_wrapper_data->ops, file_wrapper_data, + orig_file->f_flags, NULL); + revert_creds(old_cred); if (IS_ERR(wrapper_file)) { pr_err("ksu_fdwrapper: getfile failed: %ld\n", PTR_ERR(wrapper_file)); ret = PTR_ERR(wrapper_file); diff --git a/kernel/supercall/dispatch.c b/kernel/supercall/dispatch.c index 242b57116..5f997c287 100644 --- a/kernel/supercall/dispatch.c +++ b/kernel/supercall/dispatch.c @@ -1298,7 +1298,8 @@ static const struct ksu_ioctl_cmd_map ksu_ioctl_handlers[] = { .cmd = KSU_IOCTL_GET_WRAPPER_FD, .name = "GET_WRAPPER_FD", .handler = do_get_wrapper_fd, - .perm_check = manager_or_root + .perm_check = manager_or_root, + .allow_su_session = true }, { .cmd = KSU_IOCTL_MANAGE_MARK, @@ -1334,7 +1335,8 @@ static const struct ksu_ioctl_cmd_map ksu_ioctl_handlers[] = { .cmd = KSU_IOCTL_DISABLE_ESCAPE_TO_ROOT, .name = "DISABLE_ESCAPE_TO_ROOT", .handler = do_disable_escape_to_root, - .perm_check = only_root + .perm_check = only_root, + .allow_su_session = true }, // downstream begin { @@ -1376,7 +1378,7 @@ static const struct ksu_ioctl_cmd_map ksu_ioctl_handlers[] = { }; // clang-format on -long ksu_supercall_handle_ioctl(unsigned int cmd, void __user *argp) +long ksu_supercall_handle_ioctl(const struct file *filp, unsigned int cmd, void __user *argp) { int i; @@ -1387,7 +1389,8 @@ long ksu_supercall_handle_ioctl(unsigned int cmd, void __user *argp) for (i = 0; ksu_ioctl_handlers[i].handler; i++) { if (cmd == ksu_ioctl_handlers[i].cmd) { // Check permission first - if (ksu_ioctl_handlers[i].perm_check && !ksu_ioctl_handlers[i].perm_check()) { + if (ksu_ioctl_handlers[i].perm_check && !ksu_ioctl_handlers[i].perm_check() && + !(ksu_ioctl_handlers[i].allow_su_session && ksu_is_su_session_fd(filp))) { pr_warn("ksu ioctl: permission denied for cmd=0x%x uid=%d\n", cmd, ksu_get_uid_t(current_uid())); return -EPERM; } diff --git a/kernel/supercall/internal.h b/kernel/supercall/internal.h index 873c20e4e..60c25ef50 100644 --- a/kernel/supercall/internal.h +++ b/kernel/supercall/internal.h @@ -1,6 +1,7 @@ #ifndef __KSU_H_SUPERCALL_INTERNAL #define __KSU_H_SUPERCALL_INTERNAL +#include #include #include @@ -12,7 +13,7 @@ bool manager_or_root(void); bool always_allow(void); bool allowed_for_su(void); -long ksu_supercall_handle_ioctl(unsigned int cmd, void __user *argp); +long ksu_supercall_handle_ioctl(const struct file *filp, unsigned int cmd, void __user *argp); void ksu_supercall_dump_commands(void); void ksu_supercall_cleanup_state(void); diff --git a/kernel/supercall/supercall.c b/kernel/supercall/supercall.c index d01b88115..7fde216af 100644 --- a/kernel/supercall/supercall.c +++ b/kernel/supercall/supercall.c @@ -21,15 +21,22 @@ #include "arch.h" #include "klog.h" // IWYU pragma: keep +#define KSU_DRIVER_PERMISSION_SU_SESSION (1UL << 0) + +struct ksu_driver_context { + unsigned long permissions; +}; + static int anon_ksu_release(struct inode *inode, struct file *filp) { + kfree(filp->private_data); pr_info("ksu fd released\n"); return 0; } static long anon_ksu_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { - return ksu_supercall_handle_ioctl(cmd, (void __user *)arg); + return ksu_supercall_handle_ioctl(filp, cmd, (void __user *)arg); } static const struct file_operations anon_ksu_fops = { @@ -39,46 +46,64 @@ static const struct file_operations anon_ksu_fops = { .release = anon_ksu_release, }; -static void ksu_install_fd_to_user(int __user *outp) -{ - int fd = ksu_install_fd(); - pr_info("[%d] install ksu fd: %d\n", current->pid, fd); - - if (copy_to_user(outp, &fd, sizeof(fd))) { - pr_err("install ksu fd reply err\n"); - ksu_close_fd(fd); - } -} - -// Install KSU fd to current process -int ksu_install_fd(void) +static int ksu_install_fd_with_permissions(unsigned int fd_flags, unsigned long permissions) { + struct ksu_driver_context *context; struct file *filp; + const char *name; int fd; + // alloc context + context = kzalloc(sizeof(*context), GFP_KERNEL); + if (!context) + return -ENOMEM; + + context->permissions = permissions; + name = permissions & KSU_DRIVER_PERMISSION_SU_SESSION ? "[ksu_driver_su]" : "[ksu_driver]"; + // Get unused fd - fd = get_unused_fd_flags(O_CLOEXEC); + fd = get_unused_fd_flags(fd_flags); if (fd < 0) { - pr_err("ksu_install_fd: failed to get unused fd\n"); + pr_err("%s: failed to get unused fd\n", __func__); + kfree(context); return fd; } // Create anonymous inode file - filp = anon_inode_getfile("[ksu_driver]", &anon_ksu_fops, NULL, O_RDWR | O_CLOEXEC); + filp = anon_inode_getfile(name, &anon_ksu_fops, context, O_RDWR); if (IS_ERR(filp)) { - pr_err("ksu_install_fd: failed to create anon inode file\n"); + pr_err("%s: failed to create anon inode file\n", __func__); put_unused_fd(fd); + kfree(context); return PTR_ERR(filp); } // Install fd fd_install(fd, filp); - pr_info("ksu fd installed: %d for pid %d\n", fd, current->pid); + pr_info("ksu fd installed: %d, name: %s, for pid %d\n", fd, name, current->pid); return fd; } +int ksu_install_fd(void) +{ + return ksu_install_fd_with_permissions(O_CLOEXEC, 0); +} + +int ksu_install_su_fd(void) +{ + // This descriptor must be installed after the exec into ksud. + return ksu_install_fd_with_permissions(O_CLOEXEC, KSU_DRIVER_PERMISSION_SU_SESSION); +} + +bool ksu_is_su_session_fd(const struct file *filp) +{ + const struct ksu_driver_context *context = filp->private_data; + + return context && (context->permissions & KSU_DRIVER_PERMISSION_SU_SESSION); +} + #ifdef CONFIG_KSU_TOOLKIT_SUPPORT extern int ksu_try_handle_toolkit_cmd(int magic2, unsigned int cmd, void __user **arg); #endif @@ -99,7 +124,13 @@ int ksu_handle_sys_reboot(int magic1, int magic2, unsigned int cmd, void __user // Check if this is a request to install KSU fd if (magic2 == KSU_INSTALL_MAGIC2) { - ksu_install_fd_to_user((int __user *)*arg); + int fd = ksu_install_fd(); + pr_info("[%d] install ksu fd: %d\n", current->pid, fd); + + if (copy_to_user((int __user *)*arg, &fd, sizeof(fd))) { + pr_err("install ksu fd reply err\n"); + ksu_close_fd(fd); + } return 0; } diff --git a/kernel/supercall/supercall.h b/kernel/supercall/supercall.h index fbedd11f8..4728b17a1 100644 --- a/kernel/supercall/supercall.h +++ b/kernel/supercall/supercall.h @@ -1,6 +1,7 @@ #ifndef __KSU_H_SUPERCALL #define __KSU_H_SUPERCALL +#include #include #include @@ -14,10 +15,14 @@ struct ksu_ioctl_cmd_map { const char *name; ksu_ioctl_handler_t handler; ksu_perm_check_t perm_check; // Permission check function + bool allow_su_session; }; // Install KSU fd to current process int ksu_install_fd(void); +// Install a KSU fd that authorizes operations required while starting su. +int ksu_install_su_fd(void); +bool ksu_is_su_session_fd(const struct file *filp); void ksu_supercalls_init(void); void ksu_supercalls_exit(void); diff --git a/kernel/tools/kernel_compat.mk b/kernel/tools/kernel_compat.mk index 4ad0da5c5..4558a4698 100644 --- a/kernel/tools/kernel_compat.mk +++ b/kernel/tools/kernel_compat.mk @@ -301,4 +301,14 @@ $(info -- $(REPO_NAME)/compat: module.h found) ccflags-y += -DKSU_COMPAT_HAS_UAPI_MODULE_H endif +# optional hook +ifneq ($(shell grep -q "ksu_handle_post_execve" $(srctree)/fs/exec.c; echo $$?),0) +$(info -- $(REPO_NAME)/compat: ksu_handle_post_execve hook not found) +ccflags-y += -DKSU_COMPAT_NO_POST_EXECVE_HOOK +endif +# https://github.com/torvalds/linux/commit/a721f7b8c3548e943e514a957f2a37f4763b9888 +ifeq ($(shell grep -q -F "void security_bprm_committed_creds(const struct linux_binprm *bprm)" $(srctree)/security/security.c; echo $$?),0) +$(info -- $(REPO_NAME)/compat constify bprm parameter in security_bprm_committed_creds found) +ccflags-y += -DKSU_COMPAT_CONSTIFY_BPRM_PARAMETER_IN_SECURITY_BPRM_COMMITTED_CREDS +endif diff --git a/kernel/tools/susfs_compat.mk b/kernel/tools/susfs_compat.mk index a51f668fc..904bad1c2 100644 --- a/kernel/tools/susfs_compat.mk +++ b/kernel/tools/susfs_compat.mk @@ -6,3 +6,11 @@ ifeq ($(shell grep -q "ksu_selinux_hide_running" $(srctree)/security/selinux/hoo $(info -- $(REPO_NAME)/susfs_feature_check: selinux_hide manual hook found) ccflags-y += -DKSU_COMPAT_HAS_SUSFS_FEATURE_SELINUX_HIDE endif + +# susfs's dev branch currently using post_execve_hook +# but in other branch, it still direcctly call ksu_install_su_fd +# to avoid install su fd repeatedly +ifeq ($(shell grep -q "ksu_install_su_fd" $(srctree)/fs/exec.c; echo $$?),0) +$(info -- $(REPO_NAME)/compat: ksu_install_su_fd direct call found) +ccflags-y += -DKSU_COMPAT_HAS_SUSFS_INSTALL_SU_FD_DIRECT_CALL +endif diff --git a/uapi/supercall.h b/uapi/supercall.h index 4eb2dfad5..811df2dac 100644 --- a/uapi/supercall.h +++ b/uapi/supercall.h @@ -15,7 +15,8 @@ #define KSU_FULL_VERSION_STRING 255 // 2: allowlist v4 root profile flags -static const __u32 KERNEL_SU_UAPI_VERSION = 2; +// 3: scoped su-session driver fd +static const __u32 KERNEL_SU_UAPI_VERSION = 3; /* Magic numbers for reboot hook to install fd */ DEFINE_KSU_UAPI_CONST(__u32, KSU_INSTALL_MAGIC1, 0xDEADBEEF) diff --git a/userspace/ksud/Cargo.lock b/userspace/ksud/Cargo.lock index 55141b10e..54de5cac6 100644 --- a/userspace/ksud/Cargo.lock +++ b/userspace/ksud/Cargo.lock @@ -675,17 +675,6 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" -[[package]] -name = "futures-macro" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.4", -] - [[package]] name = "futures-task" version = "0.3.34" @@ -699,7 +688,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", - "futures-macro", "futures-task", "pin-project-lite", "slab", @@ -867,28 +855,6 @@ dependencies = [ "hashbrown 0.17.1", ] -[[package]] -name = "inotify" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592" -dependencies = [ - "bitflags 2.13.1", - "futures-util", - "inotify-sys", - "libc", - "tokio", -] - -[[package]] -name = "inotify-sys" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" -dependencies = [ - "libc", -] - [[package]] name = "is_executable" version = "1.0.6" @@ -1016,7 +982,6 @@ dependencies = [ "figlet-rs", "getopts", "humansize", - "inotify", "is_executable", "java-properties", "jwalk", @@ -1213,17 +1178,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "mio" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - [[package]] name = "no_std_io2" version = "0.9.4" @@ -1732,16 +1686,6 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" -[[package]] -name = "socket2" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "strsim" version = "0.11.1" @@ -1836,11 +1780,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", - "libc", - "mio", "pin-project-lite", - "socket2", - "windows-sys 0.61.2", ] [[package]] @@ -1931,12 +1871,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - [[package]] name = "wasm-bindgen" version = "0.2.122" diff --git a/userspace/ksud/Cargo.toml b/userspace/ksud/Cargo.toml index 0c70020dc..8398ea4f3 100644 --- a/userspace/ksud/Cargo.toml +++ b/userspace/ksud/Cargo.toml @@ -58,7 +58,6 @@ serde = { version = "1.0", features = ["derive"] } ksuinit = { path = "../ksuinit" } adb_client = { git = "https://github.com/Kernel-SU/adb_client" } num_enum = "0.7" -inotify = "0.11.2" prop-rs-android = { git = "https://github.com/Kernel-SU/ksu_props", rev = "6f5723105d8d4cacad31d83d343defbf032c7b33" } [target.'cfg(not(target_os = "android"))'.dependencies] diff --git a/userspace/ksud/src/android/ksucalls.rs b/userspace/ksud/src/android/ksucalls.rs index 78e5c3347..ac7e9c967 100644 --- a/userspace/ksud/src/android/ksucalls.rs +++ b/userspace/ksud/src/android/ksucalls.rs @@ -1,7 +1,7 @@ #![allow(clippy::unreadable_literal)] use anyhow::{Result, bail}; -use std::{cell::Cell, fs, os::fd::RawFd, sync::OnceLock}; +use std::{cell::Cell, fs, io, os::fd::RawFd, sync::OnceLock}; use crate::{android::uapi, defs::MountInfo}; @@ -70,31 +70,47 @@ pub fn setup_sigsys_handler() { } } +const DRIVER_FD_NAME: &str = "anon_inode:[ksu_driver]"; +const SU_DRIVER_FD_NAME: &str = "anon_inode:[ksu_driver_su]"; + // Global driver fd cache static DRIVER_FD: OnceLock = OnceLock::new(); static INFO_CACHE: OnceLock = OnceLock::new(); -fn scan_driver_fd() -> Option { - let fd_dir = fs::read_dir("/proc/self/fd").ok()?; +fn scan_driver_fd() -> io::Result> { + let fd_dir = fs::read_dir("/proc/self/fd")?; + let mut driver_fd = None; for entry in fd_dir.flatten() { if let Ok(fd_num) = entry.file_name().to_string_lossy().parse::() { let link_path = format!("/proc/self/fd/{fd_num}"); if let Ok(target) = fs::read_link(&link_path) { let target_str = target.to_string_lossy(); - if target_str.contains("[ksu_driver]") { - return Some(fd_num); + if target_str == SU_DRIVER_FD_NAME { + return Ok(Some(fd_num)); + } + if target_str == DRIVER_FD_NAME { + driver_fd = Some(fd_num); } } } } - None + Ok(driver_fd) +} + +pub fn claim_inherited_driver_fd() -> io::Result<()> { + if DRIVER_FD.get().is_none() + && let Some(fd) = scan_driver_fd()? + { + let _ = DRIVER_FD.set(fd); + } + Ok(()) } // Get cached driver fd fn init_driver_fd() -> Option { - let fd = scan_driver_fd(); + let fd = scan_driver_fd().ok().flatten(); if fd.is_none() { let mut fd = -1; with_svc_call(|| unsafe { diff --git a/userspace/ksud/src/android/su.rs b/userspace/ksud/src/android/su.rs index f21d0c52f..f504974c7 100644 --- a/userspace/ksud/src/android/su.rs +++ b/userspace/ksud/src/android/su.rs @@ -4,11 +4,12 @@ use std::{ cmp::Ordering, env, ffi::{CStr, CString}, + io, path::PathBuf, process::Command, }; -use anyhow::{Context, Ok, Result, bail}; +use anyhow::{Context, Ok, Result, anyhow, bail}; use getopts::Options; use libc::c_int; use log::error; @@ -19,6 +20,7 @@ use rustix::{ use crate::{ android::{ + ksucalls, ksucalls::{get_wrapped_fd, set_ksu_no_new_privs}, utils::{self, umask}, }, @@ -82,16 +84,27 @@ fn set_selinux_context(context: &str) -> Result<()> { fn wrap_tty(fd: c_int) { let inner_fn = move || -> Result<()> { - if unsafe { libc::isatty(fd) != 1 } { + if unsafe { libc::isatty(fd) != 1 } + && io::Error::last_os_error().raw_os_error() != Some(libc::EACCES) + { return Ok(()); } + + // The root profile is already active here, so its SELinux domain may + // return EACCES while querying the original terminal. In that case, + // check the wrapped fd instead, since that descriptor is intended to + // bypass this restriction. let new_fd = get_wrapped_fd(fd).context("get_wrapped_fd")?; - if unsafe { libc::dup2(new_fd, fd) } == -1 { - bail!("dup {new_fd} -> {fd} errno: {}", unsafe { - *libc::__errno() - }); + if unsafe { libc::isatty(new_fd) != 1 } { + unsafe { libc::close(new_fd) }; + return Ok(()); } + let dup_result = unsafe { libc::dup2(new_fd, fd) }; + let dup_errno = unsafe { *libc::__errno() }; unsafe { libc::close(new_fd) }; + if dup_result == -1 { + bail!("dup {new_fd} -> {fd} errno: {dup_errno}"); + } Ok(()) }; @@ -102,9 +115,13 @@ fn wrap_tty(fd: c_int) { #[allow(clippy::similar_names)] pub fn root_shell() -> Result<()> { - // we are root now, this was set in kernel! + // The kernel has already applied the selected root profile. + + // A su-session driver fd deliberately survives the exec into ksud. Claim + // it before handling any arguments and restore FD_CLOEXEC so it cannot + // leak into the target shell, including when fd wrapping is disabled. + ksucalls::claim_inherited_driver_fd().context("claim inherited KernelSU driver fd")?; - use anyhow::anyhow; let env_args: Vec = env::args().collect(); let program = env_args[0].clone(); let mut executable: Option = None; diff --git a/userspace/ksud/src/android/unload.rs b/userspace/ksud/src/android/unload.rs index a4bff5120..1f405fae4 100644 --- a/userspace/ksud/src/android/unload.rs +++ b/userspace/ksud/src/android/unload.rs @@ -64,7 +64,7 @@ fn find_ksu_fd_holders() -> Vec { let link_path = fd_entry.path(); if let Ok(target) = fs::read_link(&link_path) { let target_str = target.to_string_lossy(); - if target_str.contains("[ksu_driver]") || target_str.contains("[ksu_fdwrapper]") { + if target_str.contains("[ksu_driver") || target_str.contains("[ksu_fdwrapper]") { pids.push(pid); break; } @@ -95,7 +95,7 @@ fn close_ksu_fds() { }; if let Ok(target) = fs::read_link(entry.path()) { let target_str = target.to_string_lossy(); - if target_str.contains("[ksu_driver]") || target_str.contains("[ksu_fdwrapper]") { + if target_str.contains("[ksu_driver") || target_str.contains("[ksu_fdwrapper]") { info!("unload: closing fd {fd} -> {target_str}"); unsafe { libc::close(fd); From ff765f8507541ed2c959f331ad23e4650cdfbd83 Mon Sep 17 00:00:00 2001 From: 5ec1cff <56485584+5ec1cff@users.noreply.github.com> Date: Mon, 7 Sep 2026 11:02:54 +0800 Subject: [PATCH 05/34] feat: new version matching detection mechanism (https://github.com/tiann/KernelSU/pull/3516) KernelSU's version checking mechanism requires users to always use the corresponding manager, kernel, and ksud. If a version mismatch occurs, a banner warning will appear at the top of the manager, reminding the user to update. This is to ensure that updating one component will also update the others for proper functionality. However, this can be confusing in some situations because certain kernels are not yet compatible with the official LKM. Users may need to compile and add commits themselves while still using the official manager, leading to a version mismatch between the compiled LKM and the manager, resulting in a warning that is actually unnecessary. To address this, this commit improves the version checking mechanism. Now, the manager will only issue a warning and force the user to update the LKM if the UAPI version is mismatched. Furthermore, to ensure that users using the manager's built-in LKM receive update prompts correctly, a bundled lkm flag has been introduced. If a user uses the built-in LKM, an update prompt will be issued if the version is incompatible. However, if using an external LKM, this prompt will not be received; instead, the word "Custom" will be displayed in the version field, indicating that the user needs to maintain the functionality's availability. Finally, the text and interaction logic of update prompts and warnings have been optimized, simplifying the descriptions and allowing users to click to jump to the LKM installation page. --------- [cherry-picked from upstream commit https://github.com/tiann/KernelSU/commit/e727186054b6a6b39b9f4262037086202eed8365] Downstream changes: Skip `Custom` badge, because i don't like it Co-authored-by: YuKongA <70465933+YuKongA@users.noreply.github.com> Signed-off-by: AlexLiuDev233 --- kernel/core/init.c | 5 + kernel/include/ksu.h | 3 + kernel/supercall/dispatch.c | 6 ++ manager/app/build.gradle.kts | 2 - manager/app/src/main/cpp/jni.c | 4 + manager/app/src/main/cpp/ksu.c | 6 ++ manager/app/src/main/cpp/ksu.h | 4 + .../java/com/resukisu/resukisu/Natives.kt | 11 +-- .../ApplicationControlRepository.kt | 4 +- .../resukisu/data/kernel/KernelRepository.kt | 11 +-- .../resukisu/domain/model/KernelState.kt | 8 +- .../resukisu/ui/component/KsuIsValidCheck.kt | 2 +- .../resukisu/ui/screen/main/HomePage.kt | 91 ++++++++++--------- .../resukisu/ui/screen/main/MainScreen.kt | 4 +- .../resukisu/ui/screen/main/SettingsPage.kt | 4 +- .../app/src/main/res/values-fr/strings.xml | 2 - .../app/src/main/res/values-hu/strings.xml | 2 - .../app/src/main/res/values-in/strings.xml | 2 - .../app/src/main/res/values-pl/strings.xml | 2 - .../src/main/res/values-pt-rBR/strings.xml | 2 - .../app/src/main/res/values-ru/strings.xml | 2 - .../app/src/main/res/values-tr/strings.xml | 2 - .../app/src/main/res/values-uk/strings.xml | 2 - .../app/src/main/res/values-vi/strings.xml | 2 - .../src/main/res/values-zh-rCN/strings.xml | 5 +- .../src/main/res/values-zh-rHK/strings.xml | 2 - .../src/main/res/values-zh-rTW/strings.xml | 2 - manager/app/src/main/res/values/strings.xml | 5 +- uapi/supercall.h | 4 +- userspace/ksud/src/android/cli.rs | 4 + userspace/ksud/src/android/ksucalls.rs | 2 + userspace/ksud/src/android/late_load/mod.rs | 1 + userspace/ksud/src/boot_patch.rs | 6 ++ 33 files changed, 115 insertions(+), 99 deletions(-) diff --git a/kernel/core/init.c b/kernel/core/init.c index 9be1c15b5..4927a2cd1 100644 --- a/kernel/core/init.c +++ b/kernel/core/init.c @@ -155,6 +155,11 @@ bool allow_shell = false; bool ksu_no_custom_rc = false; module_param_named(norc, ksu_no_custom_rc, bool, 0); +#ifdef MODULE +bool ksu_bundled = false; +module_param_named(bundled, ksu_bundled, bool, 0); +#endif + char ksu_block_modules[256]; module_param_string(block_modules, ksu_block_modules, sizeof(ksu_block_modules), 0); MODULE_PARM_DESC(block_modules, "Comma-separated preset module names to acknowledge without loading"); diff --git a/kernel/include/ksu.h b/kernel/include/ksu.h index fc62f9f9f..85e23ac22 100644 --- a/kernel/include/ksu.h +++ b/kernel/include/ksu.h @@ -11,6 +11,9 @@ extern struct cred *ksu_cred; extern bool ksu_late_loaded; extern bool allow_shell; +#ifdef MODULE +extern bool ksu_bundled; +#endif extern bool ksu_no_custom_rc; #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0) || defined(KSU_COMPAT_HAS_SELINUX_POLICY_STRUCT) diff --git a/kernel/supercall/dispatch.c b/kernel/supercall/dispatch.c index 5f997c287..1edc0640d 100644 --- a/kernel/supercall/dispatch.c +++ b/kernel/supercall/dispatch.c @@ -66,6 +66,9 @@ static int do_get_info(void __user *arg) #ifdef MODULE cmd.flags |= KSU_GET_INFO_FLAG_LKM; + if (ksu_bundled) { + cmd.flags |= KSU_GET_INFO_FLAG_BUNDLED; + } #endif #ifdef EXPECTED_PR_BUILD_SIZE cmd.flags |= KSU_GET_INFO_FLAG_PR_BUILD; @@ -101,6 +104,9 @@ static int do_get_info_legacy(void __user *arg) #ifdef MODULE cmd.flags |= KSU_GET_INFO_FLAG_LKM; + if (ksu_bundled) { + cmd.flags |= KSU_GET_INFO_FLAG_BUNDLED; + } #endif if (is_manager()) { diff --git a/manager/app/build.gradle.kts b/manager/app/build.gradle.kts index b4c882056..d54200a83 100644 --- a/manager/app/build.gradle.kts +++ b/manager/app/build.gradle.kts @@ -255,7 +255,5 @@ dependencies { implementation(libs.lsposed.cxx) - implementation(libs.com.github.topjohnwu.libsu.core) - implementation(libs.accompanist.drawablepainter) } diff --git a/manager/app/src/main/cpp/jni.c b/manager/app/src/main/cpp/jni.c index d7f710c9f..79e26bd1f 100644 --- a/manager/app/src/main/cpp/jni.c +++ b/manager/app/src/main/cpp/jni.c @@ -243,6 +243,10 @@ NativeBridgeNP(isPrBuild, jboolean) { return is_pr_build(); } +NativeBridgeNP(isLkmBundled, jboolean) { + return is_lkm_bundled(); +} + NativeBridgeNP(isLateLoadMode, jboolean) { return is_late_load_mode(); } diff --git a/manager/app/src/main/cpp/ksu.c b/manager/app/src/main/cpp/ksu.c index 4dbe60679..fc7c8d4c8 100644 --- a/manager/app/src/main/cpp/ksu.c +++ b/manager/app/src/main/cpp/ksu.c @@ -145,6 +145,12 @@ bool is_late_load_mode() { return false; } +bool is_lkm_bundled() { + auto info = get_info(); + return (info.flags & KSU_GET_INFO_FLAG_LKM) != 0 && + (info.flags & KSU_GET_INFO_FLAG_BUNDLED) != 0; +} + bool is_pr_build() { auto info = get_info(); if (info.version > 0) { diff --git a/manager/app/src/main/cpp/ksu.h b/manager/app/src/main/cpp/ksu.h index c93ff2648..8ee126fa0 100644 --- a/manager/app/src/main/cpp/ksu.h +++ b/manager/app/src/main/cpp/ksu.h @@ -27,7 +27,11 @@ bool is_safe_mode(); bool is_lkm_mode(); bool is_manager(); + bool is_late_load_mode(); + +bool is_lkm_bundled(); + bool is_pr_build(); void get_full_version(char* buff); diff --git a/manager/app/src/main/java/com/resukisu/resukisu/Natives.kt b/manager/app/src/main/java/com/resukisu/resukisu/Natives.kt index ed264316d..b8f68feaf 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/Natives.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/Natives.kt @@ -57,6 +57,9 @@ object Natives { val isLkmMode: Boolean external get + val isLkmBundled: Boolean + external get + val isLateLoadMode: Boolean external get @@ -197,12 +200,8 @@ object Natives { val managerUAPIVersion: Int external get - fun checkUAPIMismatch(): Boolean { - return kernelUAPIVersion != managerUAPIVersion - } - - fun requireNewKernel(): Boolean { - return (version != -1 && version < MINIMAL_SUPPORTED_KERNEL) || checkUAPIMismatch() + fun isFullFeatured(): Boolean { + return isManager && kernelUAPIVersion == managerUAPIVersion } @Immutable diff --git a/manager/app/src/main/java/com/resukisu/resukisu/data/application/ApplicationControlRepository.kt b/manager/app/src/main/java/com/resukisu/resukisu/data/application/ApplicationControlRepository.kt index 1a9b76700..39050b0c3 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/data/application/ApplicationControlRepository.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/data/application/ApplicationControlRepository.kt @@ -10,7 +10,9 @@ class ApplicationControlRepository( ) { suspend fun ensureManagerInstalled(): Result = withContext(Dispatchers.IO) { runCatching { - if (Natives.isManager && !Natives.requireNewKernel()) ksuCliRepository.install() + if (Natives.isFullFeatured() && ksuCliRepository.rootAvailable()) { + ksuCliRepository.install() + } } } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/data/kernel/KernelRepository.kt b/manager/app/src/main/java/com/resukisu/resukisu/data/kernel/KernelRepository.kt index db811e5e3..951370883 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/data/kernel/KernelRepository.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/data/kernel/KernelRepository.kt @@ -24,6 +24,7 @@ class KernelRepository( val kernelUapi = if (isManager) Natives.kernelUAPIVersion else null val managerUapi = runCatching { Natives.managerUAPIVersion }.getOrDefault(1) val fullVersion = runCatching { Natives.getFullVersion() }.getOrDefault("Unknown") + val isRootAvailable = runCatching { ksuCliRepository.rootAvailable() }.getOrDefault(false) KernelStatus( isManager = isManager, ksuVersion = ksuVersion, @@ -32,13 +33,9 @@ class KernelRepository( ksuFullVersion = "$fullVersion (${Natives.version}/$kernelUapi)", lkmMode = ksuVersion?.let { if (kernelVersion.isGKI()) Natives.isLkmMode else null }, kernelVersion = kernelVersion, - isRootAvailable = runCatching { ksuCliRepository.rootAvailable() }.getOrDefault(false), - requireNewKernel = runCatching { isManager && Natives.requireNewKernel() }.getOrDefault( - false - ), - uapiMismatch = runCatching { isManager && Natives.checkUAPIMismatch() }.getOrDefault( - false - ), + isRootAvailable = isRootAvailable, + isFullFeatured = isRootAvailable && runCatching { Natives.isFullFeatured() } + .getOrDefault(false), isSELinuxPermissive = runCatching { isSELinuxPermissive() }.getOrDefault(false), isOfficialSignature = runCatching { ksuCliRepository.isOfficialSignature(application.packageResourcePath) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/KernelState.kt b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/KernelState.kt index b3092f2c5..3d20dadfb 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/KernelState.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/KernelState.kt @@ -22,8 +22,7 @@ data class KernelStatus( val lkmMode: Boolean? = null, val kernelVersion: KernelVersion, val isRootAvailable: Boolean = false, - val requireNewKernel: Boolean = false, - val uapiMismatch: Boolean = false, + val isFullFeatured: Boolean = false, val isSELinuxPermissive: Boolean = false, val isOfficialSignature: Boolean = true, val kernelPatchImplementation: KernelPatchImplementation = KernelPatchImplementation.NONE, @@ -31,10 +30,7 @@ data class KernelStatus( val isSafeMode: Boolean = false, val isLateLoadMode: Boolean = false, val isPrBuild: Boolean = false, -) { - val isValid: Boolean - get() = isManager && !requireNewKernel && isRootAvailable -} +) data class KernelFeatureSettings( val suEnabled: Boolean, diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/KsuIsValidCheck.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/KsuIsValidCheck.kt index c58e32ca3..7df22f01d 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/KsuIsValidCheck.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/KsuIsValidCheck.kt @@ -8,6 +8,6 @@ inline fun KsuIsValid( status: KernelStatus, content: @Composable () -> Unit ) { - if (status.isValid) + if (status.isFullFeatured) content() } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/HomePage.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/HomePage.kt index be02ba5bf..cf03690db 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/HomePage.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/HomePage.kt @@ -202,14 +202,10 @@ fun HomePage( ) { // 状态卡片 if (uiState.isCoreDataLoaded) { - if (uiState.systemStatus.requireNewKernel) { + if (uiState.systemStatus.isManager && !uiState.systemStatus.isFullFeatured) { if ((uiState.systemStatus.ksuVersion ?: 0) > BuildConfig.VERSION_CODE) { WarningCard( - message = stringResource( - id = R.string.require_manager_version, - BuildConfig.VERSION_CODE, - uiState.systemStatus.ksuVersion ?: 0 - ), + message = stringResource(R.string.require_manager_version), icon = { Icon( imageVector = Icons.TwoTone.Error, @@ -217,15 +213,17 @@ fun HomePage( tint = MaterialTheme.colorScheme.onErrorContainer, modifier = Modifier.size(18.dp) ) + }, + onClick = { + navigator.push(Route.Install(preselectedKernelUri = null)) } ) } else { WarningCard( - message = stringResource( - id = R.string.require_kernel_version, - uiState.systemStatus.ksuVersion ?: 0, - BuildConfig.VERSION_CODE - ), + message = if (uiState.systemStatus.lkmMode == true) + stringResource(R.string.require_kernel_version) + else + stringResource(R.string.require_kernel_version_gki), icon = { Icon( imageVector = Icons.TwoTone.Error, @@ -233,6 +231,9 @@ fun HomePage( tint = MaterialTheme.colorScheme.onErrorContainer, modifier = Modifier.size(18.dp) ) + }, + onClick = { + navigator.push(Route.Install(preselectedKernelUri = null)) } ) } @@ -347,11 +348,8 @@ fun HomePage( ) } Spacer(modifier = Modifier.height(10.dp)) - ManagerUpdateCard(uiState.stableManagerUpdate) - Spacer(modifier = Modifier.height(10.dp)) ManagerUpdateCard(uiState.betaManagerUpdate) - Spacer(modifier = Modifier.height(10.dp)) if (uiState.isBetaManagerUpdateCheckFailed) { WarningCard( message = stringResource(R.string.beta_update_check_failed), @@ -466,6 +464,8 @@ private fun ManagerUpdateCardContent(updateInfo: ManagerUpdateInfo) { ) } ) + + Spacer(modifier = Modifier.height(10.dp)) } @OptIn(ExperimentalMaterial3ExpressiveApi::class) @@ -533,38 +533,39 @@ private fun TopBar( // 重启按钮 var showDropdown by remember { mutableStateOf(false) } - KsuIsValid(uiState.systemStatus) { - IconButton(onClick = { - showDropdown = true - }) { - Icon( - imageVector = Icons.TwoTone.PowerSettingsNew, - contentDescription = stringResource(id = R.string.reboot) - ) - - DropdownMenuPopup(expanded = showDropdown, onDismissRequest = { - showDropdown = false + KsuIsValid(uiState.systemStatus) { -> if (uiState.systemStatus.isRootAvailable) { + IconButton(onClick = { + showDropdown = true }) { - DropdownMenuGroup( - shapes = MenuDefaults.groupShapes() - ) { - val pm = - LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager? - var methods = mapOf( - R.string.reboot to "", - R.string.reboot_soft to "soft_reboot", - R.string.reboot_recovery to "recovery", - R.string.reboot_bootloader to "bootloader", - R.string.reboot_download to "download", - R.string.reboot_edl to "edl" - ) + Icon( + imageVector = Icons.TwoTone.PowerSettingsNew, + contentDescription = stringResource(id = R.string.reboot) + ) - @Suppress("DEPRECATION") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) { - methods = methods + (R.string.reboot_userspace to "userspace") - } + DropdownMenuPopup(expanded = showDropdown, onDismissRequest = { + showDropdown = false + }) { + DropdownMenuGroup( + shapes = MenuDefaults.groupShapes() + ) { + val pm = + LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager? + var methods = mapOf( + R.string.reboot to "", + R.string.reboot_soft to "soft_reboot", + R.string.reboot_recovery to "recovery", + R.string.reboot_bootloader to "bootloader", + R.string.reboot_download to "download", + R.string.reboot_edl to "edl" + ) - RebootDropdownItems(methods, onReboot) + @Suppress("DEPRECATION") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) { + methods = methods + (R.string.reboot_userspace to "userspace") + } + + RebootDropdownItems(methods, onReboot) + } } } } @@ -765,7 +766,7 @@ private fun InfoCard( item( - visible = systemStatus.isValid + visible = systemStatus.isManager ) { SettingsBaseWidget( iconPlaceholder = false, @@ -855,7 +856,7 @@ private fun InfoCard( } item( - visible = !isSimpleMode && systemStatus.isValid + visible = !isSimpleMode && systemStatus.isFullFeatured ) { SettingsBaseWidget( iconPlaceholder = false, diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/MainScreen.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/MainScreen.kt index e92b156f7..ea27d7e22 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/MainScreen.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/MainScreen.kt @@ -53,8 +53,8 @@ fun MainScreen() { val themeConfig: ThemeConfig = koinInject() val homeViewModel = koinViewModel() val homeState by homeViewModel.state.collectAsStateWithLifecycle() - val pages = remember(homeState.systemStatus.isValid) { - BottomBarDestination.getPages(homeState.systemStatus.isValid) + val pages = remember(homeState.systemStatus.isFullFeatured) { + BottomBarDestination.getPages(homeState.systemStatus.isFullFeatured) } val coroutineScope = rememberCoroutineScope() diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt index 7251e035e..73dbacba5 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt @@ -188,7 +188,7 @@ fun SettingsPage(bottomPadding: Dp) { ) ) { // 配置卡片 - if (homeState.systemStatus.isValid) { + if (homeState.systemStatus.isFullFeatured) { item { val modeItems = listOf( stringResource(id = R.string.settings_mode_default), @@ -448,7 +448,7 @@ fun SettingsPage(bottomPadding: Dp) { ) {} } - if (homeState.systemStatus.isValid) { + if (homeState.systemStatus.isFullFeatured) { item { SettingsJumpPageWidget( icon = Icons.TwoTone.Security, diff --git a/manager/app/src/main/res/values-fr/strings.xml b/manager/app/src/main/res/values-fr/strings.xml index 2d19f3b50..7b63f9067 100644 --- a/manager/app/src/main/res/values-fr/strings.xml +++ b/manager/app/src/main/res/values-fr/strings.xml @@ -432,8 +432,6 @@ Effacement de la configuration du gestionnaire dynamique Effacer la configuration du gestionnaire dynamique ? Gestionnaires dynamiques - La version installée du gestionnaire KernelSU (%1$d) est trop ancienne pour que KernelSU fonctionne correctement. La mise à jour du gestionnaire en version %2$d ou ultérieure est nécessaire ! - Version de KernelSU (%1$d) obsolète pour garantir un fonctionnement correct du gestionnaire. Mise à jour en version %2$d ou ultérieure requise ! Gestion des chemins de démontage Gère les chemins de démontage du noyau Aucun chemin de démontage existant diff --git a/manager/app/src/main/res/values-hu/strings.xml b/manager/app/src/main/res/values-hu/strings.xml index fb2f74f1c..07bc657de 100644 --- a/manager/app/src/main/res/values-hu/strings.xml +++ b/manager/app/src/main/res/values-hu/strings.xml @@ -353,8 +353,6 @@ Fejlesztő(k) Leírás Támogatott készülékek - Úgy tűnik a root menedzser verzió %1$d túl alacsony. Jelen esetben a KernelSU implementáció megfelelő működéséhez minimum %2$d verzió szükséges! - Úgy tűnik a kernelbe telepített KernelSU implementáció verzió %1$d túl alacsony. Jelen esetben a root menedzser megfelelő működéséhez minimum %2$d verzió szükséges! Leválasztandó elérési útvonalak Fájlrendszer elérési útvonalak kezelése a leválasztáshoz Nincs megadva elérési útvonal diff --git a/manager/app/src/main/res/values-in/strings.xml b/manager/app/src/main/res/values-in/strings.xml index 55c3cbd15..aa7be8956 100644 --- a/manager/app/src/main/res/values-in/strings.xml +++ b/manager/app/src/main/res/values-in/strings.xml @@ -362,8 +362,6 @@ Sembunyikan jalur asli dari pemetaan memori di /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Catatan: Fitur ini tidak mendukung penyembunyian pemetaan memori anonim, serta tidak dapat menyembunyikan inline hook atau PLT hook yang disebabkan oleh library yang diinjeksi itu sendiri Pemberitahuan Penting: Untuk aplikasi dengan mekanisme deteksi injeksi yang dirancang dengan baik, fitur ini mungkin tidak akan efektif untuk melewati deteksi Pertama, temukan PID dan UID aplikasi target menggunakan ps -enf, lalu cek jalur yang relevan di /proc//maps dan bandingkan nomor perangkat dengan yang ada di /proc/1/mountinfo untuk memastikan konsistensi. Fungsi penyembunyian pemetaan memori hanya dapat bekerja dengan benar jika nomor perangkat tersebut cocok - Versi manajer (%1$d) terlalu rendah. Perbarui ke versi %2$d atau lebih tinggi agar KernelSU berjalan normal! - Versi KernelSU (%1$d) terlalu rendah. Perbarui ke versi %2$d atau lebih tinggi agar manajer berjalan normal! Manajemen Jalur Umount Kelola jalur unmount kernel Tidak ada jalur umount yang ditemukan diff --git a/manager/app/src/main/res/values-pl/strings.xml b/manager/app/src/main/res/values-pl/strings.xml index 6e0db262e..abb6035b6 100644 --- a/manager/app/src/main/res/values-pl/strings.xml +++ b/manager/app/src/main/res/values-pl/strings.xml @@ -367,8 +367,6 @@ Ukrywa prawdziwe ścieżki plików map pamięci przed /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Ta funkcja nie wspiera ukrywania anonimowy map pamięci, ani nie może ukryć inline hooków lub PLT haków wywołanych przez wstrzykniętą bibliotekę Dla aplikacji z dobrą implementacją mechanizmu wykrywania wstrzyknięć ta funkcja może nie być skuteczna Najpierw znajdź PID oraz UID wybranej aplikacji używając ps -enf, a następnie sprawdź znaczące ścieżki w /proc/<pod>/maps i porównaj liczby urządzenia z tymi, które znajdują się w /proc/1/mountinfo. Funkcja może działać poprawnie tylko, gdy te liczby się zgadzają - Aktualna wersja menadżera KernelSU %1$d jest zbyt niska aby KernelSU działało poprawnie. Zaktualizuj menadżer do wersji %2$d lub wyższej! - Aktualna wersja KernelSU %1$d jest zbyt niska aby menadżer działał poprawnie. Zaktualizuj KernelSU do wersji %2$d lub wyższej! Zarządzanie ścieżkami odmontowywania Zarządzaj ścieżkami odmontowywania przez jądra Brak ścieżek diff --git a/manager/app/src/main/res/values-pt-rBR/strings.xml b/manager/app/src/main/res/values-pt-rBR/strings.xml index 1103b724d..736f319c0 100644 --- a/manager/app/src/main/res/values-pt-rBR/strings.xml +++ b/manager/app/src/main/res/values-pt-rBR/strings.xml @@ -373,8 +373,6 @@ Oculta os caminhos reais dos mapeamentos de memória em /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Observação: este recurso não suporta a ocultação de mapeamentos de memória anônimos, nem pode ocultar hooks embutidos ou hooks PLT causados pela própria biblioteca injetada Aviso importante: Para aplicações com mecanismos de detecção de injeção bem implementados, este recurso pode não ser eficaz para contornar a detecção Primeiro, encontre o PID e o UID do aplicativo de destino usando o comando `ps -enf`. Em seguida, verifique os caminhos relevantes em `/proc/<pid>/maps` e compare os números dos dispositivos com os de `/proc/1/mountinfo` para garantir a consistência. A função de ocultação do mapa só funcionará corretamente se os números dos dispositivos coincidirem - A versão atual do gerenciador KernelSU, %1$d, é muito antiga para o funcionamento correto do KernelSU. Atualize o gerenciador para a versão %2$d ou superior! - A versão atual do KernelSU, %1$d, é muito antiga para o gerenciador funcionar corretamente. Atualize para a versão %2$d ou superior! Gestão de Caminhos de Umount Gerenciar caminhos de desmontagem do kernel Não existem caminhos de desmontagem diff --git a/manager/app/src/main/res/values-ru/strings.xml b/manager/app/src/main/res/values-ru/strings.xml index 78e589416..31d765450 100644 --- a/manager/app/src/main/res/values-ru/strings.xml +++ b/manager/app/src/main/res/values-ru/strings.xml @@ -428,8 +428,6 @@ Стиль палитры Спецификация цвета AOSP стиль - Текущая версия %1$d KernelSU слишком низкая для корректной работы. Пожалуйста обновите менеджер до %2$d и выше - Текущая версия %1$d KernelSU слишком низкая для корректной работы. Обновите пожалуйста менеджер до версии %2$d и выше Ридми Статус Стандарт diff --git a/manager/app/src/main/res/values-tr/strings.xml b/manager/app/src/main/res/values-tr/strings.xml index 4312a8faa..2ef25932e 100644 --- a/manager/app/src/main/res/values-tr/strings.xml +++ b/manager/app/src/main/res/values-tr/strings.xml @@ -400,8 +400,6 @@ KernelSU Klasik Hareketi Takip Et Çekirdek - KernelSU yöneticisinin mevcut sürümü %1$d, bu KernelSU\'nun düzgün çalışması için çok düşük. Lütfen yöneticiyi %2$d veya daha yüksek bir sürüme yükseltin! - Mevcut KernelSU sürümü %1$d yöneticinin düzgün çalışması için çok düşük. Lütfen %2$d veya daha yüksek bir sürüme yükseltin! Modül İndirmeleri İndiriliyor %s İndirildi diff --git a/manager/app/src/main/res/values-uk/strings.xml b/manager/app/src/main/res/values-uk/strings.xml index 42cb578b3..22468c7b0 100644 --- a/manager/app/src/main/res/values-uk/strings.xml +++ b/manager/app/src/main/res/values-uk/strings.xml @@ -367,8 +367,6 @@ Приховати реальні шляхи до файлів відображень пам\'яті у /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Зверніть увагу: ця функція не підтримує приховання анонімних відображень пам\'яті та не може приховати inline-хуки або PLT-хуки, спричинені самою впровадженою бібліотекою Важливе попередження: для додатків з добре реалізованими механізмами виявлення впровадження ця функція може не ефективно обійти виявлення Спочатку знайдіть PID та UID цільового додатка за допомогою ps -enf, потім перевірте відповідні шляхи в /proc/<pid>/maps та порівняйте номери пристроїв із /proc/1/mountinfo для забезпечення відповідності. Тільки при збігу номерів пристроїв функція приховання map працюватиме правильно - Поточна версія менеджера KernelSU %1$d є занадто низькою для коректної роботи KernelSU. Будь ласка, оновіть менеджер до версії %2$d або вище! - Поточна версія KernelSU %1$d є занадто низькою для коректної роботи менеджера. Будь ласка, оновіть до версії %2$d або вище! Керування шляхами розмонтування Керувати шляхами розмонтування ядра Шляхи розмонтування відсутні diff --git a/manager/app/src/main/res/values-vi/strings.xml b/manager/app/src/main/res/values-vi/strings.xml index 2082e5090..151a9cb62 100644 --- a/manager/app/src/main/res/values-vi/strings.xml +++ b/manager/app/src/main/res/values-vi/strings.xml @@ -362,8 +362,6 @@ Tìm module Module Kernel - Phiên bản %1$d của trình quản lý KernelSU đã quá lỗi thời để KernelSU hoạt động đúng cách. Hãy nâng cấp trình quản lý lên phiên bản %2$d hoặc cao hơn! - Phiên bản %1$d của KernelSU đã quá cũ để trình quản lý có thể hoạt động bình thường. Hãy cập nhật lên phiên bản %2$d hoặc cao hơn! Không có đường dẫn umount nào tồn tại 0=Unmount bình thường, 2=MNT_DETACH Tất cả các thay đổi sẽ có hiệu lực ngay lập tức. diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index 7775230ca..b932ff0e7 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -374,8 +374,9 @@ 从 /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap] 中隐藏内存映射的真实文件路径。请注意:此功能不支持隐藏匿名内存映射,也无法隐藏由注入库本身产生的内联钩子或 PLT 钩子 重要提示:对于具备完善注入检测机制的应用,此功能可能无法有效绕过检测 首先通过 ps -enf 查找目标应用的 PID 和 UID,然后检查 /proc/<pid>/maps 中的相关路径,并与 /proc/1/mountinfo 中的设备号进行比对以确保一致性。只有当设备号一致时,隐藏映射才能正常工作 - 当前 KernelSU 管理器版本 %1$d 过低,KernelSU 无法正常工作. 请将 KernelSU 管理器版本升级至 %2$d 或以上! - 当前 KernelSU 版本 %1$d 过低,管理器无法正常工作,请将内核 KernelSU 版本升级至 %2$d 或以上! + 需要更新管理器 + 需要更新内核,点击此处安装 + 需要更新内核 Umount 路径管理 管理内核卸载路径 没有任何内核卸载路径 diff --git a/manager/app/src/main/res/values-zh-rHK/strings.xml b/manager/app/src/main/res/values-zh-rHK/strings.xml index 50740a9e0..fd58f2307 100644 --- a/manager/app/src/main/res/values-zh-rHK/strings.xml +++ b/manager/app/src/main/res/values-zh-rHK/strings.xml @@ -417,8 +417,6 @@ 跟隨手勢 固定靠右 固定靠左 - 當前 KernelSU 管理器版本 %1$d 過低,無法正常運作,請升級至 %2$d 或更高版本! - 當前 KernelSU 版本 %1$d 過低,管理器無法正常運作,請升級至 %2$d 或更高版本! 模組下載 正在下載 %s 下載完成 diff --git a/manager/app/src/main/res/values-zh-rTW/strings.xml b/manager/app/src/main/res/values-zh-rTW/strings.xml index e64612863..fcb1b5328 100644 --- a/manager/app/src/main/res/values-zh-rTW/strings.xml +++ b/manager/app/src/main/res/values-zh-rTW/strings.xml @@ -361,8 +361,6 @@ 從 /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap] 中隱藏記憶體映射的真實檔案路徑。請注意:此功能不支援隱藏「匿名記憶體映射」,也無法隱藏由程式庫本身注入產生的內聯掛鉤或 PLT 掛鉤 重要提示:對於具備完整注入偵測機制的應用程式,此功能可能無法有效繞過偵測 首先透過 ps -enf 搜尋目標應用程式的 PID 和 UID,然後檢查 /proc/<pid>/maps 中的相關路徑,並與 /proc/1/mountinfo 中的裝置號碼進行比對確保一致性。只有當裝置號碼一致時,隱藏映射才能正常運作 - 目前 KernelSU 管理器版本 %1$d 過低,KernelSU 無法正常運作。請將 KernelSU 管理器版本升級至 %2$d 或以上! - 目前 KernelSU 版本 %1$d 過低,管理器無法正常運作。請將內核 KernelSU 版本升級至 %2$d 或以上! 卸載路徑管理 管理內核卸載路徑 沒有既有的內核卸載路徑 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 519fb022c..bd163181c 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -378,8 +378,9 @@ Hide the real file paths of memory mappings from /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Please note: This feature does not support hiding anonymous memory mappings, nor can it hide inline hooks or PLT hooks caused by the injected library itself Important Notice: For applications with well-implemented injection detection mechanisms, this feature may not effectively bypass detection First, find the target application\'s PID and UID using ps -enf, then check the relevant paths in /proc/<pid>/maps and compare the device numbers with those in /proc/1/mountinfo to ensure consistency. Only when the device numbers match can the map hiding function work properly - The current KernelSU manager version %1$d is too low for KernelSU to work properly. Please upgrade manager to version %2$d or higher! - The current KernelSU version %1$d is too low for the manager to work properly. Please upgrade to version %2$d or higher! + Manager update required + Kernel update required. Tap to install. + Kernel update required Umount Path Management Manage kernel unmount paths No existing umount paths diff --git a/uapi/supercall.h b/uapi/supercall.h index 811df2dac..25ab7976a 100644 --- a/uapi/supercall.h +++ b/uapi/supercall.h @@ -16,7 +16,8 @@ // 2: allowlist v4 root profile flags // 3: scoped su-session driver fd -static const __u32 KERNEL_SU_UAPI_VERSION = 3; +// 4: add KSU_GET_INFO_FLAG_BUNDLED +static const __u32 KERNEL_SU_UAPI_VERSION = 4; /* Magic numbers for reboot hook to install fd */ DEFINE_KSU_UAPI_CONST(__u32, KSU_INSTALL_MAGIC1, 0xDEADBEEF) @@ -34,6 +35,7 @@ DEFINE_KSU_UAPI_CONST(__u32, KSU_GET_INFO_FLAG_LKM, (1U << 0)) DEFINE_KSU_UAPI_CONST(__u32, KSU_GET_INFO_FLAG_MANAGER, (1U << 1)) DEFINE_KSU_UAPI_CONST(__u32, KSU_GET_INFO_FLAG_LATE_LOAD, (1U << 2)) DEFINE_KSU_UAPI_CONST(__u32, KSU_GET_INFO_FLAG_PR_BUILD, (1U << 3)) +DEFINE_KSU_UAPI_CONST(__u32, KSU_GET_INFO_FLAG_BUNDLED, (1U << 4)) struct ksu_get_info_cmd { __u32 version; /* Output: KERNEL_SU_VERSION */ diff --git a/userspace/ksud/src/android/cli.rs b/userspace/ksud/src/android/cli.rs index 6f2bce33d..d87995c4e 100755 --- a/userspace/ksud/src/android/cli.rs +++ b/userspace/ksud/src/android/cli.rs @@ -813,6 +813,10 @@ pub fn run() -> Result<()> { println!("uapi_version: {}", info.uapi_version); println!("features: 0x{:x}", info.features); println!("lkm: {}", ksucalls::is_lkm()); + println!( + "bundled: {}", + (info.flags & uapi::KSU_GET_INFO_FLAG_BUNDLED) != 0 + ); println!("late_load: {}", ksucalls::is_late_load()); println!("runtime_mode: {}", ksucalls::runtime_mode()); println!( diff --git a/userspace/ksud/src/android/ksucalls.rs b/userspace/ksud/src/android/ksucalls.rs index ac7e9c967..db3523e01 100644 --- a/userspace/ksud/src/android/ksucalls.rs +++ b/userspace/ksud/src/android/ksucalls.rs @@ -7,7 +7,9 @@ use crate::{android::uapi, defs::MountInfo}; // sigsys handler std::thread_local! { + #[allow(clippy::missing_const_for_thread_local)] static SVC_IN_FLIGHT: Cell = const { Cell::new(false) }; + #[allow(clippy::missing_const_for_thread_local)] static SIGSYS_OCCURRED: Cell = const { Cell::new(false) }; } diff --git a/userspace/ksud/src/android/late_load/mod.rs b/userspace/ksud/src/android/late_load/mod.rs index 040eb5506..aa90136df 100644 --- a/userspace/ksud/src/android/late_load/mod.rs +++ b/userspace/ksud/src/android/late_load/mod.rs @@ -67,6 +67,7 @@ pub fn run(package_name: &String, kmi: Option, allow_shell: bool) -> Res // 4. Load kernelsu.ko from memory with manual relocation info!("Loading kernelsu.ko for KMI {kmi}..."); + // bundled flag is meaningless in jailbreak mode since we can't flash boot to update it. let params = if allow_shell { cstr!("allow_shell=1") } else { diff --git a/userspace/ksud/src/boot_patch.rs b/userspace/ksud/src/boot_patch.rs index 7813e6801..f3b4265f6 100644 --- a/userspace/ksud/src/boot_patch.rs +++ b/userspace/ksud/src/boot_patch.rs @@ -554,6 +554,9 @@ pub fn patch(args: BootPatchArgs) -> Result<()> { ); } + // None means --no-install: preserve the marker for the existing LKM. + let bundled_lkm = (!no_install).then_some(kmod.is_none()); + let kmi = kmi.map_or_else( || -> Result<_> { if kmod.is_some() { @@ -738,6 +741,9 @@ pub fn patch(args: BootPatchArgs) -> Result<()> { apply_config("no custom rc", "norc=1", no_custom_rc); apply_config("allow shell", "allow_shell=1", allow_shell); + if let Some(bundled) = bundled_lkm { + apply_config("bundled LKM", "bundled=1", bundled); + } if ksu_config.is_empty() { cpio.rm("ksu_config", false); From 4e82e3224341cbd31773a4fe8f9a10953290297b Mon Sep 17 00:00:00 2001 From: 5ec1cff <56485584+5ec1cff@users.noreply.github.com> Date: Tue, 8 Sep 2026 23:48:42 +0800 Subject: [PATCH 06/34] feat(resetprop): support update long value for ro props (https://github.com/tiann/KernelSU/pull/3696) [cherry-picked upstream commit https://github.com/tiann/KernelSU/commit/fccd77fe9f202b9c28b5f0942fe588b8c2bdae26] --- userspace/ksud/Cargo.lock | 170 +++++++++++++++--------- userspace/ksud/Cargo.toml | 2 +- userspace/ksud/src/android/resetprop.rs | 30 ++++- 3 files changed, 134 insertions(+), 68 deletions(-) diff --git a/userspace/ksud/Cargo.lock b/userspace/ksud/Cargo.lock index 54de5cac6..ef04a53b9 100644 --- a/userspace/ksud/Cargo.lock +++ b/userspace/ksud/Cargo.lock @@ -148,7 +148,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -248,9 +248,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.4" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" dependencies = [ "find-msvc-tools", "jobserver", @@ -339,7 +339,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -387,6 +387,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -416,9 +422,9 @@ dependencies = [ [[package]] name = "crossbeam" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +checksum = "e71406cd8807725f7ac2f999a4cdd32e98f829fdf65f528343cebf945e41df1e" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -429,18 +435,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -448,27 +454,27 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.20" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-queue" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "crypto-common" @@ -547,11 +553,17 @@ checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "encoding_rs" -version = "0.8.35" +version = "0.8.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" dependencies = [ "cfg-if", + "core_detect", + "multiversion", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", ] [[package]] @@ -648,15 +660,15 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", "miniz_oxide", @@ -781,9 +793,9 @@ dependencies = [ [[package]] name = "hybrid-array" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ "typenum", ] @@ -847,9 +859,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown 0.17.1", @@ -916,13 +928,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.99" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -1038,9 +1049,9 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libflate" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4da9b700e758e57152a1fd1c52cbdc5727c1aa6d8743dc1acda917398f1d76c" +checksum = "561a8da1a50e1428d3c51321dafeca849df992a5bb67720c386131234caba82e" dependencies = [ "adler32", "crc32fast", @@ -1170,14 +1181,41 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.9" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ "adler2", "simd-adler32", ] +[[package]] +name = "multiversion" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" +dependencies = [ + "multiversion-macros", +] + +[[package]] +name = "multiversion-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 3.0.5", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + [[package]] name = "no_std_io2" version = "0.9.4" @@ -1306,9 +1344,9 @@ dependencies = [ [[package]] name = "proc-macro-error-attr3" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0084e6206a967a2dad822180626b2f6b07a3b379325e8f1ec0438e33a469ba7" +checksum = "9e564d14133360e1ae169ffde5da25881b5fa47261665b8e5713c212c27799da" dependencies = [ "proc-macro2", "quote", @@ -1316,14 +1354,14 @@ dependencies = [ [[package]] name = "proc-macro-error3" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cf066225f2373bc711684792b69bdeac0356019b007e721090c24d92d5d5a50" +checksum = "8f0d4471b3436c22106b21913b1dda531558918ae9b7ec55d58aa84b43552233" dependencies = [ "proc-macro-error-attr3", "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -1338,7 +1376,7 @@ dependencies = [ [[package]] name = "prop-rs" version = "0.2.0" -source = "git+https://github.com/Kernel-SU/ksu_props?rev=6f5723105d8d4cacad31d83d343defbf032c7b33#6f5723105d8d4cacad31d83d343defbf032c7b33" +source = "git+https://github.com/Kernel-SU/ksu_props?rev=ddb6ee7294467f7f25bad2118e9e24eee104144b#ddb6ee7294467f7f25bad2118e9e24eee104144b" dependencies = [ "prost", ] @@ -1346,7 +1384,7 @@ dependencies = [ [[package]] name = "prop-rs-android" version = "0.2.0" -source = "git+https://github.com/Kernel-SU/ksu_props?rev=6f5723105d8d4cacad31d83d343defbf032c7b33#6f5723105d8d4cacad31d83d343defbf032c7b33" +source = "git+https://github.com/Kernel-SU/ksu_props?rev=ddb6ee7294467f7f25bad2118e9e24eee104144b#ddb6ee7294467f7f25bad2118e9e24eee104144b" dependencies = [ "libc", "log", @@ -1553,6 +1591,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "scroll" version = "0.13.0" @@ -1564,13 +1608,13 @@ dependencies = [ [[package]] name = "scroll_derive" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d" +checksum = "e1a36a382ed65dbcc0ab47fd5e9a94112417ccd34560a392ef3b7b0f0ec39148" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] @@ -1600,7 +1644,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -1680,6 +1724,12 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.12" @@ -1705,9 +1755,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.4" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" dependencies = [ "proc-macro2", "quote", @@ -1750,7 +1800,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -1873,9 +1923,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.122" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" dependencies = [ "cfg-if", "once_cell", @@ -1886,9 +1936,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.122" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1896,22 +1946,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.122" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.122" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ "unicode-ident", ] @@ -2170,18 +2220,18 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.2.4" +version = "7.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" +version = "2.1.0+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" dependencies = [ "cc", "pkg-config", diff --git a/userspace/ksud/Cargo.toml b/userspace/ksud/Cargo.toml index 8398ea4f3..704f3417d 100644 --- a/userspace/ksud/Cargo.toml +++ b/userspace/ksud/Cargo.toml @@ -58,7 +58,7 @@ serde = { version = "1.0", features = ["derive"] } ksuinit = { path = "../ksuinit" } adb_client = { git = "https://github.com/Kernel-SU/adb_client" } num_enum = "0.7" -prop-rs-android = { git = "https://github.com/Kernel-SU/ksu_props", rev = "6f5723105d8d4cacad31d83d343defbf032c7b33" } +prop-rs-android = { git = "https://github.com/Kernel-SU/ksu_props", rev = "ddb6ee7294467f7f25bad2118e9e24eee104144b" } [target.'cfg(not(target_os = "android"))'.dependencies] env_logger = { version = "0.11.10", default-features = false } diff --git a/userspace/ksud/src/android/resetprop.rs b/userspace/ksud/src/android/resetprop.rs index 3f50e9121..dc67486ea 100644 --- a/userspace/ksud/src/android/resetprop.rs +++ b/userspace/ksud/src/android/resetprop.rs @@ -187,8 +187,12 @@ fn execute(cli: &Args) -> Result<()> { if let Some(path) = &cli.file { let file = File::open(path).with_context(|| format!("Failed to open {path}"))?; let reader = BufReader::new(file); - rp.load_props(reader.lines()) - .context("Failed to load properties from file")?; + if rp + .load_props(reader.lines()) + .context("Failed to load properties from file")? + { + eprintln!("resetprop: warning: rebuild is needed!"); + } return Ok(()); } @@ -225,8 +229,12 @@ fn execute(cli: &Args) -> Result<()> { match (name, value) { // resetprop name value (set) (Some(name), Some(value)) => { - rp.set(name, value) - .with_context(|| format!("Failed to set {name}"))?; + if rp + .set(name, value) + .with_context(|| format!("Failed to set {name}"))? + { + eprintln!("resetprop: warning: rebuild is needed!"); + } } // resetprop name (get) @@ -272,7 +280,8 @@ pub(crate) fn set_property_direct(name: &str, value: &str) -> Result<()> { sys_prop::init().context("Failed to initialize system property API")?; direct_resetprop() .set(name, value) - .with_context(|| format!("Failed to set {name}")) + .with_context(|| format!("Failed to set {name}"))?; + Ok(()) } /// Load system.prop file using internal resetprop API. @@ -292,8 +301,15 @@ pub fn load_system_prop_file(path: &Path) -> Result<()> { let file = File::open(path).with_context(|| format!("Failed to open {}", path.display()))?; let reader = BufReader::new(file); - rp.load_props(reader.lines()) - .with_context(|| format!("Failed to load properties from {}", path.display()))?; + if rp + .load_props(reader.lines()) + .with_context(|| format!("Failed to load properties from {}", path.display()))? + { + log::warn!( + "warning: after loaded prop file from {}, rebuild is needed!", + path.display() + ); + } info!("Loaded system.prop from {}", path.display()); Ok(()) From 830a5aedc742cb7399cccb61df60d55fd7cbcb90 Mon Sep 17 00:00:00 2001 From: Wang Han <416810799@qq.com> Date: Wed, 9 Sep 2026 20:54:57 +0800 Subject: [PATCH 07/34] kernel: Reject all signature block id except v2 (https://github.com/tiann/KernelSU/pull/3700) Old code doesn't check for v3.2 signature block id and is unsafe. To ensure no additional block id is forgotten in the future, let's just allow ids we need. --------- [cherry-picked from upstream commit https://github.com/tiann/KernelSU/commit/aa08b390cdcad1c354c0ba130a6a44378cb0f9af] Co-authored-by: 5ec1cff <56485584+5ec1cff@users.noreply.github.com> Signed-off-by: AlexLiuDev233 --- kernel/manager/apk_sign.c | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/kernel/manager/apk_sign.c b/kernel/manager/apk_sign.c index d60e5d56e..8e52febf4 100644 --- a/kernel/manager/apk_sign.c +++ b/kernel/manager/apk_sign.c @@ -245,8 +245,6 @@ static __always_inline bool check_v2_signature(char *path, u8 *signature_index) bool v2_signing_valid = false; int v2_signing_blocks = 0; - bool v3_signing_exist = false; - bool v3_1_signing_exist = false; u8 matched_index = -1; int i; struct file *fp = filp_open(path, O_RDONLY, 0); @@ -328,18 +326,13 @@ static __always_inline bool check_v2_signature(char *path, u8 *signature_index) if (id == 0x7109871au) { v2_signing_blocks++; - v2_signing_valid = check_block(fp, &pos, pair_end, &matched_index); - } else if (id == 0xf05368c0u) { - // http://aospxref.com/android-14.0.0_r2/xref/frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java#73 - v3_signing_exist = true; - } else if (id == 0x1b93ad61u) { - // http://aospxref.com/android-14.0.0_r2/xref/frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java#74 - v3_1_signing_exist = true; - } else { + } else if (id != 0x42726577u) { // APK verity padding + // https://cs.android.com/android/platform/superproject/+/android-latest-release:tools/apksig/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java;l=102;drc=ebe4dfd4fd6550c949a6c7c2427484bf5e96500b #ifdef CONFIG_KSU_DEBUG - pr_info("Unknown id: 0x%08x\n", id); + pr_info("Unexpected signature block id: 0x%08x\n", id); #endif + goto invalid; } pos = pair_end; } @@ -365,11 +358,6 @@ static __always_inline bool check_v2_signature(char *path, u8 *signature_index) clean: filp_close(fp, 0); - if (v2_signing_valid && (v3_signing_exist || v3_1_signing_exist)) { - pr_err("Unexpected v3 signature scheme found!\n"); - return false; - } - if (v2_signing_valid) { if (signature_index) { *signature_index = matched_index; From 23a40c0f1dc047ee73a1dfd70a3f14df70021cac Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Wed, 9 Sep 2026 19:05:33 +0200 Subject: [PATCH 08/34] manager: update translations from Weblate (#395) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translations updated in [Hosted Weblate](https://hosted.weblate.org) for [ReSukiSU/ReSukiSU](https://hosted.weblate.org/projects/resukisu/resukisu/). Translation status: ![Weblate translation status](https://hosted.weblate.org/widget/resukisu/resukisu/matrix-auto.svg) Signed-off-by: AlexLiuDev233 Co-authored-by: Lê Phúc Hưng Co-authored-by: Oğuz Ersen Co-authored-by: karigane <169052233+karigane-cha@users.noreply.github.com> Co-authored-by: kuklux Co-authored-by: Влад Плотников Co-authored-by: AlexLiuDev233 --- .../app/src/main/res/values-ja/strings.xml | 24 +++++++++++++--- .../app/src/main/res/values-ru/strings.xml | 28 +++++++++---------- .../app/src/main/res/values-tr/strings.xml | 2 +- .../app/src/main/res/values-uk/strings.xml | 2 +- .../app/src/main/res/values-vi/strings.xml | 3 ++ 5 files changed, 39 insertions(+), 20 deletions(-) diff --git a/manager/app/src/main/res/values-ja/strings.xml b/manager/app/src/main/res/values-ja/strings.xml index 8b9c885a8..7f6581543 100644 --- a/manager/app/src/main/res/values-ja/strings.xml +++ b/manager/app/src/main/res/values-ja/strings.xml @@ -132,10 +132,10 @@ Android バージョン デバイスモデル 「%s」にスーパーユーザー権限を付与することはできません - 古典 su 命令 - /system/bin/su 経由での 根源権限(特権)を 許可します(新規 手順/工程 のみ) - module の umount - App Profile に基づき、kernel 側で module を umount します (あぷり ぷろふぁいる に もとづき、かーねる がわで もじゅーる を あんまうんと します) + 従来の su コマンド + 新規プロセスにおいて、/system/bin/su を経由して root アクセスを許可します。 + モジュールのアンマウント(カーネルレベル) + アプリ プロファイルに基づいて、カーネルからモジュールをアンマウントします カーネルはこの機能に対応していません この機能はモジュールによって管理されています デフォルト @@ -266,4 +266,20 @@ 構成を削除 操作が失敗しました %d 個のアプリが含まれています + Seccomp のステータス + Not supported + Disabled + Strict + Filter + Unknown + SU ログ + ログファイル + SU ログの読み込みに失敗しました + SU ログは有効になっていません + SU ログはサポートされていません + 有効化 + 種類で絞り込む + ログを消去 + WebView のアンマウント + WebView プロセスからの情報漏洩を防ぎますが、モジュールが動作しなくなる可能性があります。適用するには再起動してください diff --git a/manager/app/src/main/res/values-ru/strings.xml b/manager/app/src/main/res/values-ru/strings.xml index 31d765450..aae21f749 100644 --- a/manager/app/src/main/res/values-ru/strings.xml +++ b/manager/app/src/main/res/values-ru/strings.xml @@ -4,7 +4,7 @@ Узнать больше Не установлен Нажмите, чтобы установить - Работает + Активен Не поддерживается Драйвера KernelSU не найдены в ядре, неверное ядро? Версия ядра @@ -39,7 +39,7 @@ Неизвестное событие Поиск лога Не удалось включить модуль %s - Не удалось отключить модуль %s + Не удалось отключить модуль: %s Нет установленных модулей Модули Репозиторий модулей @@ -47,20 +47,20 @@ Сортировка (по звездам) Сортировать (Сначала включённые) Удалить - Установка + Установить Перезагрузить Настройки Мягкая перезагрузка Мягкая перезагрузка Перезагрузить в Recovery - Перезагрузить в Bootloader + Перезагрузить в загрузчик Перезагрузить в Download Перезагрузить в EDL О приложении Проверить исходный код Проверить исходный код на GitHub Присоединиться к сообществу - Присоединиться к нашему Telegram-каналу + Присоединиться к нашему Telegram-сообществу Open Source лицензия Просмотреть сторонние open-source библиотеки и их лицензии Посетить домашнюю страницу @@ -69,7 +69,7 @@ Вы уверены, что хотите удалить модуль %s? Вы уверены, что хотите удалить модуль %s? Это действие повлияет на все модули, и некоторые функции мета модуля (например, монтирование) больше не будут работать %s удалён - Не удалось удалить %s + Не удалось удалить: %s Версия Автор Показать системные приложения @@ -128,7 +128,7 @@ Управление локальным и онлайн-шаблоном профиля приложения Создать шаблон Редактирование шаблона - Идентификационный номер + ID Неверный ID шаблона Название Описание @@ -147,13 +147,13 @@ Затрагиваемые приложения Не удалось получить список изменений: %s Не удалось выдать root! - Это debug-сборка из Pull Request. НЕ используйте в продакшене! + Это debug-сборка из PR. НЕ используйте в продакшене! Действие Закрыть Прямая установка (Рекомендуется) Выбрать файл Установка в неактивный слот (После OTA) - Ваше устройство будет **ПРИНУДИТЕЛЬНО** загружено в текущий неактивный слот после перезагрузки! \n Используйте эту опцию только после завершения OTA. \n Продолжить? + Ваше устройство будет **ПРИНУДИТЕЛЬНО** загружено в текущий неактивный слот после перезагрузки!\nИспользуйте эту опцию только после завершения OTA.\nПродолжить? Далее Выбрать раздел Использовать локальный файл LKM @@ -167,23 +167,23 @@ Временно удалить KernelSU, восстановить исходное состояние после следующей перезагрузки. Удалить KernelSU (рут и все модули) полностью. Восстановить исходный заводской образ (если существует резервная копия), обычно используется перед OTA; если вам нужно удалить KernelSU, используйте «Удалить полностью». - Установка + Прошивка Установка выполнена Установка не выполнена Выбран LKM: %s Сохранить логи Логи сохранены - неизвестный модуль + Неизвестный модуль Подтвердить Отмена Резервная копия создана успешно Ошибка резервного копирования списка: %1$s - Подтвердите восстановление списка + Подтверждение восстановления белого списка Эта операция перезапишет текущий список разрешений. Продолжить? Список успешно восстановлен Не удалось восстановить список: %1$s Резервное копирование списка - Восстановить список + Белый список восстановления ключей Пользовательский фон приложения Выберите изображение в качестве фона Включить блюр @@ -427,7 +427,7 @@ Настройки темы Стиль палитры Спецификация цвета - AOSP стиль + AOSP Ридми Статус Стандарт diff --git a/manager/app/src/main/res/values-tr/strings.xml b/manager/app/src/main/res/values-tr/strings.xml index 2ef25932e..ad5653fec 100644 --- a/manager/app/src/main/res/values-tr/strings.xml +++ b/manager/app/src/main/res/values-tr/strings.xml @@ -488,7 +488,7 @@ Dinamik yöneticiyi temizle Dinamik yönetici yapılandırmasını silmek istediğinize emin misiniz? Yöneticileri yönet - Hakkında + Sürüm Bilgisi Durum Bilgisi SüperKullanıcı: %1$d, Modüller: %2$d Çekirdek sürücüsü sürümü diff --git a/manager/app/src/main/res/values-uk/strings.xml b/manager/app/src/main/res/values-uk/strings.xml index 22468c7b0..4eac7a780 100644 --- a/manager/app/src/main/res/values-uk/strings.xml +++ b/manager/app/src/main/res/values-uk/strings.xml @@ -513,7 +513,7 @@ Очистити динамічний менеджер Ви впевнені, що хочете очистити налаштування динамічного менеджера? Керування менеджерами - Про + Інформація про версію Інформація про стан Суперкористувач: %1$d, Модулі: %2$d Версія драйвера ядра diff --git a/manager/app/src/main/res/values-vi/strings.xml b/manager/app/src/main/res/values-vi/strings.xml index 151a9cb62..0e462f8b6 100644 --- a/manager/app/src/main/res/values-vi/strings.xml +++ b/manager/app/src/main/res/values-vi/strings.xml @@ -400,4 +400,7 @@ Cài đặt Cần có Quyền truy cập vào thông báo để có thể hiện tiến trình tải. Tải thất bại: Cần có Quyền truy cập thông báo + Cờ + Chọn 1 phương thức crop + Cắt ảnh thất bại From 1e25a4c5c0e7eb71292f1fa4f48704e5490f1b82 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Thu, 10 Sep 2026 16:04:41 +0200 Subject: [PATCH 09/34] manager: update translations from Weblate (#400) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translations updated in [Hosted Weblate](https://hosted.weblate.org) for [ReSukiSU/ReSukiSU](https://hosted.weblate.org/projects/resukisu/resukisu/). Translation status: ![Weblate translation status](https://hosted.weblate.org/widget/resukisu/resukisu/matrix-auto.svg) --------- Signed-off-by: AlexLiuDev233 Co-authored-by: NicosXRus Co-authored-by: Oğuz Ersen Co-authored-by: kuklux Co-authored-by: tilla2 Co-authored-by: AlexLiuDev233 --- manager/app/src/main/res/font/monospace.xml | 7 ------- manager/app/src/main/res/values-hu/strings.xml | 3 +++ manager/app/src/main/res/values-ja/strings.xml | 2 -- manager/app/src/main/res/values-ru/strings.xml | 4 ++++ manager/app/src/main/res/values-tr/strings.xml | 3 +++ manager/app/src/main/res/values-uk/strings.xml | 3 +++ 6 files changed, 13 insertions(+), 9 deletions(-) delete mode 100644 manager/app/src/main/res/font/monospace.xml diff --git a/manager/app/src/main/res/font/monospace.xml b/manager/app/src/main/res/font/monospace.xml deleted file mode 100644 index ea8dcd70f..000000000 --- a/manager/app/src/main/res/font/monospace.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/manager/app/src/main/res/values-hu/strings.xml b/manager/app/src/main/res/values-hu/strings.xml index 07bc657de..e6828725d 100644 --- a/manager/app/src/main/res/values-hu/strings.xml +++ b/manager/app/src/main/res/values-hu/strings.xml @@ -535,4 +535,7 @@ Beépített SuSFS Menedzser engedélyezése vagy letiltása. Valamely harmadik féltől származó modullal összeférhetetlen lehet. Beépített SuSFS Menedzser letiltva. Engedélyezd, vagy telepíts egy harmadik féltől származó modult a SuSFS funkciók használatához. + Root menedzser frissítése szükséges + Kernel frissítése szükséges. Kattints a telepítéshez. + Kernel frissítése szükséges diff --git a/manager/app/src/main/res/values-ja/strings.xml b/manager/app/src/main/res/values-ja/strings.xml index 7f6581543..150375e5e 100644 --- a/manager/app/src/main/res/values-ja/strings.xml +++ b/manager/app/src/main/res/values-ja/strings.xml @@ -280,6 +280,4 @@ 有効化 種類で絞り込む ログを消去 - WebView のアンマウント - WebView プロセスからの情報漏洩を防ぎますが、モジュールが動作しなくなる可能性があります。適用するには再起動してください diff --git a/manager/app/src/main/res/values-ru/strings.xml b/manager/app/src/main/res/values-ru/strings.xml index aae21f749..34dda9aba 100644 --- a/manager/app/src/main/res/values-ru/strings.xml +++ b/manager/app/src/main/res/values-ru/strings.xml @@ -534,4 +534,8 @@ менеджер SUSFS Включите встроенный менеджер SUSFS. Может конфликтовать со сторонними модулями. Встроенный менеджер SUSFS отключен. Включите его или установите сторонний модуль для использования функций SUSFS. + + Требуется обновление менеджера + Требуется обновление ядра. Нажмите, чтобы установить. + Требуется обновление ядра diff --git a/manager/app/src/main/res/values-tr/strings.xml b/manager/app/src/main/res/values-tr/strings.xml index ad5653fec..67ac2dc4b 100644 --- a/manager/app/src/main/res/values-tr/strings.xml +++ b/manager/app/src/main/res/values-tr/strings.xml @@ -535,4 +535,7 @@ SUSFS Yöneticisi Yerleşik SUSFS yöneticisini etkinleştirir. Üçüncü taraf modüllerle çakışabilir. Yerleşik SUSFS yöneticisi devre dışı. SUSFS özelliklerini kullanmak için bunu etkinleştirin veya üçüncü taraf bir modül yükleyin. + Yönetici güncellemesi gerekiyor + Çekirdek güncellemesi gerekiyor. Kurmak için dokunun. + Çekirdek güncellemesi gerekiyor diff --git a/manager/app/src/main/res/values-uk/strings.xml b/manager/app/src/main/res/values-uk/strings.xml index 4eac7a780..8158c1737 100644 --- a/manager/app/src/main/res/values-uk/strings.xml +++ b/manager/app/src/main/res/values-uk/strings.xml @@ -535,4 +535,7 @@ Менеджер SUSFS Використовувати вбудований моноширинний шрифт + Потрібне оновлення ядра + Потрібне оновлення менеджера + Потрібне оновлення ядра. Натисніть, щоб встановити. From 246d3e52e667cb72ce8f70c93b70d3b42b100b76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?YC=E9=85=B1luyancib?= Date: Thu, 10 Sep 2026 23:11:35 +0800 Subject: [PATCH 10/34] manager: replace SettingsJumpPageWidget with SettingsChooseWidget (#401) for unify ui interface --- .../resukisu/ui/screen/main/SettingsPage.kt | 179 +++--------------- 1 file changed, 30 insertions(+), 149 deletions(-) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt index 73dbacba5..59b15b4e0 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt @@ -41,8 +41,6 @@ import androidx.compose.material.icons.twotone.FolderOff import androidx.compose.material.icons.twotone.Info import androidx.compose.material.icons.twotone.Language import androidx.compose.material.icons.twotone.Policy -import androidx.compose.material.icons.twotone.RadioButtonChecked -import androidx.compose.material.icons.twotone.RadioButtonUnchecked import androidx.compose.material.icons.twotone.RemoveCircle import androidx.compose.material.icons.twotone.RemoveModerator import androidx.compose.material.icons.twotone.Save @@ -50,8 +48,6 @@ import androidx.compose.material.icons.twotone.Security import androidx.compose.material.icons.twotone.Settings import androidx.compose.material.icons.twotone.Share import androidx.compose.material.icons.twotone.Update -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon @@ -60,7 +56,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.rememberTopAppBarState @@ -87,10 +82,8 @@ import com.resukisu.resukisu.BuildConfig import com.resukisu.resukisu.R import com.resukisu.resukisu.domain.usecase.GenerateBugreportUseCase import com.resukisu.resukisu.ui.component.ConfirmResult -import com.resukisu.resukisu.ui.component.DialogHandle import com.resukisu.resukisu.ui.component.SwipeableSnackbarHost import com.resukisu.resukisu.ui.component.rememberConfirmDialog -import com.resukisu.resukisu.ui.component.rememberCustomDialog import com.resukisu.resukisu.ui.component.rememberLoadingDialog import com.resukisu.resukisu.ui.component.settings.SegmentedColumn import com.resukisu.resukisu.ui.component.settings.SettingsBaseWidget @@ -625,30 +618,40 @@ fun UninstallItem( val showTodo = { Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show() } - val uninstallDialog = rememberUninstallDialog { uninstallType -> - scope.launch { - val result = uninstallConfirmDialog.awaitConfirm( - title = context.getString(uninstallType.title), - content = context.getString(uninstallType.message) - ) - if (result == ConfirmResult.Confirmed) { - withLoading { - when (uninstallType) { - UninstallType.TEMPORARY -> showTodo() - UninstallType.PERMANENT -> navigator.push(Route.Flash.uninstall()) - UninstallType.RESTORE_STOCK_IMAGE -> navigator.push(Route.Flash.restore()) - UninstallType.NONE -> Unit - } - } - } - } + val options = remember { + listOf( + UninstallType.PERMANENT, + UninstallType.RESTORE_STOCK_IMAGE + ) } - SettingsJumpPageWidget( + SettingsChooseWidget( icon = Icons.TwoTone.Delete, title = stringResource(id = R.string.settings_uninstall), - onClick = { - uninstallDialog.show() + items = options.map { stringResource(it.title) }, + itemDescriptions = options.map { + if (it.message != 0) stringResource(it.message) else null + }, + selectedIndex = -1, + onSelectedIndexChange = { index -> + options.getOrNull(index)?.let { uninstallType -> + scope.launch { + val result = uninstallConfirmDialog.awaitConfirm( + title = context.getString(uninstallType.title), + content = context.getString(uninstallType.message) + ) + if (result == ConfirmResult.Confirmed) { + withLoading { + when (uninstallType) { + UninstallType.TEMPORARY -> showTodo() + UninstallType.PERMANENT -> navigator.push(Route.Flash.uninstall()) + UninstallType.RESTORE_STOCK_IMAGE -> navigator.push(Route.Flash.restore()) + UninstallType.NONE -> Unit + } + } + } + } + } } ) } @@ -672,128 +675,6 @@ enum class UninstallType(val title: Int, val message: Int, val icon: ImageVector NONE(0, 0, Icons.TwoTone.Delete) } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle { - return rememberCustomDialog { dismiss -> - val options = listOf( - UninstallType.PERMANENT, - UninstallType.RESTORE_STOCK_IMAGE - ) - var selectedOption by remember { mutableStateOf(null) } - - AlertDialog( - onDismissRequest = { - dismiss() - }, - title = { - Text( - text = stringResource(R.string.settings_uninstall), - style = MaterialTheme.typography.headlineSmall, - ) - }, - text = { - Column( - modifier = Modifier.padding(vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - options.forEach { option -> - val isSelected = selectedOption == option - val backgroundColor = if (isSelected) - MaterialTheme.colorScheme.primaryContainer - else - Color.Transparent - val contentColor = if (isSelected) - MaterialTheme.colorScheme.onPrimaryContainer - else - MaterialTheme.colorScheme.onSurface - - Row( - modifier = Modifier - .fillMaxWidth() - .clip(MaterialTheme.shapes.medium) - .background(backgroundColor) - .clickable { - selectedOption = option - } - .padding(vertical = 12.dp, horizontal = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = option.icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier - .padding(end = 16.dp) - .size(24.dp) - ) - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = stringResource(option.title), - style = MaterialTheme.typography.titleMedium, - ) - if (option.message != 0) { - Text( - text = stringResource(option.message), - style = MaterialTheme.typography.bodyMedium, - color = if (isSelected) - contentColor.copy(alpha = 0.8f) - else - MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - if (isSelected) { - Icon( - imageVector = Icons.TwoTone.RadioButtonChecked, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(24.dp) - ) - } else { - Icon( - imageVector = Icons.TwoTone.RadioButtonUnchecked, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(24.dp) - ) - } - } - } - } - }, - confirmButton = { - Button( - onClick = { - selectedOption?.let { onSelected(it) } - dismiss() - }, - enabled = selectedOption != null, - ) { - Text( - text = stringResource(android.R.string.ok) - ) - } - }, - dismissButton = { - TextButton( - onClick = { - dismiss() - } - ) { - Text( - text = stringResource(android.R.string.cancel), - ) - } - }, - shape = MaterialTheme.shapes.extraLarge, - tonalElevation = 4.dp - ) - } -} - @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable private fun TopBar( From 7ad75c5df35781f560f62c5cd6663ba60de56a7c Mon Sep 17 00:00:00 2001 From: AlexLiuDev233 Date: Fri, 11 Sep 2026 19:23:37 +0800 Subject: [PATCH 11/34] kernel: get session_keyring from key_permission LSM call in init second stage execve, it might still haven't inited [ 2.658107] KernelSU: kernel_compat: pid=1 comm=init current_session=(null) current_session_id=0 So, let's delay session_keyring init to key_permission in these devices Signed-off-by: AlexLiuDev233 --- kernel/compat/kernel_compat.c | 24 +++++------------------- kernel/compat/kernel_compat.h | 13 +++++++++++++ kernel/core/init.c | 4 ++++ kernel/hook/lsm_hooks.c | 25 ++++++++++++++++++++++++- 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/kernel/compat/kernel_compat.c b/kernel/compat/kernel_compat.c index 8be372694..e7936b184 100644 --- a/kernel/compat/kernel_compat.c +++ b/kernel/compat/kernel_compat.c @@ -9,6 +9,7 @@ #include #include #include +#include #include "klog.h" // IWYU pragma: keep #include "kernel_compat.h" @@ -196,26 +197,11 @@ void ksu_run_in_init_if_possible(void (*callback)(void *), void *data) } #ifdef KSU_COMPAT_REQUIRE_SESSION_KEYRING -#include -#include -#include #include "ksu.h" -static inline struct key *ksu_get_session_keyring(const struct cred *cred) -{ -// https://github.com/torvalds/linux/commit/3a50597de8635cd05133bd12c95681c82fe7b878 -#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 8, 0) - return rcu_dereference(cred->session_keyring); -#else - return rcu_dereference(current->cred->tgcred->session_keyring); -#endif -} - +struct key *init_session_keyring = NULL; extern int install_session_keyring_to_cred(struct cred *, struct key *); -// WARNING! Make sure caller in init!!! -// https://github.com/torvalds/linux/commit/5c7e372caa35d303e414caeb64ee2243fd3cac3d -// in our target kernel version, it are protected by rcu, so let's rcu_dereference here void setup_ksu_cred_session_keyring(void) { if (ksu_get_session_keyring(ksu_cred)) { @@ -223,12 +209,12 @@ void setup_ksu_cred_session_keyring(void) return; } - if (strcmp(current->comm, "init")) { - // we are only interested in `init` process + if (init_session_keyring == NULL) { + // if init_session_keyring is null, skip return; } - install_session_keyring_to_cred(ksu_cred, ksu_get_session_keyring(current_cred())); + install_session_keyring_to_cred(ksu_cred, init_session_keyring); pr_info("kernel_compat: %s: install init_session_keyring to ksu_cred\n", __func__); } diff --git a/kernel/compat/kernel_compat.h b/kernel/compat/kernel_compat.h index 50141dc27..14e952927 100644 --- a/kernel/compat/kernel_compat.h +++ b/kernel/compat/kernel_compat.h @@ -279,7 +279,20 @@ extern void ksu_run_in_init_if_possible(void (*callback)(void *), void *data); #if defined(CONFIG_KEYS) && (LINUX_VERSION_CODE < KERNEL_VERSION(4, 10, 0) || defined(KSU_COMPAT_IS_HISI_LEGACY) || \ defined(KSU_COMPAT_IS_HISI_LEGACY_HM2)) #define KSU_COMPAT_REQUIRE_SESSION_KEYRING +#include + +extern struct key *init_session_keyring; extern void setup_ksu_cred_session_keyring(void); + +static inline struct key *ksu_get_session_keyring(const struct cred *cred) +{ +// https://github.com/torvalds/linux/commit/3a50597de8635cd05133bd12c95681c82fe7b878 +#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 8, 0) + return rcu_dereference(cred->session_keyring); +#else + return rcu_dereference(current->cred->tgcred->session_keyring); +#endif +} #endif #if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 3, 0) || defined(KSU_HAS_MODERN_STATIC_KEY_INTERFACE) diff --git a/kernel/core/init.c b/kernel/core/init.c index 4927a2cd1..78d9e6ae9 100644 --- a/kernel/core/init.c +++ b/kernel/core/init.c @@ -142,6 +142,10 @@ void setup_ksu_cred(void) { setup_ksu_cred_selinux(); #ifdef KSU_COMPAT_REQUIRE_SESSION_KEYRING + if (init_session_keyring == NULL) { + init_session_keyring = ksu_get_session_keyring(current_cred()); + } + setup_ksu_cred_session_keyring(); #endif } diff --git a/kernel/hook/lsm_hooks.c b/kernel/hook/lsm_hooks.c index 16088e643..45e60f1fd 100644 --- a/kernel/hook/lsm_hooks.c +++ b/kernel/hook/lsm_hooks.c @@ -83,6 +83,23 @@ static void ksu_handle_bprm_committed_creds(struct linux_binprm *bprm) } #endif +#ifdef KSU_COMPAT_REQUIRE_SESSION_KEYRING +static int ksu_handle_key_permission(key_ref_t key_ref, const struct cred *cred, unsigned perm) +{ + if (init_session_keyring != NULL) { + return 0; + } + if (strcmp(current->comm, "init")) { + // we are only interested in `init` process + return 0; + } + init_session_keyring = ksu_get_session_keyring(cred); + pr_info("%s: got init_session_keyring, trying install..\n", __func__); + setup_ksu_cred_session_keyring(); + return 0; +} +#endif + #if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 2, 0) || defined(KSU_COMPAT_HAS_LIST_OF_LSM_HOOKS) #include @@ -99,6 +116,10 @@ static struct security_hook_list ksu_hooks[] = { #ifdef KSU_COMPAT_NO_POST_EXECVE_HOOK LSM_HOOK_INIT(bprm_committed_creds, ksu_handle_bprm_committed_creds), #endif + +#ifdef KSU_COMPAT_REQUIRE_SESSION_KEYRING + LSM_HOOK_INIT(key_permission, ksu_handle_key_permission), +#endif }; void __init ksu_lsm_hook_built_in_init(void) @@ -148,7 +169,9 @@ void __init ksu_lsm_hook_built_in_init(void) IF_CONFIG_KSU_MANUAL_HOOK_AUTO_INITRC_HOOK( \ HOOK_ITEM(file_permission, ksu_file_permission, (struct file * file, int mask), (file, mask))) \ IF_KSU_COMPAT_NO_POST_EXECVE_HOOK( \ - HOOK_ITEM(bprm_committed_creds, ksu_handle_bprm_committed_creds, (struct linux_binprm * bprm), (bprm))) + HOOK_ITEM(bprm_committed_creds, ksu_handle_bprm_committed_creds, (struct linux_binprm * bprm), (bprm))) \ + HOOK_ITEM(key_permission, ksu_handle_key_permission, (key_ref_t key_ref, const struct cred *cred, unsigned perm), \ + (key_ref, cred, perm)) #define STRIP_PARENS(...) __VA_ARGS__ From 07a28eb8226bc94fee2662fb54a7290120e3360a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:05:53 +0800 Subject: [PATCH 12/34] build(deps): bump the crates group in /userspace/ksuinit with 2 updates (#396) --- userspace/ksuinit/Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/userspace/ksuinit/Cargo.lock b/userspace/ksuinit/Cargo.lock index 7f576677a..84ff40fa4 100644 --- a/userspace/ksuinit/Cargo.lock +++ b/userspace/ksuinit/Cargo.lock @@ -122,9 +122,9 @@ dependencies = [ [[package]] name = "scroll_derive" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d" +checksum = "e1a36a382ed65dbcc0ab47fd5e9a94112417ccd34560a392ef3b7b0f0ec39148" dependencies = [ "proc-macro2", "quote", @@ -133,9 +133,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.119" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" dependencies = [ "proc-macro2", "quote", From 4f27ee12d16841d240315ab7b34c0de5fe91411c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:22:54 +0800 Subject: [PATCH 13/34] build(deps): bump the crates group across 1 directory with 4 updates (#399) Bumps the crates group with 2 updates in the /userspace/ksud directory: [bindgen](https://github.com/rust-lang/rust-bindgen) and [prettyplease](https://github.com/dtolnay/prettyplease). Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- userspace/ksud/Cargo.lock | 36 ++++++++++-------------------------- userspace/ksud/Cargo.toml | 2 +- 2 files changed, 11 insertions(+), 27 deletions(-) diff --git a/userspace/ksud/Cargo.lock b/userspace/ksud/Cargo.lock index ef04a53b9..bdc024589 100644 --- a/userspace/ksud/Cargo.lock +++ b/userspace/ksud/Cargo.lock @@ -50,7 +50,7 @@ dependencies = [ "bytemuck", "bzip2", "flate2", - "itertools 0.14.0", + "itertools", "lz4", "lzma-rust2 0.15.8", "num-traits", @@ -165,22 +165,21 @@ checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" [[package]] name = "bindgen" -version = "0.72.1" +version = "0.73.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +checksum = "54be9a169b85a1bef39252af30bb6246a31b23c7e0f6a162520dd869f8dad33c" dependencies = [ "bitflags 2.13.1", "cexpr", "clang-sys", - "itertools 0.13.0", "log", "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash", - "shlex 1.3.0", - "syn 2.0.119", + "shlex", + "syn 3.0.5", ] [[package]] @@ -255,7 +254,7 @@ dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex 2.0.1", + "shlex", ] [[package]] @@ -882,15 +881,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -1325,12 +1315,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "prettyplease" -version = "0.2.37" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" dependencies = [ "proc-macro2", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] @@ -1409,7 +1399,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools", "proc-macro2", "quote", "syn 2.0.119", @@ -1706,12 +1696,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "shlex" version = "2.0.1" diff --git a/userspace/ksud/Cargo.toml b/userspace/ksud/Cargo.toml index 704f3417d..a6334456a 100644 --- a/userspace/ksud/Cargo.toml +++ b/userspace/ksud/Cargo.toml @@ -70,5 +70,5 @@ lto = true codegen-units = 1 [build-dependencies] -bindgen = "0.72.1" +bindgen = "0.73.1" cc = "1" From 6930e97b59f8f5a7a2e75583f7be1cf982856157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?YC=E9=85=B1luyancib?= Date: Fri, 11 Sep 2026 22:36:56 +0800 Subject: [PATCH 14/34] kernel: add setup submodule command(#183) This commit will close #180. --- kernel/setup.sh | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/kernel/setup.sh b/kernel/setup.sh index 4b495aaef..8d7b5f6aa 100644 --- a/kernel/setup.sh +++ b/kernel/setup.sh @@ -8,6 +8,7 @@ display_usage() { echo " --cleanup: Cleans up previous modifications made by the script." echo " : Sets up or updates the KernelSU to specified tag or commit." echo " -h, --help: Displays this usage information." + echo " --submodule: Resets KernelSU as a submodule." echo " (no args): Sets up or updates the KernelSU environment to the latest tagged version." } @@ -34,6 +35,14 @@ perform_cleanup() { if [ -d "$GKI_ROOT/KernelSU" ]; then rm -rf "$GKI_ROOT/KernelSU" && echo "[-] KernelSU directory deleted." fi + if [ -f "$GKI_ROOT/.gitmodules" ] && grep -q 'KernelSU' "$GKI_ROOT/.gitmodules"; then + echo "[!] KernelSU has been added as a submodule." + echo "[!] Please remove it manually." + echo "[!] You can run the following commands:" + echo "--- git submodule deinit -f KernelSU" + echo "--- git rm -f KernelSU" + echo "--- git commit -m 'Remove KernelSU submodule'" + fi } # Sets up or update KernelSU environment @@ -62,6 +71,36 @@ setup_kernelsu() { grep -q "kernelsu" "$DRIVER_MAKEFILE" || printf "\nobj-\$(CONFIG_KSU) += kernelsu/\n" >> "$DRIVER_MAKEFILE" && echo "[+] Modified Makefile." grep -q "source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG" || sed -i "/endmenu/i\source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG" && echo "[+] Modified Kconfig." echo '[+] Done.' + echo '[!] If you want to add submodule in your kernelsource,you can run this setup script with --submodule argument.' +} + +# Setup KernelSU as submodule +setup_submodule() { + cd "$GKI_ROOT" + + if [ ! -d "$GKI_ROOT/KernelSU" ]; then + echo '[!] KernelSU directory does not exist. Please run the script without --submodule first.' + exit 127 + fi + + if [ ! -d "$GKI_ROOT/.git" ]; then + echo '[!] GKI_ROOT is not a git repository. Skipping submodule setup.' + return 0 + fi + + if [ "${CI:-false}" = "true" ] || [ "${GITHUB_ACTIONS:-false}" = "true" ]; then + echo '[!] Running in CI. Skipping submodule setup.' + return 0 + fi + + if [ -f "$GKI_ROOT/.gitmodules" ] && grep -q 'KernelSU' "$GKI_ROOT/.gitmodules"; then + echo '[!] KernelSU is already a submodule. Skipping submodule setup.' + return 0 + fi + + echo '[+] Setting up KernelSU as submodule...' + git submodule add https://github.com/ReSukiSU/ReSukiSU KernelSU || echo '[!] Failed to add KernelSU as a submodule.' + echo '[+] Done.' } # Process command-line arguments @@ -70,6 +109,9 @@ if [ "$#" -eq 0 ]; then setup_kernelsu elif [ "$1" = "-h" ] || [ "$1" = "--help" ]; then display_usage +elif [ "$1" = "--submodule" ]; then + initialize_variables + setup_submodule elif [ "$1" = "--cleanup" ]; then initialize_variables perform_cleanup From 052ca27775cc9c8570e3b1ba3db7c29486d6020e Mon Sep 17 00:00:00 2001 From: OmachiriManbu Date: Sat, 12 Sep 2026 01:37:08 +0800 Subject: [PATCH 15/34] kernel: compat for old version of susfs and include task_stack.h for LTO (#403) - include task_stack.h in adb_root to make LTO happy - always flag KSU_IN_EXECVE to avoid old susfs cause install su_fd in every execve Co-authored-by: AlexLiuDev233 --- kernel/feature/adb_root.c | 6 ++++++ kernel/feature/sucompat.c | 7 +------ kernel/feature/sucompat.h | 3 +++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/kernel/feature/adb_root.c b/kernel/feature/adb_root.c index b795a6012..f79030db3 100644 --- a/kernel/feature/adb_root.c +++ b/kernel/feature/adb_root.c @@ -8,6 +8,12 @@ #include #include #include +#include + +// https://github.com/torvalds/linux/commit/68db0cf10678630d286f4bbbbdfa102951a35faa +#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0) +#include +#endif #include "adb_root.h" #include "arch.h" diff --git a/kernel/feature/sucompat.c b/kernel/feature/sucompat.c index 244d26d0b..aa4a13ebf 100644 --- a/kernel/feature/sucompat.c +++ b/kernel/feature/sucompat.c @@ -384,22 +384,17 @@ static inline int do_ksu_handle_execveat_sucompat(int *fd, const char *filename, memcpy((void *)filename, ksud_path, sizeof(ksud_path)); out: ksu_sulog_emit_pending(pending_sucompat, 0, GFP_KERNEL); -#ifdef CONFIG_KSU_MANUAL_HOOK - // flag for post execve hook, mostly: bprm_committed_creds LSM hooks - // no need care in susfs, susfs completed everything + // always flag that, to avoid old version of susfs hang in boot set_thread_flag(TIF_PROC_IN_KSU_EXECVE); -#endif return 0; } // fd, filename, argv, envp, flags and retval were NOT provided in bprm_committed_creds (KSU_COMPAT_NO_POST_EXECVE_HOOK)! int ksu_handle_post_execve(int *fd, const char *filename, void *argv, void *envp, int *flags, int *retval) { -#ifdef CONFIG_KSU_MANUAL_HOOK if (likely(!test_thread_flag(TIF_PROC_IN_KSU_EXECVE))) { return -EINVAL; } -#endif #ifndef KSU_COMPAT_HAS_SUSFS_INSTALL_SU_FD_DIRECT_CALL ksu_install_su_fd(); #endif diff --git a/kernel/feature/sucompat.h b/kernel/feature/sucompat.h index 13fcfbeb0..efdc23d14 100644 --- a/kernel/feature/sucompat.h +++ b/kernel/feature/sucompat.h @@ -43,6 +43,9 @@ long ksu_handle_execveat_sucompat_internal(const char __user **filename_user, in #elif defined(CONFIG_KSU_SUSFS) // susfs #include +// sync with manual hook +#define TIF_PROC_IN_KSU_EXECVE 61 + #define ksu_is_current_proc_unprivillege susfs_is_current_proc_no_su #define ksu_set_current_proc_unprivillege susfs_set_current_proc_no_su #define ksu_clear_current_proc_unprivillege susfs_clear_current_proc_no_su From 19b6f4213ca1df53881783cc5ca9f7c7bd6dbe19 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 12 Sep 2026 00:41:12 +0000 Subject: [PATCH 16/34] manager: bump the maven group across 1 directory with 18 updates and other minimal fixes (#351) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bumps the maven group with 18 updates in the /manager directory - migrate nav framework to miuix-nav - refine PredictiveBackAnimation - deny HomePage refresh - navigation badge settings - home cards icon settings - refine OpenSourceLicense page Close #383 --------- Signed-off-by: dependabot[bot] Signed-off-by: AlexLiuDev233 Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: YC酱luyancib Co-authored-by: YC酱luyancib <2058282593@qq.com> Co-authored-by: wxxsfxyzm <65166044+wxxsfxyzm@users.noreply.github.com> Co-authored-by: AlexLiuDev233 --- manager/app/build.gradle.kts | 11 +- manager/app/proguard-rules.pro | 1 - .../LocalNavigationEventDispatcherOwner.kt | 57 -- .../compose/NavigationEventHandler.kt | 431 ------------- .../compose/NavigationEventState.kt | 73 --- .../RememberNavigationEventDispatcherOwner.kt | 93 --- .../compose/RememberNavigationEventState.kt | 53 -- .../data/system/HomeRuntimeRepository.kt | 11 +- .../resukisu/domain/model/HomeRuntime.kt | 2 + .../domain/usecase/HomeRuntimeUseCases.kt | 6 +- .../com/resukisu/resukisu/ui/NavContainer.kt | 610 +++++++++++------- .../ui/activity/component/NavigationBar.kt | 16 +- .../AOSPCrossActivityAnimation.kt | 174 ----- .../predictiveback/AospNavTransition.kt | 263 ++++++++ .../predictiveback/ClassicNavTransition.kt | 63 ++ .../predictiveback/InstallerNavTransition.kt | 19 + .../KernelSUClassicPredictiveBackAnimation.kt | 54 -- .../MiuixPredictiveBackAnimation.kt | 44 -- .../predictiveback/NavTransitionEasing.kt | 31 + .../predictiveback/NavTransitionGeometry.kt | 19 + .../NoPredictiveBackAnimation.kt | 63 -- .../NoPredictiveBackTransition.kt | 116 ++++ .../PredictiveBackAnimationHandler.kt | 81 --- .../predictiveback/ScaleNavTransition.kt | 122 ++++ .../ScalePredictiveBackAnimation.kt | 195 ------ .../ui/component/settings/SegmentedColumn.kt | 26 +- .../component/settings/SettingsBaseWidget.kt | 62 +- .../settings/SettingsDropdownWidget.kt | 8 +- .../resukisu/ui/navigation/Navigator.kt | 33 +- .../resukisu/resukisu/ui/navigation/Routes.kt | 5 +- .../resukisu/resukisu/ui/screen/AppProfile.kt | 76 +-- .../ui/screen/DynamicManagerScreen.kt | 2 + .../resukisu/ui/screen/ExecuteModuleAction.kt | 4 +- .../com/resukisu/resukisu/ui/screen/Flash.kt | 3 +- .../resukisu/resukisu/ui/screen/Install.kt | 2 + .../resukisu/ui/screen/SulogScreen.kt | 26 +- .../resukisu/resukisu/ui/screen/Template.kt | 20 +- .../resukisu/ui/screen/TemplateEditor.kt | 16 +- .../resukisu/ui/screen/UmountManagerScreen.kt | 2 + .../resukisu/ui/screen/about/About.kt | 8 +- .../screen/about/OpenSourceLicenseScreen.kt | 80 +-- .../ui/screen/kernelFlash/KernelFlash.kt | 9 +- .../resukisu/ui/screen/main/HomePage.kt | 103 +-- .../resukisu/ui/screen/main/MainScreen.kt | 139 ++-- .../resukisu/ui/screen/main/ModulePage.kt | 23 +- .../resukisu/ui/screen/main/SettingsPage.kt | 11 +- .../resukisu/ui/screen/main/SuperUserPage.kt | 29 +- .../ui/screen/moduleRepo/ModuleRepo.kt | 17 +- .../screen/moduleRepo/OnlineModuleDetail.kt | 8 +- .../resukisu/ui/screen/susfs/SuSFSConfig.kt | 8 +- .../ui/screen/themeSettings/ThemeSettings.kt | 91 +-- .../crop/BackgroundCropActivity.kt | 2 + .../resukisu/ui/util/CompositionProvider.kt | 1 + .../resukisu/ui/util/ScaffoldWindowInsets.kt | 16 + .../resukisu/ui/viewmodel/HomeViewModel.kt | 36 +- .../src/main/res/values-zh-rCN/strings.xml | 4 + manager/app/src/main/res/values/strings.xml | 4 + manager/gradle/libs.versions.toml | 27 +- manager/gradle/wrapper/gradle-wrapper.jar | Bin 48462 -> 47505 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- manager/gradlew | 4 +- manager/gradlew.bat | 4 +- 62 files changed, 1519 insertions(+), 2000 deletions(-) delete mode 100644 manager/app/src/main/java/androidx/navigationevent/compose/LocalNavigationEventDispatcherOwner.kt delete mode 100644 manager/app/src/main/java/androidx/navigationevent/compose/NavigationEventHandler.kt delete mode 100644 manager/app/src/main/java/androidx/navigationevent/compose/NavigationEventState.kt delete mode 100644 manager/app/src/main/java/androidx/navigationevent/compose/RememberNavigationEventDispatcherOwner.kt delete mode 100644 manager/app/src/main/java/androidx/navigationevent/compose/RememberNavigationEventState.kt delete mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/AOSPCrossActivityAnimation.kt create mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/AospNavTransition.kt create mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/ClassicNavTransition.kt create mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/InstallerNavTransition.kt delete mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/KernelSUClassicPredictiveBackAnimation.kt delete mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/MiuixPredictiveBackAnimation.kt create mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NavTransitionEasing.kt create mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NavTransitionGeometry.kt delete mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NoPredictiveBackAnimation.kt create mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NoPredictiveBackTransition.kt delete mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/PredictiveBackAnimationHandler.kt create mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/ScaleNavTransition.kt delete mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/ScalePredictiveBackAnimation.kt create mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/util/ScaffoldWindowInsets.kt diff --git a/manager/app/build.gradle.kts b/manager/app/build.gradle.kts index d54200a83..5dcd71ae8 100644 --- a/manager/app/build.gradle.kts +++ b/manager/app/build.gradle.kts @@ -172,10 +172,6 @@ base { ) } -configurations.all { - exclude(group = "androidx.navigationevent", module = "navigationevent-compose") -} - aboutLibraries { library { // Enable the duplication mode, allows to merge, or link dependencies which relate @@ -217,14 +213,9 @@ dependencies { implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.viewmodel.compose) - implementation(libs.androidx.lifecycle.viewmodel.navigation3) - implementation(libs.androidx.navigation3.runtime) implementation(libs.miuix.blur) - implementation(libs.miuix.navigation) - implementation(libs.androidx.navigationevent) { - exclude(group = "androidx.navigation", module = "navigationevent-compose") - } + implementation(libs.miuix.nav) implementation(libs.aboutlibraries.core) implementation(libs.aboutlibraries.compose.m3) diff --git a/manager/app/proguard-rules.pro b/manager/app/proguard-rules.pro index 2e50898c5..77ed8dedd 100644 --- a/manager/app/proguard-rules.pro +++ b/manager/app/proguard-rules.pro @@ -37,7 +37,6 @@ -dontwarn javax.lang.model.util.SimpleTypeVisitor8 -dontwarn javax.lang.model.util.Types -dontwarn javax.tools.Diagnostic$Kind --dontwarn androidx.navigationevent.compose.RememberNavigationEventStateKt** -dontwarn com.yalantis.ucrop** -keep class com.yalantis.ucrop** { *; } -keep interface com.yalantis.ucrop** { *; } diff --git a/manager/app/src/main/java/androidx/navigationevent/compose/LocalNavigationEventDispatcherOwner.kt b/manager/app/src/main/java/androidx/navigationevent/compose/LocalNavigationEventDispatcherOwner.kt deleted file mode 100644 index 799997532..000000000 --- a/manager/app/src/main/java/androidx/navigationevent/compose/LocalNavigationEventDispatcherOwner.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.navigationevent.compose - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.ProvidedValue -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.ui.platform.LocalView -import androidx.navigationevent.NavigationEventDispatcher -import androidx.navigationevent.NavigationEventDispatcherOwner -import androidx.navigationevent.findViewTreeNavigationEventDispatcherOwner - -/** The CompositionLocal containing the current [NavigationEventDispatcher]. */ -object LocalNavigationEventDispatcherOwner { - private val LocalNavigationEventDispatcherOwner = - compositionLocalOf { null } - - /** - * Returns current composition local value for the owner or `null` if one has not been provided - * nor is one available via [findViewTreeNavigationEventDispatcherOwner] on the current - * `androidx.compose.ui.platform.LocalView`. - */ - val current: NavigationEventDispatcherOwner? - @Composable - get() = - LocalNavigationEventDispatcherOwner.current - ?: findViewTreeNavigationEventDispatcherOwner() - - /** - * Associates a [LocalNavigationEventDispatcherOwner] key to a value in a call to - * [CompositionLocalProvider]. - */ - infix fun provides( - navigationEventDispatcherOwner: NavigationEventDispatcherOwner - ): ProvidedValue { - return LocalNavigationEventDispatcherOwner.provides(navigationEventDispatcherOwner) - } -} - -@Composable -internal fun findViewTreeNavigationEventDispatcherOwner(): NavigationEventDispatcherOwner? = - LocalView.current.findViewTreeNavigationEventDispatcherOwner() \ No newline at end of file diff --git a/manager/app/src/main/java/androidx/navigationevent/compose/NavigationEventHandler.kt b/manager/app/src/main/java/androidx/navigationevent/compose/NavigationEventHandler.kt deleted file mode 100644 index 8fb728839..000000000 --- a/manager/app/src/main/java/androidx/navigationevent/compose/NavigationEventHandler.kt +++ /dev/null @@ -1,431 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.navigationevent.compose - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.navigationevent.NavigationEvent -import androidx.navigationevent.NavigationEventHandler -import androidx.navigationevent.NavigationEventInfo -import androidx.navigationevent.NavigationEventTransitionState - -/** - * A composable that handles navigation events using simple lambda handlers, driven by a manually - * hoisted [NavigationEventState]. - * - * This is the core implementation of the navigation event handler. This overload must be used when - * you need to hoist the [NavigationEventState] (by calling [rememberNavigationEventState] at a - * higher level). Hoisting is necessary when other composables need to react to the gesture's - * [NavigationEventTransitionState] (held within the `state` object), for example, to drive custom - * animations. - * - * ## Precedence - * When multiple [NavigationEventHandler] are present in the composition, the one that is composed - * *last* among all enabled handlers will be invoked. - * - * ## Usage - * It is important to call this composable **unconditionally**. Use [isBackEnabled] and - * [isForwardEnabled] to control whether the handler is active. This is preferable to conditionally - * calling [NavigationEventHandler] (e.g., inside an `if` block), as conditional calls can change - * the order of composition, leading to unpredictable behavior where different handlers are invoked - * after recomposition. - * - * ## Timing Consideration - * There are cases where a predictive back or forward gesture may be dispatched within a rendering - * frame before the corresponding `enabled` flag is updated, which can cause unexpected behavior - * (see [b/375343407](https://issuetracker.google.com/375343407), - * [b/384186542](https://issuetracker.google.com/384186542)). For example, if [isBackEnabled] is set - * to `false`, a back gesture initiated in the same frame may still trigger this handler because the - * system sees the stale `true` value. - * - * @param state The hoisted [NavigationEventState] (returned from [rememberNavigationEventState]) to - * be registered. This object links this handler's callbacks to the unique handler instance that - * is producing the state. - * @param isForwardEnabled Controls whether forward navigation gestures are handled. - * @param onForwardCancelled Called if a forward navigation gesture is cancelled. - * @param onForwardCompleted Called when a forward navigation gesture completes. - * @param isBackEnabled Controls whether back navigation gestures are handled. - * @param onBackCancelled Called if a back navigation gesture is cancelled. - * @param onBackCompleted Called when a back navigation gesture completes. - * @throws IllegalArgumentException If the provided [NavigationEventState] is passed to multiple - * [NavigationEventHandler] Composable. Each handler must have its own unique state. - */ -@Composable -fun NavigationEventHandler( - state: NavigationEventState, - // ---- Forward Events ---- - isForwardEnabled: Boolean = true, - onForwardCancelled: (() -> Unit) -> Unit = { callBack -> - callBack() - }, - onForwardCompleted: (() -> Unit) -> Unit = { callBack -> - callBack() - }, - // ---- Back Events ---- - isBackEnabled: Boolean = true, - onBackCancelled: (() -> Unit) -> Unit = { callBack -> - callBack() - }, - onBackCompleted: (() -> Unit) -> Unit = { callBack -> - callBack() - }, -) { - if (LocalInspectionMode.current) { - // TODO(b/462365661): Return early to prevent Preview crashes. Future work should implement - // full support for navigation events in Interactive Previews instead of disabling them. - return - } - - val dispatcher = - checkNotNull(LocalNavigationEventDispatcherOwner.current) { - "No NavigationEventDispatcher was provided via LocalNavigationEventDispatcherOwner" - } - .navigationEventDispatcher - - val sourceHandler = - remember(state) { - ComposeNavigationEventHandler( - initialInfo = state.currentInfo, - onTransitionStateChanged = { transitionState -> - state.transitionState = transitionState - }, - ) - } - - SideEffect { - sourceHandler.isForwardEnabled = isForwardEnabled - sourceHandler.currentOnForwardCancelled = onForwardCancelled - sourceHandler.currentOnForwardCompleted = onForwardCompleted - - sourceHandler.isBackEnabled = isBackEnabled - sourceHandler.currentOnBackCancelled = onBackCancelled - sourceHandler.currentOnBackCompleted = onBackCompleted - - sourceHandler.setInfo(state.currentInfo, state.backInfo, state.forwardInfo) - } - - DisposableEffect(state) { - require(state.sourceHandler == null) { - "NavigationEventState '$state' is already registered with a NavigationEventHandler '$sourceHandler'." - } - - state.sourceHandler = sourceHandler - dispatcher.addHandler(sourceHandler) - - onDispose { - sourceHandler.remove() - state.sourceHandler = null - } - } -} - -/** - * A composable that handles only back navigation gestures, driven by a manually hoisted - * [NavigationEventState]. - * - * This is a convenience wrapper around the core [NavigationEventHandler] overload for cases where - * forward navigation is not relevant. Use this overload when hoisting state (e.g., for custom - * animations). - * - * Refer to the primary [NavigationEventHandler] KDoc for details on precedence, unconditional - * usage, and timing considerations. - * - * @param state The hoisted [NavigationEventState] (returned from [rememberNavigationEventState]) to - * be registered. - * @param isBackEnabled Controls whether back navigation gestures are handled. - * @param onBackCancelled Called if a back navigation gesture is cancelled. - * @param onBackCompleted Called when a back navigation gesture completes and navigation occurs. - */ -@Composable -fun NavigationBackHandler( - state: NavigationEventState, - isBackEnabled: Boolean = true, - onBackCancelled: (() -> Unit) -> Unit = { callback -> - callback() - }, - onBackCompleted: (() -> Unit) -> Unit, -) { - NavigationEventHandler( - state = state, - onForwardCancelled = { - - }, - onForwardCompleted = {}, - isForwardEnabled = false, // disable forward - onBackCancelled = onBackCancelled, - onBackCompleted = onBackCompleted, - isBackEnabled = isBackEnabled, - ) -} - -/** - * A composable that handles only forward navigation gestures, driven by a manually hoisted - * [NavigationEventState]. - * - * This is a convenience wrapper around the core [NavigationEventHandler] overload for cases where - * back navigation is not relevant. Use this overload when hoisting state. - * - * Refer to the primary [NavigationEventHandler] KDoc for details on precedence, unconditional - * usage, and timing considerations. - * - * @param state The hoisted [NavigationEventState] (returned from [rememberNavigationEventState]) to - * be registered. - * @param isForwardEnabled Controls whether forward navigation gestures are handled. - * @param onForwardCancelled Called if a forward navigation gesture is cancelled. - * @param onForwardCompleted Called when a forward navigation gesture completes and navigation - * occurs. - */ -@Composable -fun NavigationForwardHandler( - state: NavigationEventState, - isForwardEnabled: Boolean = true, - onForwardCancelled: (() -> Unit) -> Unit = { callBack -> - callBack() - }, - onForwardCompleted: (() -> Unit) -> Unit, -) { - NavigationEventHandler( - state = state, - onForwardCancelled = onForwardCancelled, - onForwardCompleted = onForwardCompleted, - isForwardEnabled = isForwardEnabled, - onBackCancelled = { callBack -> callBack() }, - onBackCompleted = { callBack -> callBack() }, - isBackEnabled = false, // disable back - ) -} - -/** A simple [NavigationEventHandler] that delegates its methods to lambda functions. */ -private class ComposeNavigationEventHandler( - initialInfo: T, - private val onTransitionStateChanged: (NavigationEventTransitionState) -> Unit = {}, -) : - NavigationEventHandler( - initialInfo = initialInfo, - isBackEnabled = false, - isForwardEnabled = false, - ) { - - var currentOnForwardCancelled: (() -> Unit) -> Unit = {} - var currentOnForwardCompleted: (() -> Unit) -> Unit = {} - var currentOnBackCancelled: (() -> Unit) -> Unit = {} - var currentOnBackCompleted: (() -> Unit) -> Unit = {} - - override fun onForwardStarted(event: NavigationEvent) { - onTransitionStateChanged(transitionState) - } - - override fun onForwardProgressed(event: NavigationEvent) { - onTransitionStateChanged(transitionState) - } - - override fun onForwardCancelled() { - currentOnForwardCancelled.invoke { - onTransitionStateChanged(transitionState) - } - } - - override fun onForwardCompleted() { - currentOnForwardCompleted.invoke { - onTransitionStateChanged(transitionState) - } - } - - override fun onBackStarted(event: NavigationEvent) { - onTransitionStateChanged(transitionState) - } - - override fun onBackProgressed(event: NavigationEvent) { - onTransitionStateChanged(transitionState) - } - - override fun onBackCancelled() { - currentOnBackCancelled.invoke { - onTransitionStateChanged(transitionState) - } - } - - override fun onBackCompleted() { - currentOnBackCompleted.invoke { - onTransitionStateChanged(transitionState) - } - } -} - -// Compatible with the fucking miuix -/** - * A composable that handles only back navigation gestures, driven by a manually hoisted - * [NavigationEventState]. - * - * This is a convenience wrapper around the core [NavigationEventHandler] overload for cases where - * forward navigation is not relevant. Use this overload when hoisting state (e.g., for custom - * animations). - * - * Refer to the primary [NavigationEventHandler] KDoc for details on precedence, unconditional - * usage, and timing considerations. - * - * @param state The hoisted [NavigationEventState] (returned from [rememberNavigationEventState]) to - * be registered. - * @param isBackEnabled Controls whether back navigation gestures are handled. - * @param onBackCancelled Called if a back navigation gesture is cancelled. - * @param onBackCompleted Called when a back navigation gesture completes and navigation occurs. - */ -@Composable -@Suppress("unused") // Reason: Miuix Library use that -fun NavigationBackHandler( - state: NavigationEventState, - isBackEnabled: Boolean = true, - onBackCancelled: () -> Unit = {}, - onBackCompleted: () -> Unit, -) { - NavigationEventHandler( - state = state, - onForwardCancelled = {}, - onForwardCompleted = {}, - isForwardEnabled = false, // disable forward - onBackCancelled = { callBack -> - callBack() - onBackCancelled() - }, - onBackCompleted = { callBack -> - callBack() - onBackCompleted() - }, - isBackEnabled = isBackEnabled, - ) -} - -/** - * A composable that handles navigation events using simple lambda handlers, driven by a manually - * hoisted [NavigationEventState]. - * - * This is the core implementation of the navigation event handler. This overload must be used when - * you need to hoist the [NavigationEventState] (by calling [rememberNavigationEventState] at a - * higher level). Hoisting is necessary when other composables need to react to the gesture's - * [NavigationEventTransitionState] (held within the `state` object), for example, to drive custom - * animations. - * - * ## Precedence - * When multiple [NavigationEventHandler] are present in the composition, the one that is composed - * *last* among all enabled handlers will be invoked. - * - * ## Usage - * It is important to call this composable **unconditionally**. Use [isBackEnabled] and - * [isForwardEnabled] to control whether the handler is active. This is preferable to conditionally - * calling [NavigationEventHandler] (e.g., inside an `if` block), as conditional calls can change - * the order of composition, leading to unpredictable behavior where different handlers are invoked - * after recomposition. - * - * ## Timing Consideration - * There are cases where a predictive back or forward gesture may be dispatched within a rendering - * frame before the corresponding `enabled` flag is updated, which can cause unexpected behavior - * (see [b/375343407](https://issuetracker.google.com/375343407), - * [b/384186542](https://issuetracker.google.com/384186542)). For example, if [isBackEnabled] is set - * to `false`, a back gesture initiated in the same frame may still trigger this handler because the - * system sees the stale `true` value. - * - * @param state The hoisted [NavigationEventState] (returned from [rememberNavigationEventState]) to - * be registered. This object links this handler's callbacks to the unique handler instance that - * is producing the state. - * @param isForwardEnabled Controls whether forward navigation gestures are handled. - * @param onForwardCancelled Called if a forward navigation gesture is cancelled. - * @param onForwardCompleted Called when a forward navigation gesture completes. - * @param isBackEnabled Controls whether back navigation gestures are handled. - * @param onBackCancelled Called if a back navigation gesture is cancelled. - * @param onBackCompleted Called when a back navigation gesture completes. - * @throws IllegalArgumentException If the provided [NavigationEventState] is passed to multiple - * [NavigationEventHandler] Composable. Each handler must have its own unique state. - */ -@Composable -@Suppress("unused") // Reason: Keep same ABI -fun NavigationEventHandler( - state: NavigationEventState, - // ---- Forward Events ---- - isForwardEnabled: Boolean = true, - onForwardCancelled: () -> Unit = {}, - onForwardCompleted: () -> Unit = {}, - // ---- Back Events ---- - isBackEnabled: Boolean = true, - onBackCancelled: () -> Unit = {}, - onBackCompleted: () -> Unit = {}, -) { - NavigationEventHandler( - state, - isForwardEnabled, - onForwardCancelled = { callBack -> - callBack() - onForwardCancelled() - }, - onForwardCompleted = { callBack -> - callBack() - onForwardCompleted() - }, - isBackEnabled, - onBackCancelled = { callBack -> - callBack() - onBackCancelled() - }, - onBackCompleted = { callBack -> - callBack() - onBackCompleted() - } - ) -} - -/** - * A composable that handles only forward navigation gestures, driven by a manually hoisted - * [NavigationEventState]. - * - * This is a convenience wrapper around the core [NavigationEventHandler] overload for cases where - * back navigation is not relevant. Use this overload when hoisting state. - * - * Refer to the primary [NavigationEventHandler] KDoc for details on precedence, unconditional - * usage, and timing considerations. - * - * @param state The hoisted [NavigationEventState] (returned from [rememberNavigationEventState]) to - * be registered. - * @param isForwardEnabled Controls whether forward navigation gestures are handled. - * @param onForwardCancelled Called if a forward navigation gesture is cancelled. - * @param onForwardCompleted Called when a forward navigation gesture completes and navigation - * occurs. - */ -@Composable -fun NavigationForwardHandler( - state: NavigationEventState, - isForwardEnabled: Boolean = true, - onForwardCancelled: () -> Unit = {}, - onForwardCompleted: () -> Unit, -) { - NavigationEventHandler( - state = state, - onForwardCancelled = { callBack -> - callBack() - onForwardCancelled() - }, - onForwardCompleted = { callBack -> - callBack() - onForwardCompleted() - }, - isForwardEnabled = isForwardEnabled, - onBackCancelled = { callBack -> callBack() }, - onBackCompleted = { callBack -> callBack() }, - isBackEnabled = false, // disable back - ) -} \ No newline at end of file diff --git a/manager/app/src/main/java/androidx/navigationevent/compose/NavigationEventState.kt b/manager/app/src/main/java/androidx/navigationevent/compose/NavigationEventState.kt deleted file mode 100644 index 6d4d8f61a..000000000 --- a/manager/app/src/main/java/androidx/navigationevent/compose/NavigationEventState.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.navigationevent.compose - -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.navigationevent.NavigationEventHandler -import androidx.navigationevent.NavigationEventInfo -import androidx.navigationevent.NavigationEventTransitionState -import androidx.navigationevent.NavigationEventTransitionState.Idle - -/** - * This class serves as the Compose-layer adapter for the navigation event system. It holds the - * developer-defined history partitions ([currentInfo], [backInfo], [forwardInfo]) and is updated - * with the local [transitionState] by the [NavigationEventHandler] it is provided to. - * - * This object is created via [rememberNavigationEventState] and consumed by - * [NavigationEventHandler] to link the hoisted history state with the active handler's callbacks - * and gesture state. - * - * @see androidx.navigationevent.compose.NavigationEventHandler - */ -@Stable -class NavigationEventState -internal constructor( - currentInfo: T, - backInfo: List = emptyList(), - forwardInfo: List = emptyList(), -) { - - /** - * The current physical gesture state from the dispatcher. This value is collected from the - * local [NavigationEventHandler] and will be either [NavigationEventTransitionState.Idle] or - * [NavigationEventTransitionState.InProgress]. This property will update frequently during a - * gesture. - */ - var transitionState: NavigationEventTransitionState by mutableStateOf(Idle) - - /** History partitions relative to the current position. */ - - /** A list of destinations the user may navigate back to. */ - var backInfo: List by mutableStateOf(backInfo) - - /** The contextual information for the currently active destination. */ - var currentInfo: T by mutableStateOf(currentInfo) - - /** A list of destinations the user may navigate forward to. */ - var forwardInfo: List by mutableStateOf(forwardInfo) - - /** - * The internal handler instance associated with this state object. This handler is created and - * remembered by [rememberNavigationEventState] and is registered with the dispatcher when - * passed to [NavigationEventHandler]. This guarantees the link between the hoisted state and - * the active handler. - */ - var sourceHandler: NavigationEventHandler? = null -} diff --git a/manager/app/src/main/java/androidx/navigationevent/compose/RememberNavigationEventDispatcherOwner.kt b/manager/app/src/main/java/androidx/navigationevent/compose/RememberNavigationEventDispatcherOwner.kt deleted file mode 100644 index 29ac26da2..000000000 --- a/manager/app/src/main/java/androidx/navigationevent/compose/RememberNavigationEventDispatcherOwner.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.navigationevent.compose - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.navigationevent.NavigationEventDispatcher -import androidx.navigationevent.NavigationEventDispatcherOwner -import androidx.navigationevent.NavigationEventInput - -/** - * Remembers a new [NavigationEventDispatcherOwner] which creates a dispatcher linked to a parent - * dispatcher found in the composition. - * - * This composable creates a dispatcher that links to any parent dispatcher found in the - * composition, forming a parent-child relationship. If no parent exists, it automatically becomes a - * new root dispatcher, this is the top-most parent in a hierarchy. This is useful for isolating - * navigation handling within specific UI sections, such as a self-contained feature screen or tab. - * - * The dispatcher's lifecycle is automatically managed. It is created only once and automatically - * disposed of when the composable leaves the composition, preventing memory leaks. - * - * When used to create a root dispatcher, you must use a [NavigationEventInput] to send it events. - * Otherwise, the dispatcher will be detached and will not receive events. - * - * To provide the new [NavigationEventDispatcherOwner] to a sub-composition, use - * [androidx.compose.runtime.CompositionLocalProvider]: - * - * @samples androidx.navigationevent.compose.samples.RememberNavigationEventDispatcherOwner - * - * **Null parent:** If [parent] is **EXPLICITLY** `null`, this creates a root dispatcher that runs - * independently. By default, it requires a parent from the [LocalNavigationEventDispatcherOwner] - * and will throw an [IllegalStateException] if one is not present. - * - * @param enabled Controls if the dispatcher is active. If this value changes, the dispatcher's - * `isEnabled` property will be updated. When `false`, this dispatcher and any of its children - * will not receive events. Defaults to `true`. - * @param parent The [NavigationEventDispatcherOwner] to use as the parent, or `null` if it is a - * root. Defaults to the owner from [LocalNavigationEventDispatcherOwner]. - * @return A new [NavigationEventDispatcherOwner] that is remembered across compositions. - */ -@Composable -fun rememberNavigationEventDispatcherOwner( - enabled: Boolean = true, - parent: NavigationEventDispatcherOwner? = - checkNotNull(LocalNavigationEventDispatcherOwner.current) { - "No NavigationEventDispatcherOwner provided in LocalNavigationEventDispatcherOwner. " + - "If you intended to create a root dispatcher, explicitly pass null as the parent." - }, -): NavigationEventDispatcherOwner { - val localDispatcher = - remember(parent) { - // If a parent dispatcher exists, link to it. Otherwise, create a new root dispatcher. - if (parent != null) { - NavigationEventDispatcher(parent = parent.navigationEventDispatcher) - } else { - NavigationEventDispatcher() - } - } - - LaunchedEffect(enabled) { localDispatcher.isEnabled = enabled } - - // Clean up the dispatcher on dispose to prevent memory leaks. - DisposableEffect(localDispatcher) { onDispose { localDispatcher.dispose() } } - - return remember(localDispatcher) { - ComposeNavigationEventDispatcherOwner(navigationEventDispatcher = localDispatcher) - } -} - -/** - * A private, concrete implementation of [NavigationEventDispatcherOwner] that simply holds a given - * [NavigationEventDispatcher]. - */ -private class ComposeNavigationEventDispatcherOwner( - override val navigationEventDispatcher: NavigationEventDispatcher -) : NavigationEventDispatcherOwner diff --git a/manager/app/src/main/java/androidx/navigationevent/compose/RememberNavigationEventState.kt b/manager/app/src/main/java/androidx/navigationevent/compose/RememberNavigationEventState.kt deleted file mode 100644 index e2c9712ec..000000000 --- a/manager/app/src/main/java/androidx/navigationevent/compose/RememberNavigationEventState.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.navigationevent.compose - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.remember -import androidx.navigationevent.NavigationEventInfo - -/** - * Remembers and returns a [NavigationEventState] instance. - * - * This composable creates and remembers a [NavigationEventState] object, which holds a - * [NavigationEventHandler] internally. This is the state object that can be passed to - * [NavigationEventHandler] (the composable) to "hoist" the state. - * - * The state's handler info (currentInfo, backInfo, forwardInfo) is kept in sync with the provided - * parameters via a [SideEffect]. - * - * @param T The type of [NavigationEventInfo] this state will manage. - * @param currentInfo The object representing the current destination. - * @param backInfo A list of destinations the user may navigate back to (nearest-first). - * @param forwardInfo A list of destinations the user may navigate forward to (nearest-first). - * @return A stable, remembered [NavigationEventState] instance. - */ -@Composable -fun rememberNavigationEventState( - currentInfo: T, - backInfo: List = emptyList(), - forwardInfo: List = emptyList(), -): NavigationEventState { - val state = remember { NavigationEventState(currentInfo, backInfo, forwardInfo) } - SideEffect { - state.currentInfo = currentInfo - state.backInfo = backInfo - state.forwardInfo = forwardInfo - } - return state -} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/data/system/HomeRuntimeRepository.kt b/manager/app/src/main/java/com/resukisu/resukisu/data/system/HomeRuntimeRepository.kt index 55c7b4461..005bc6719 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/data/system/HomeRuntimeRepository.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/data/system/HomeRuntimeRepository.kt @@ -15,7 +15,10 @@ class HomeRuntimeRepository( private val application: Application, private val ksuCliRepository: KsuCliRepository, ) { - suspend fun getBasicInfo(managerUapiVersion: Int): HomeBasicInfo = + suspend fun getBasicInfo( + managerUapiVersion: Int, + includeSelinuxStatus: Boolean = true, + ): HomeBasicInfo = withContext(Dispatchers.IO) { val uname = runCatching { Os.uname() }.getOrNull() HomeBasicInfo( @@ -27,7 +30,11 @@ class HomeRuntimeRepository( BuildConfig.VERSION_CODE, managerUapiVersion, ), - selinuxStatus = runCatching { getSELinuxStatus(application) }.getOrDefault("Unknown"), + selinuxStatus = if (includeSelinuxStatus) { + runCatching { getSELinuxStatus(application) }.getOrDefault("Unknown") + } else { + "" + }, seccompStatus = runCatching { Os.prctl(21, 0, 0, 0, 0) }.getOrDefault(-1), ) } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/HomeRuntime.kt b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/HomeRuntime.kt index beb9c0a74..b35a8641a 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/HomeRuntime.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/HomeRuntime.kt @@ -43,6 +43,8 @@ data class HomeDashboardState( val betaManagerUpdate: ManagerUpdateInfo? = null, val isBetaManagerUpdateCheckFailed: Boolean = false, val isSimpleMode: Boolean = false, + val showNavigationBarBadge: Boolean = true, + val showHomeCardIcons: Boolean = false, val isInitialDataLoaded: Boolean = false, val isCoreDataLoaded: Boolean = false, val isExtendedDataLoaded: Boolean = false, diff --git a/manager/app/src/main/java/com/resukisu/resukisu/domain/usecase/HomeRuntimeUseCases.kt b/manager/app/src/main/java/com/resukisu/resukisu/domain/usecase/HomeRuntimeUseCases.kt index 5de1a02db..e1170e73f 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/domain/usecase/HomeRuntimeUseCases.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/domain/usecase/HomeRuntimeUseCases.kt @@ -4,8 +4,10 @@ import com.resukisu.resukisu.data.network.NetworkStatusRepository import com.resukisu.resukisu.data.system.HomeRuntimeRepository class GetHomeBasicInfoUseCase(private val repository: HomeRuntimeRepository) { - suspend operator fun invoke(managerUapiVersion: Int) = - repository.getBasicInfo(managerUapiVersion) + suspend operator fun invoke( + managerUapiVersion: Int, + includeSelinuxStatus: Boolean = true, + ) = repository.getBasicInfo(managerUapiVersion, includeSelinuxStatus) } class GetHomeModuleOverviewUseCase(private val repository: HomeRuntimeRepository) { diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/NavContainer.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/NavContainer.kt index b2a348bdf..1ecbadbae 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/NavContainer.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/NavContainer.kt @@ -11,10 +11,15 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.LocalOverscrollFactory import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.pager.PagerState import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -23,9 +28,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.paint @@ -42,38 +45,25 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.core.app.ActivityCompat import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator -import androidx.navigation3.runtime.NavEntryDecorator -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberDecoratedNavEntries -import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator -import androidx.navigation3.scene.SceneInfo -import androidx.navigation3.scene.SinglePaneSceneStrategy -import androidx.navigation3.scene.rememberSceneState -import androidx.navigation3.ui.NavDisplay +import androidx.navigationevent.NavigationEventInfo import androidx.navigationevent.compose.NavigationBackHandler -import androidx.navigationevent.compose.NavigationEventState import androidx.navigationevent.compose.rememberNavigationEventState import com.resukisu.resukisu.ui.activity.PermissionRequestInterface -import com.resukisu.resukisu.ui.animation.predictiveback.AOSPCrossActivityAnimation -import com.resukisu.resukisu.ui.animation.predictiveback.KernelSUClassicPredictiveBackAnimation -import com.resukisu.resukisu.ui.animation.predictiveback.MiuixPredictiveBackAnimation -import com.resukisu.resukisu.ui.animation.predictiveback.NoPredictiveBackAnimation -import com.resukisu.resukisu.ui.animation.predictiveback.ScalePredictiveBackAnimation +import com.resukisu.resukisu.ui.animation.predictiveback.installerNavTransition import com.resukisu.resukisu.ui.component.InstallConfirmationDialog import com.resukisu.resukisu.ui.component.ZipFileDetector import com.resukisu.resukisu.ui.component.ZipFileInfo import com.resukisu.resukisu.ui.component.ZipType import com.resukisu.resukisu.ui.navigation.HandleDeepLink import com.resukisu.resukisu.ui.navigation.LocalNavigator +import com.resukisu.resukisu.ui.navigation.Navigator import com.resukisu.resukisu.ui.navigation.Route -import com.resukisu.resukisu.ui.navigation.rememberNavigator import com.resukisu.resukisu.ui.overscroll.StretchOverscrollCompensationState import com.resukisu.resukisu.ui.overscroll.rememberCustomOverscrollFactory import com.resukisu.resukisu.ui.screen.AppProfileScreen @@ -94,13 +84,16 @@ import com.resukisu.resukisu.ui.screen.moduleRepo.ModuleRepoScreen import com.resukisu.resukisu.ui.screen.moduleRepo.OnlineModuleDetailScreen import com.resukisu.resukisu.ui.screen.susfs.SuSFSConfigScreen import com.resukisu.resukisu.ui.screen.themeSettings.ThemeSettingsScreen +import com.resukisu.resukisu.ui.theme.BackgroundRenderState import com.resukisu.resukisu.ui.theme.LocalBackgroundRenderState import com.resukisu.resukisu.ui.theme.ThemeConfig import com.resukisu.resukisu.ui.util.LocalBackgroundBlurAnchor import com.resukisu.resukisu.ui.util.LocalBlurState import com.resukisu.resukisu.ui.util.LocalPermissionRequestInterface +import com.resukisu.resukisu.ui.util.LocalPortraitState import com.resukisu.resukisu.ui.util.LocalSnackbarHost import com.resukisu.resukisu.ui.util.LocalStretchOverscrollCompensationState +import com.resukisu.resukisu.ui.util.rememberDeviceCornerRadius import com.resukisu.resukisu.ui.viewmodel.MainIntentViewModel import com.resukisu.resukisu.ui.viewmodel.PredictiveBackAnimation import com.resukisu.resukisu.ui.viewmodel.SettingsViewModel @@ -116,6 +109,11 @@ import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel import top.yukonga.miuix.kmp.blur.LayerBackdrop import top.yukonga.miuix.kmp.blur.rememberLayerBackdrop +import top.yukonga.miuix.kmp.nav.core.NavCornerClipMode +import top.yukonga.miuix.kmp.nav.core.NavDisplay +import top.yukonga.miuix.kmp.nav.core.NavDisplayEffects +import top.yukonga.miuix.kmp.nav.core.rememberNavBackStack +import top.yukonga.miuix.kmp.nav.transition.NavSwipeDirection import top.yukonga.miuix.kmp.shader.isRenderEffectSupported import kotlin.coroutines.resume @@ -173,7 +171,24 @@ fun NavContainer( } } - val navigator = rememberNavigator(Route.Main) + val backStack = rememberNavBackStack(Route.Main) + val navigator = remember(backStack) { Navigator(backStack) } + val onBack = remember(navigator) { + { + when (val top = navigator.current()) { + is Route.TemplateEditor -> { + if (!top.readOnly) { + navigator.setResult("template_edit", true) + } else { + navigator.pop() + } + } + + else -> navigator.pop() + } + } + } + val useBlur = themeConfig.isEnableBlur lateinit var permissionRequestHandler: ManagedActivityResultLauncher, Map> @@ -334,226 +349,365 @@ fun NavContainer( } ) - val predictiveBackAnimationHandler = remember( + val navCornerRadius = rememberDeviceCornerRadius(defaultRadius = 0.dp) + val roundAllCorners = + settings.predictiveBackAnimation == PredictiveBackAnimation.AOSP || + settings.predictiveBackAnimation == PredictiveBackAnimation.Scale || + settings.predictiveBackAnimation == PredictiveBackAnimation.KernelSUClassic + val backdropColor = MaterialTheme.colorScheme.surfaceContainer + val effects = remember(navCornerRadius, roundAllCorners, backdropColor) { + NavDisplayEffects( + enableCornerClip = true, + cornerClipRadius = if (roundAllCorners && navCornerRadius <= 0.dp) 32.dp else navCornerRadius, + cornerClipMode = if (roundAllCorners) NavCornerClipMode.All else NavCornerClipMode.Leading, + dimAmount = 0.5f, + backdropColor = backdropColor, + blockInputDuringTransition = false, + ) + } + val transition = remember( settings.predictiveBackAnimation, settings.predictiveBackExitDirection ) { - when (settings.predictiveBackAnimation) { - PredictiveBackAnimation.None -> NoPredictiveBackAnimation() - PredictiveBackAnimation.AOSP -> AOSPCrossActivityAnimation(settings.predictiveBackExitDirection) - PredictiveBackAnimation.Scale -> ScalePredictiveBackAnimation( - settings.predictiveBackExitDirection - ) - - PredictiveBackAnimation.KernelSUClassic -> KernelSUClassicPredictiveBackAnimation() - PredictiveBackAnimation.MIUIX -> MiuixPredictiveBackAnimation() - } + installerNavTransition( + animation = settings.predictiveBackAnimation, + exitDirection = settings.predictiveBackExitDirection, + ) } + val swipeBackDirection = when (LocalLayoutDirection.current) { + LayoutDirection.Rtl -> NavSwipeDirection.RightToLeft + LayoutDirection.Ltr -> NavSwipeDirection.LeftToRight + } + val interceptPredictiveBack = + settings.predictiveBackAnimation == PredictiveBackAnimation.None && backStack.size > 1 - var gestureState: NavigationEventState>? = null - val navigationScope = rememberCoroutineScope() - val onBack: (() -> Unit) -> Unit = { callBack -> - navigationScope.launch { - predictiveBackAnimationHandler.onBackPressed( - transitionState = gestureState?.transitionState, - currentPageKey = navigator.current() - ) - - callBack() - - when (val top = navigator.current()) { - is Route.TemplateEditor -> { - if (!top.readOnly) { - navigator.setResult("template_edit", true) - } else { - navigator.pop() - } - } - - else -> navigator.pop() + NavDisplay( + backStack = backStack, + onBack = onBack, + transition = transition, + effects = effects, + ) { + entry(swipeDismiss = swipeBackDirection) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + AboutScreen() + } + } + entry(swipeDismiss = swipeBackDirection) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + OpenSourceLicenseScreen() + } + } + entry(swipeDismiss = swipeBackDirection) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + SulogScreen() + } + } + entry(swipeDismiss = NavSwipeDirection.None) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + MainScreen() + } + } + entry(swipeDismiss = swipeBackDirection) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + AppProfileTemplateScreen() + } + } + entry(swipeDismiss = NavSwipeDirection.None) { key -> + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + TemplateEditorScreen( + templateId = key.templateId, + readOnly = key.readOnly, + isCreation = key.isCreation, + ) + } + } + entry(swipeDismiss = swipeBackDirection) { key -> + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + AppProfileScreen(key.uid, key.packageName) + } + } + entry(swipeDismiss = swipeBackDirection) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + ModuleRepoScreen() + } + } + entry(swipeDismiss = swipeBackDirection) { key -> + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + OnlineModuleDetailScreen(key.moduleId) + } + } + entry(swipeDismiss = NavSwipeDirection.None) { key -> + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + InstallScreen(key.preselectedKernelUri) + } + } + entry(swipeDismiss = swipeBackDirection) { key -> + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + FlashScreen(key.toFlashIt()) + } + } + entry(swipeDismiss = swipeBackDirection) { key -> + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + ExecuteModuleActionScreen(key.moduleId) + } + } + entry(swipeDismiss = NavSwipeDirection.None) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + MainScreen() + } + } + entry(swipeDismiss = NavSwipeDirection.None) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + MainScreen() + } + } + entry(swipeDismiss = NavSwipeDirection.None) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + MainScreen() + } + } + entry(swipeDismiss = NavSwipeDirection.None) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + MainScreen() + } + } + entry(swipeDismiss = swipeBackDirection) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + ThemeSettingsScreen(settingsViewModel = settingsViewModel) + } + } + entry(swipeDismiss = swipeBackDirection) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + SuSFSConfigScreen() + } + } + entry(swipeDismiss = swipeBackDirection) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + UmountManagerScreen() + } + } + entry(swipeDismiss = swipeBackDirection) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + DynamicManagerScreen() + } + } + entry(swipeDismiss = NavSwipeDirection.None) { key -> + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + KernelFlashScreen(key.kernelUri, key.selectedSlot) } } } + } +} - val entries = - rememberDecoratedNavEntries( - backStack = navigator.backStack, - entryDecorators = listOf( - rememberSaveableStateHolderNavEntryDecorator(), - rememberViewModelStoreNavEntryDecorator(), - NavEntryDecorator( - onPop = { key -> - predictiveBackAnimationHandler.onPagePop( - contentPageKey = key, - animationScope = navigationScope - ) - } - ) { content -> - val snackBarHostState = remember { SnackbarHostState() } - var backgroundBlurAnchorCoordinates by remember { - mutableStateOf(null) - } +@Composable +private fun ManagerNavEntry( + interceptPredictiveBack: Boolean, + onBack: () -> Unit, + themeConfig: ThemeConfig, + backgroundRenderState: BackgroundRenderState, + useBlur: Boolean, + content: @Composable () -> Unit, +) { + val navigationEventState = rememberNavigationEventState(NavigationEventInfo.None) + NavigationBackHandler( + state = navigationEventState, + isBackEnabled = interceptPredictiveBack, + onBackCompleted = onBack, + ) + val snackBarHostState = remember { androidx.compose.material3.SnackbarHostState() } + var backgroundBlurAnchorCoordinates by remember { + mutableStateOf(null) + } - LaunchedEffect(backgroundRenderState.imagePainter) { - if (backgroundRenderState.imagePainter == null) { - backgroundBlurAnchorCoordinates = null - } - } + LaunchedEffect(backgroundRenderState.imagePainter) { + if (backgroundRenderState.imagePainter == null) { + backgroundBlurAnchorCoordinates = null + } + } - with(predictiveBackAnimationHandler) { - Box( - modifier = Modifier - .fillMaxSize() - .predictiveBackAnimationDecorator( - gestureState?.transitionState, - content.contentKey, - navigator.current() - ) - .then( - if (!themeConfig.backgroundImageLoaded) Modifier.background( - MaterialTheme.colorScheme.surfaceContainer - ) else Modifier - ) - ) { - val surfaceContainer = - MaterialTheme.colorScheme.surfaceContainer - - CompositionLocalProvider( - LocalBlurState provides rememberMaterial3BlurBackdrop( - themeConfig.isEnableBlur - ), - LocalSnackbarHost provides snackBarHostState, - LocalBackgroundBlurAnchor provides backgroundBlurAnchorCoordinates, - ) { - backgroundRenderState.imagePainter?.let { - Box( - modifier = Modifier - .fillMaxSize() - .zIndex(-1f) - .onGloballyPositioned { newCoordinates -> - backgroundBlurAnchorCoordinates = - newCoordinates.takeIf { coordinates -> - coordinates.isAttached - } - } - .paint( - painter = it, - contentScale = ContentScale.Crop, - ) - .drawWithContent { - drawContent() - drawRect( - color = surfaceContainer.copy( - alpha = themeConfig.backgroundDim - ) - ) - } - ) - } - content.Content() + BoxWithConstraints( + modifier = Modifier + .fillMaxSize() + .then( + if (!themeConfig.backgroundImageLoaded) Modifier.background( + MaterialTheme.colorScheme.surfaceContainer + ) else Modifier + ) + ) { + val isPortrait = maxWidth < maxHeight || (maxHeight / maxWidth > 1.4f) + val surfaceContainer = + MaterialTheme.colorScheme.surfaceContainer + + CompositionLocalProvider( + LocalPortraitState provides isPortrait, + LocalBlurState provides rememberMaterial3BlurBackdrop( + enableBlur = useBlur + ), + LocalSnackbarHost provides snackBarHostState, + LocalBackgroundBlurAnchor provides backgroundBlurAnchorCoordinates, + ) { + backgroundRenderState.imagePainter?.let { + Box( + modifier = Modifier + .fillMaxSize() + .zIndex(-1f) + .onGloballyPositioned { newCoordinates -> + backgroundBlurAnchorCoordinates = + newCoordinates.takeIf { coordinates -> + coordinates.isAttached } - } } - } - ), - entryProvider = entryProvider { - entry { AboutScreen() } - entry { OpenSourceLicenseScreen() } - entry { SulogScreen() } - entry { MainScreen() } - entry { AppProfileTemplateScreen() } - entry { key -> - TemplateEditorScreen( - templateId = key.templateId, - readOnly = key.readOnly, - isCreation = key.isCreation, - ) - } - entry { key -> AppProfileScreen(key.uid, key.packageName) } - entry { ModuleRepoScreen() } - entry { key -> - OnlineModuleDetailScreen( - key.moduleId + .paint( + painter = it, + contentScale = ContentScale.Crop, ) - } - entry { key -> InstallScreen(key.preselectedKernelUri) } - entry { key -> FlashScreen(key.toFlashIt()) } - entry { key -> - ExecuteModuleActionScreen( - key.moduleId - ) - } - entry { MainScreen() } - entry { MainScreen() } - entry { MainScreen() } - entry { MainScreen() } - entry { - ThemeSettingsScreen(settingsViewModel = settingsViewModel) - } - entry { SuSFSConfigScreen() } - entry { UmountManagerScreen() } - entry { DynamicManagerScreen() } - entry { key -> - KernelFlashScreen( - key.kernelUri, - key.selectedSlot - ) - } - }, - ) - - val sceneState = - rememberSceneState( - entries = entries, - sceneStrategies = listOf(SinglePaneSceneStrategy()), - sceneDecoratorStrategies = emptyList(), - sharedTransitionScope = null, - onBack = { - onBack {} - }, - ) - val scene = sceneState.currentScene - - // Predictive Back Handling - val currentInfo = SceneInfo(scene) - val previousSceneInfos = sceneState.previousScenes.map { SceneInfo(it) } - gestureState = rememberNavigationEventState( - currentInfo = currentInfo, - backInfo = previousSceneInfos - ) - - NavigationBackHandler( - state = gestureState, - isBackEnabled = scene.previousEntries.isNotEmpty(), - onBackCompleted = { callBack -> - onBack(callBack) - }, - onBackCancelled = { callBack -> - callBack() + .drawWithContent { + drawContent() + drawRect( + color = surfaceContainer.copy( + alpha = themeConfig.backgroundDim + ) + ) + } + ) } - ) - - NavDisplay( - sceneState = sceneState, - navigationEventState = gestureState, - contentAlignment = Alignment.TopStart, - sizeTransform = null, - predictivePopTransitionSpec = { swipeEdge -> - with(predictiveBackAnimationHandler) { - onPredictivePopTransitionSpec(swipeEdge = swipeEdge) - } - }, - popTransitionSpec = { - with(predictiveBackAnimationHandler) { - onPopTransitionSpec() - } - }, - transitionSpec = { - with(predictiveBackAnimationHandler) { - onTransitionSpec() - } - }, - ) + Box( + modifier = Modifier + .fillMaxSize() + .windowInsetsPadding( + WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal) + ) + ) { + content() + } + } } } @@ -635,7 +789,7 @@ fun rememberMaterial3BlurBackdrop( 0f } val physicalPageOffset = pageOffset * pagerViewportWidth * - if (layoutDirection == LayoutDirection.Ltr) 1f else -1f + if (layoutDirection == LayoutDirection.Ltr) 1f else -1f val backgroundOffset = pagerViewportLeft + physicalPageOffset val backgroundBitmap = backgroundRenderState.imageBitmap @@ -716,7 +870,7 @@ private fun ShortcutIntentHandler( .putExtra("from_webui_shortcut", true) .addFlags( Intent.FLAG_ACTIVITY_NEW_TASK or - Intent.FLAG_ACTIVITY_CLEAR_TASK + Intent.FLAG_ACTIVITY_CLEAR_TASK ) context.startActivity(webIntent) } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/activity/component/NavigationBar.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/activity/component/NavigationBar.kt index 473067c31..6f443c060 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/activity/component/NavigationBar.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/activity/component/NavigationBar.kt @@ -7,8 +7,8 @@ import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.Badge import androidx.compose.material3.BadgedBox @@ -56,6 +56,7 @@ fun NavigationBar( val uiState by homeViewModel.uiState.collectAsStateWithLifecycle() val superuserCount = uiState.systemInfo.superuserCount val moduleCount = uiState.systemInfo.moduleCount + val showNavigationBarBadge = uiState.showNavigationBarBadge val page = LocalSelectedPage.current val handlePageChange = LocalHandlePageChange.current @@ -63,7 +64,7 @@ fun NavigationBar( FlexibleBottomAppBar( modifier = modifier .windowInsetsPadding( - WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal) + WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal) ) .blurEffect( compensateHorizontalOverscroll = true, @@ -86,6 +87,7 @@ fun NavigationBar( }, superuserCount = superuserCount, moduleCount = moduleCount, + showNavigationBarBadge = showNavigationBarBadge, ) } } @@ -93,7 +95,7 @@ fun NavigationBar( WideNavigationRail( modifier = modifier .windowInsetsPadding( - WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal) + WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal) ) .blurEffect( compensateHorizontalOverscroll = true, @@ -121,6 +123,7 @@ fun NavigationBar( }, superuserCount = superuserCount, moduleCount = moduleCount, + showNavigationBarBadge = showNavigationBarBadge, ) } } @@ -134,6 +137,7 @@ private fun NavigationRailItem( onClick: () -> Unit, superuserCount: Int, moduleCount: Int, + showNavigationBarBadge: Boolean, ) { WideNavigationRailItem( railExpanded = false, @@ -146,6 +150,7 @@ private fun NavigationRailItem( dest = destination, superUser = superuserCount, module = moduleCount, + show = showNavigationBarBadge, ) } ) { @@ -175,6 +180,7 @@ private fun RowScope.BottomBarNavigationItem( onClick: () -> Unit, superuserCount: Int, moduleCount: Int, + showNavigationBarBadge: Boolean, ) { NavigationBarItem( selected = isSelected, @@ -186,6 +192,7 @@ private fun RowScope.BottomBarNavigationItem( dest = destination, superUser = superuserCount, module = moduleCount, + show = showNavigationBarBadge, ) } ) { @@ -214,6 +221,7 @@ private fun DestinationBadge( dest: BottomBarDestination, superUser: Int, module: Int, + show: Boolean, ) { val count = when (dest) { BottomBarDestination.SuperUser -> superUser @@ -222,7 +230,7 @@ private fun DestinationBadge( } AnimatedVisibility( - visible = count > 0, + visible = count > 0 && show, enter = fadeIn(), exit = fadeOut() ) { diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/AOSPCrossActivityAnimation.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/AOSPCrossActivityAnimation.kt deleted file mode 100644 index a8c84374a..000000000 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/AOSPCrossActivityAnimation.kt +++ /dev/null @@ -1,174 +0,0 @@ -package com.resukisu.resukisu.ui.animation.predictiveback - -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.ContentTransform -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.CubicBezierEasing -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleOut -import androidx.compose.animation.slideInHorizontally -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.TransformOrigin -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalWindowInfo -import androidx.compose.ui.unit.dp -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.scene.Scene -import androidx.navigationevent.NavigationEvent.Companion.EDGE_LEFT -import androidx.navigationevent.NavigationEventTransitionState -import androidx.navigationevent.NavigationEventTransitionState.InProgress -import com.resukisu.resukisu.ui.util.rememberDeviceCornerRadius -import com.resukisu.resukisu.ui.viewmodel.PredictiveBackExitDirection -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -class AOSPCrossActivityAnimation( - private val exitDirection: PredictiveBackExitDirection = PredictiveBackExitDirection.ALWAYS_RIGHT -) : PredictiveBackAnimationHandler { - private var exitingPageKey: String? = null - private val exitAnimatable = Animatable(0f) - - override suspend fun onBackPressed( - transitionState: NavigationEventTransitionState?, - currentPageKey: NavKey?, - ) { - exitingPageKey = currentPageKey.toString() - - exitAnimatable.animateTo( - targetValue = 1f, - animationSpec = tween(durationMillis = 150, easing = LinearEasing) - ) - } - - override fun onPagePop(contentPageKey: Any, animationScope: CoroutineScope) { - if (exitingPageKey == contentPageKey) { - exitingPageKey = null - animationScope.launch { - exitAnimatable.snapTo(0f) - } - } - } - - @Composable - override fun Modifier.predictiveBackAnimationDecorator( - transitionState: NavigationEventTransitionState?, - contentPageKey: Any, - currentPageKey: NavKey?, - ): Modifier = composed { - val windowInfo = LocalWindowInfo.current - val containerHeightPx = windowInfo.containerSize.height - val pageKey = contentPageKey.toString() - val deviceCornerRadius = rememberDeviceCornerRadius() - - val enteringStartOffsetPx = with(LocalDensity.current) { 96.dp.toPx() } - - val linearProgress = exitAnimatable.value - val emphasizedProgress = CubicBezierEasing(0.2f, 0f, 0f, 1f).transform(linearProgress) - - val progressInProgress = (transitionState as? InProgress) - val edge = progressInProgress?.latestEvent?.swipeEdge ?: 0 - val touchY = progressInProgress?.latestEvent?.touchY - val gestureProgress = progressInProgress?.latestEvent?.progress ?: 0f - - val directionMultiplier = when (exitDirection) { - PredictiveBackExitDirection.FOLLOW_GESTURE -> if (edge == EDGE_LEFT) 1f else -1f - PredictiveBackExitDirection.ALWAYS_RIGHT -> 1f - PredictiveBackExitDirection.ALWAYS_LEFT -> -1f - } - - val isExitingPage = exitingPageKey != null && exitingPageKey == pageKey - val isCurrentNavTarget = exitingPageKey == null && pageKey == currentPageKey.toString() - - val maxScale = 0.85f - val dragScale = 1f - (1f - maxScale) * gestureProgress - - val currentPivotY = if (touchY != null && containerHeightPx > 0) { - (touchY / containerHeightPx).coerceIn(0.1f, 0.9f) - } else 0.5f - val currentPivotX = if (edge == EDGE_LEFT) 0.8f else 0.2f - - this - .graphicsLayer { - if (transitionState is InProgress) - transformOrigin = TransformOrigin(currentPivotX, currentPivotY) - - when { - isExitingPage -> { - // top page when onBackPressed called (back committed) - val computedScaleX = dragScale + (maxScale - dragScale) * emphasizedProgress - val computedTranslationX = - enteringStartOffsetPx * directionMultiplier * emphasizedProgress - val computedAlpha = - if (linearProgress >= 0.2f) 0f else (1f - linearProgress * 5f).coerceAtLeast( - 0f - ) - - scaleX = computedScaleX - scaleY = computedScaleX - translationX = computedTranslationX - alpha = computedAlpha - } - - isCurrentNavTarget -> { - // top page before onBackPressed called - scaleX = dragScale - scaleY = dragScale - translationX = 0f - alpha = 1f - } - - else -> { - // bottom page - val initialTranslationX = -enteringStartOffsetPx * directionMultiplier - - if (exitingPageKey != null) { // after onBackPressed - scaleX = dragScale + (1f - dragScale) * emphasizedProgress - scaleY = dragScale + (1f - dragScale) * emphasizedProgress - translationX = initialTranslationX * (1f - emphasizedProgress) - alpha = 1f - } else if (transitionState is InProgress) { // before onBackPressed - scaleX = dragScale - scaleY = dragScale - translationX = initialTranslationX - alpha = 1f - } - } - } - } - .clip( - if (isExitingPage || isCurrentNavTarget) RoundedCornerShape(deviceCornerRadius) - else RoundedCornerShape(0.dp) - ) - } - - override fun AnimatedContentTransitionScope>.onPredictivePopTransitionSpec( - swipeEdge: Int - ): ContentTransform = ContentTransform( - targetContentEnter = EnterTransition.None, - initialContentExit = ExitTransition.None, - sizeTransform = null - ) - - override fun AnimatedContentTransitionScope>.onPopTransitionSpec(): ContentTransform = - ContentTransform( - targetContentEnter = slideInHorizontally(initialOffsetX = { -it / 4 }), - initialContentExit = scaleOut(targetScale = 0.9f) + fadeOut(), - sizeTransform = null - ) - - override fun AnimatedContentTransitionScope>.onTransitionSpec(): ContentTransform = - ContentTransform( - targetContentEnter = slideInHorizontally(initialOffsetX = { it }), - initialContentExit = ExitTransition.None, - sizeTransform = null - ) -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/AospNavTransition.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/AospNavTransition.kt new file mode 100644 index 000000000..d787ad74c --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/AospNavTransition.kt @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Copyright (C) 2026 ReSukiSU contributors +package com.resukisu.resukisu.ui.animation.predictiveback + +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp +import top.yukonga.miuix.kmp.nav.transition.NavGesture +import top.yukonga.miuix.kmp.nav.transition.NavMotion +import top.yukonga.miuix.kmp.nav.transition.NavRole +import top.yukonga.miuix.kmp.nav.transition.NavSettle +import top.yukonga.miuix.kmp.nav.transition.NavSettlePhase +import top.yukonga.miuix.kmp.nav.transition.NavSettleSpec +import top.yukonga.miuix.kmp.nav.transition.NavSwipeEdge +import top.yukonga.miuix.kmp.nav.transition.NavTransition +import top.yukonga.miuix.kmp.nav.transition.navDirectionalTransition +import top.yukonga.miuix.kmp.nav.transition.navGraphicsTransition +import kotlin.math.abs +import kotlin.math.exp +import kotlin.math.min +import kotlin.math.sin +import kotlin.math.sqrt + +private const val BOUNCE_STIFFNESS = 200f +private const val BOUNCE_DAMPING = 0.75f +private const val BOUNCE_MAX_KICK = 1000f +private const val BOUNCE_MIN_KICK = 120f +private const val OPEN_FADE_START = 0.12f +private const val OPEN_FADE_SPAN = 0.71f +private const val CLOSE_FADE_START = 0.21f +private const val CLOSE_FADE_SPAN = 0.74f +private const val CLASSIC_FADE_DURATION = 83f +private const val OPEN_FADE_OFFSET = 50f +private const val CLOSE_FADE_OFFSET = 35f +private const val CROSS_ACTIVITY_MIN_SCALE = 0.9f + +private val CrossActivityDrift = 96.dp +private val CrossActivityEdgeMargin = 8.dp + +private val ClassicActivityMotion = NavMotion( + programmatic = NavSettleSpec.Tween(durationMillis = 450, easing = FastOutExtraSlowIn), +) + +private val ClassicActivityOpen: NavTransition = navGraphicsTransition( + motion = ClassicActivityMotion, + scrim = { 0f }, +) { scope -> + val depth = scope.relativeDepth + val driftPx = with(scope.density) { CrossActivityDrift.toPx() } + if (depth <= 0f) { + val progress = topProgress(depth) + translationX = (1f - progress) * driftPx + alpha = if (scope.role == NavRole.Incoming) { + val settle = scope.settle + if (settle != null) { + ((settle.elapsedMillis - OPEN_FADE_OFFSET) / CLASSIC_FADE_DURATION) + .coerceIn(0f, 1f) + } else { + ((progress - OPEN_FADE_START) / OPEN_FADE_SPAN).coerceIn(0f, 1f) + } + } else { + 1f + } + } else { + translationX = -coverProgress(depth) * driftPx + } +} + +private val ClassicActivityClose: NavTransition = navGraphicsTransition( + motion = ClassicActivityMotion, + scrim = { 0f }, +) { scope -> + val depth = scope.relativeDepth + val driftPx = with(scope.density) { CrossActivityDrift.toPx() } + if (depth <= 0f) { + val progress = topProgress(depth) + translationX = (1f - progress) * driftPx + alpha = if (scope.role == NavRole.Outgoing) { + val settle = scope.settle + if (settle != null) { + (1f - (settle.elapsedMillis - CLOSE_FADE_OFFSET) / CLASSIC_FADE_DURATION) + .coerceIn(0f, 1f) + } else { + ((progress - CLOSE_FADE_START) / CLOSE_FADE_SPAN).coerceIn(0f, 1f) + } + } else { + 1f + } + } else { + translationX = -coverProgress(depth) * driftPx + } +} + +private val CrossActivityPredictive: NavTransition = navGraphicsTransition( + opaqueDepth = 1f, + motion = NavMotion( + commit = NavSettleSpec.Tween(durationMillis = 450, easing = FastOutExtraSlowIn), + cancel = NavSettleSpec.Spring(stiffness = 1500f), + ), + scrim = { scope -> + val settle = scope.settle + val gesture = scope.gesture + when { + settle?.phase == NavSettlePhase.Commit -> + (1f - settle.elapsedMillis / 450f).coerceIn(0f, 1f) + + gesture != null -> + (scope.relativeDepth.coerceIn(0f, 1f) / + (1f - gesture.progress).coerceAtLeast(0.01f)).coerceIn(0f, 1f) + + else -> scope.relativeDepth.coerceIn(0f, 1f) + } + }, +) { scope -> + val depth = scope.relativeDepth + val gesture = scope.gesture + val settle = scope.settle + val committing = settle?.phase == NavSettlePhase.Commit + val widthPx = scope.layoutSize.width.toFloat() + val heightPx = scope.layoutSize.height.toFloat() + val driftPx = with(scope.density) { CrossActivityDrift.toPx() } + val bounce = bounceScale(settle, gesture) + val hugMax = ( + widthPx * (1f - CROSS_ACTIVITY_MIN_SCALE) / 2f - + with(scope.density) { CrossActivityEdgeMargin.toPx() } + ).coerceAtLeast(0f) + val hugs = gesture?.swipeEdge != NavSwipeEdge.Right + if (depth <= 0f) { + val progress = topProgress(depth) + if (scope.role == NavRole.Outgoing && committing && gesture != null) { + val releaseProgress = (1f - gesture.progress).coerceAtLeast(0.01f) + val post = (1f - progress / releaseProgress).coerceIn(0f, 1f) + val releaseEasedProgress = shapedTopProgress(releaseProgress, gesture) + val committedScale = + CROSS_ACTIVITY_MIN_SCALE + (1f - CROSS_ACTIVITY_MIN_SCALE) * releaseEasedProgress + val grown = committedScale + (1f - committedScale) * post + scaleX = snapScaleToPixelExtent(grown * bounce, widthPx) + scaleY = scaleX + var tx = if (hugs) (1f - releaseEasedProgress) * hugMax else 0f + tx += post * driftPx + alpha = (1f - 5f * (settle.elapsedMillis / 450f)).coerceAtLeast(0f) + translationX = snapTranslationToPixelEdge(tx, scaleX, widthPx) + translationY = snapTranslationToPixelEdge( + translation = crossActivityYShift( + gesture = gesture, + height = heightPx, + scale = scaleX, + density = scope.density, + ), + scale = scaleY, + extent = heightPx, + ) + } else { + val easedProgress = shapedTopProgress(progress, gesture) + scaleX = snapScaleToPixelExtent( + scale = ( + CROSS_ACTIVITY_MIN_SCALE + (1f - CROSS_ACTIVITY_MIN_SCALE) * easedProgress + ) * bounce, + extent = widthPx, + ) + scaleY = scaleX + translationX = snapTranslationToPixelEdge( + translation = if (hugs) (1f - easedProgress) * hugMax else 0f, + scale = scaleX, + extent = widthPx, + ) + alpha = when { + scope.role == NavRole.Outgoing && gesture != null -> { + val releaseProgress = (1f - gesture.progress).coerceAtLeast(0.01f) + (1f - (1f - progress / releaseProgress).coerceIn(0f, 1f) * 3.5f) + .coerceAtLeast(0f) + } + + gesture != null -> 1f + else -> (progress / 0.2f).coerceIn(0f, 1f) + } + translationY = snapTranslationToPixelEdge( + translation = crossActivityYShift( + gesture = gesture, + height = heightPx, + scale = scaleX, + density = scope.density, + ), + scale = scaleX, + extent = heightPx, + ) + } + } else { + val cover = coverProgress(depth) + val post = if (gesture != null) { + val releaseProgress = gesture.progress + if (releaseProgress >= 1f) { + 1f + } else { + (((1f - cover) - releaseProgress) / (1f - releaseProgress)).coerceIn(0f, 1f) + } + } else { + 1f - cover + } + val rawTranslationX = -(1f - post) * driftPx + if (gesture != null) { + val travel = if (committing) gesture.progress else (1f - cover) + val eased = BackGestureEasing.transform(travel.coerceIn(0f, 1f)) + val liveScale = + CROSS_ACTIVITY_MIN_SCALE + (1f - CROSS_ACTIVITY_MIN_SCALE) * (1f - eased) + scaleX = snapScaleToPixelExtent( + (liveScale + (1f - liveScale) * post) * bounce, + widthPx, + ) + scaleY = scaleX + } + translationX = snapTranslationToPixelEdge(rawTranslationX, scaleX, widthPx) + translationY = snapTranslationToPixelEdge( + translation = crossActivityYShift( + gesture = gesture, + height = heightPx, + scale = scaleX, + density = scope.density, + ), + scale = scaleX, + extent = heightPx, + ) + } +} + +internal val AospNavTransition: NavTransition = navDirectionalTransition( + push = ClassicActivityOpen, + pop = ClassicActivityClose, + predictivePop = CrossActivityPredictive, +) + +private fun bounceScale(settle: NavSettle?, gesture: NavGesture?): Float { + if (settle == null || settle.phase != NavSettlePhase.Commit || gesture == null) return 1f + val factor = if (gesture.swipeEdge != NavSwipeEdge.None) 2f else 1f + val floorKick = if (gesture.progress < 0.1f) BOUNCE_MIN_KICK else 0f + val kick = (abs(settle.releaseVelocity) * 100f * (1f - CROSS_ACTIVITY_MIN_SCALE) * factor) + .coerceIn(floorKick, BOUNCE_MAX_KICK) + if (kick <= 0f) return 1f + val omega = sqrt(BOUNCE_STIFFNESS) + val omegaD = omega * sqrt(1f - BOUNCE_DAMPING * BOUNCE_DAMPING) + val t = settle.elapsedMillis / 1000f + val overlay = + -(kick / omegaD) * exp(-BOUNCE_DAMPING * omega * t) * sin(omegaD * t) + return ((100f + overlay) / 100f).coerceAtMost(1f) +} + +private fun shapedTopProgress(progress: Float, gesture: NavGesture?): Float = + if (gesture == null) progress else 1f - BackGestureEasing.transform((1f - progress).coerceIn(0f, 1f)) + +private fun crossActivityYShift( + gesture: NavGesture?, + height: Float, + scale: Float, + density: Density, +): Float { + if (gesture == null || height <= 0f) return 0f + val rawDelta = gesture.touchY - gesture.initialTouchY + val half = height / 2f + val ratio = min(half, abs(rawDelta)) / half + val damped = 1f - (1f - ratio) * (1f - ratio) + val marginPx = with(density) { CrossActivityEdgeMargin.toPx() } + val maxShift = ((height - height * scale) / 2f - marginPx).coerceAtLeast(0f) + return maxShift * damped * (if (rawDelta < 0f) -1f else 1f) +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/ClassicNavTransition.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/ClassicNavTransition.kt new file mode 100644 index 000000000..d927c7e1a --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/ClassicNavTransition.kt @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Copyright (C) 2026 ReSukiSU contributors +package com.resukisu.resukisu.ui.animation.predictiveback + +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.zIndex +import top.yukonga.miuix.kmp.nav.transition.NavMotion +import top.yukonga.miuix.kmp.nav.transition.NavSettleSpec +import top.yukonga.miuix.kmp.nav.transition.NavTransition +import top.yukonga.miuix.kmp.nav.transition.NavTransitionScope +import top.yukonga.miuix.kmp.nav.transition.NavTransitions +import top.yukonga.miuix.kmp.nav.transition.navDirectionalTransition + +private val ClassicScaleMotion = NavMotion( + commit = NavSettleSpec.Tween( + durationMillis = 200, + easing = CubicBezierEasing(0.2f, 0f, 0f, 1f), + ), + cancel = NavSettleSpec.Spring(stiffness = 1500f), + programmatic = NavSettleSpec.Tween( + durationMillis = 200, + easing = CubicBezierEasing(0.2f, 0f, 0f, 1f), + ), +) + +private val ClassicScalePop: NavTransition = object : NavTransition { + override val opaqueDepth: Float = 1f + + override val motion: NavMotion = ClassicScaleMotion + + override fun scrimFraction(scope: NavTransitionScope): Float = coverProgress(scope.relativeDepth) + + override fun Modifier.transformEntry(scope: NavTransitionScope): Modifier { + val zIndex = if (scope.relativeDepth > 0f) 1f else 0f + return graphicsLayer { + val depth = scope.relativeDepth + val widthPx = scope.layoutSize.width.toFloat() + val heightPx = scope.layoutSize.height.toFloat() + if (depth <= 0f) { + val progress = topProgress(depth) + scaleX = snapScaleToPixelExtent(0.9f + 0.1f * progress, widthPx) + scaleY = scaleX + translationX = snapTranslationToPixelEdge(0f, scaleX, widthPx) + translationY = snapTranslationToPixelEdge(0f, scaleY, heightPx) + alpha = progress + } else { + translationX = snapTranslationToPixelEdge( + translation = -coverProgress(depth) * widthPx, + scale = 1f, + extent = widthPx, + ) + } + }.zIndex(zIndex) + } +} + +internal val ClassicNavTransition: NavTransition = navDirectionalTransition( + push = NavTransitions.MiuixDefault, + pop = ClassicScalePop, + predictivePop = ClassicScalePop, +) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/InstallerNavTransition.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/InstallerNavTransition.kt new file mode 100644 index 000000000..d2789f512 --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/InstallerNavTransition.kt @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Copyright (C) 2026 ReSukiSU contributors +package com.resukisu.resukisu.ui.animation.predictiveback + +import com.resukisu.resukisu.ui.viewmodel.PredictiveBackAnimation +import com.resukisu.resukisu.ui.viewmodel.PredictiveBackExitDirection +import top.yukonga.miuix.kmp.nav.transition.NavTransition +import top.yukonga.miuix.kmp.nav.transition.NavTransitions + +fun installerNavTransition( + animation: PredictiveBackAnimation, + exitDirection: PredictiveBackExitDirection, +): NavTransition = when (animation) { + PredictiveBackAnimation.None -> NoPredictiveBackTransition + PredictiveBackAnimation.MIUIX -> NavTransitions.MiuixDefault + PredictiveBackAnimation.AOSP -> AospNavTransition + PredictiveBackAnimation.Scale -> scaleNavTransition(exitDirection) + PredictiveBackAnimation.KernelSUClassic -> ClassicNavTransition +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/KernelSUClassicPredictiveBackAnimation.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/KernelSUClassicPredictiveBackAnimation.kt deleted file mode 100644 index 29145fcff..000000000 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/KernelSUClassicPredictiveBackAnimation.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.resukisu.resukisu.ui.animation.predictiveback - -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.ContentTransform -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleOut -import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideOutHorizontally -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.scene.Scene -import androidx.navigationevent.NavigationEventTransitionState - -class KernelSUClassicPredictiveBackAnimation : PredictiveBackAnimationHandler { - override suspend fun onBackPressed( - transitionState: NavigationEventTransitionState?, - currentPageKey: NavKey? - ) { - // ignore - } - - @Composable - override fun Modifier.predictiveBackAnimationDecorator( - transitionState: NavigationEventTransitionState?, - contentPageKey: Any, - currentPageKey: NavKey?, - ): Modifier { - return this - } - - override fun AnimatedContentTransitionScope>.onPredictivePopTransitionSpec( - swipeEdge: Int - ): ContentTransform = - ContentTransform( - targetContentEnter = slideInHorizontally(initialOffsetX = { fullWidth -> -fullWidth }), - initialContentExit = scaleOut(targetScale = 0.9f) + fadeOut(), - sizeTransform = null - ) - - override fun AnimatedContentTransitionScope>.onPopTransitionSpec(): ContentTransform = - ContentTransform( - targetContentEnter = slideInHorizontally(initialOffsetX = { fullWidth -> -fullWidth }), - initialContentExit = scaleOut(targetScale = 0.9f) + fadeOut(), - sizeTransform = null - ) - - override fun AnimatedContentTransitionScope>.onTransitionSpec(): ContentTransform = - ContentTransform( - targetContentEnter = slideInHorizontally(initialOffsetX = { fullWidth -> fullWidth }), - initialContentExit = slideOutHorizontally(targetOffsetX = { fullWidth -> -fullWidth }), - sizeTransform = null - ) -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/MiuixPredictiveBackAnimation.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/MiuixPredictiveBackAnimation.kt deleted file mode 100644 index 5db075751..000000000 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/MiuixPredictiveBackAnimation.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.resukisu.resukisu.ui.animation.predictiveback - -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.ContentTransform -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.scene.Scene -import androidx.navigation3.ui.defaultPopTransitionSpec -import androidx.navigation3.ui.defaultPredictivePopTransitionSpec -import androidx.navigation3.ui.defaultTransitionSpec -import androidx.navigationevent.NavigationEventTransitionState - -class MiuixPredictiveBackAnimation : PredictiveBackAnimationHandler { - - override suspend fun onBackPressed( - transitionState: NavigationEventTransitionState?, - currentPageKey: NavKey? - ) { - // Deliberately empty. Predictive back gesture progress is natively handled - // and synchronized by the Compose transition engine. - } - - @Composable - override fun Modifier.predictiveBackAnimationDecorator( - transitionState: NavigationEventTransitionState?, - contentPageKey: Any, - currentPageKey: NavKey?, - ): Modifier { - // NavDisplay automatically handles dimming and corner clipping internally - // through NavDisplayTransitionEffects, so we return unmodified this. - return this - } - - override fun AnimatedContentTransitionScope>.onPredictivePopTransitionSpec( - swipeEdge: Int - ): ContentTransform = defaultPredictivePopTransitionSpec().invoke(this, swipeEdge) - - override fun AnimatedContentTransitionScope>.onPopTransitionSpec(): ContentTransform = - defaultPopTransitionSpec().invoke(this) - - override fun AnimatedContentTransitionScope>.onTransitionSpec(): ContentTransform = - defaultTransitionSpec().invoke(this) -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NavTransitionEasing.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NavTransitionEasing.kt new file mode 100644 index 000000000..058c7b95a --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NavTransitionEasing.kt @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Copyright (C) 2026 ReSukiSU contributors +package com.resukisu.resukisu.ui.animation.predictiveback + +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.Easing + +internal val FastOutExtraSlowIn: Easing = run { + val knotX = 0.166666f + val knotY = 0.4f + val first = CubicBezierEasing(0.05f / knotX, 0f, 0.133333f / knotX, 0.06f / knotY) + val second = CubicBezierEasing( + (0.208333f - knotX) / (1f - knotX), + (0.82f - knotY) / (1f - knotY), + (0.25f - knotX) / (1f - knotX), + (1f - knotY) / (1f - knotY), + ) + Easing { fraction -> + if (fraction < knotX) { + knotY * first.transform(fraction / knotX) + } else { + knotY + (1f - knotY) * second.transform((fraction - knotX) / (1f - knotX)) + } + } +} + +internal val BackGestureEasing: Easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) + +internal fun topProgress(depth: Float): Float = (1f + depth).coerceIn(0f, 1f) + +internal fun coverProgress(depth: Float): Float = depth.coerceIn(0f, 1f) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NavTransitionGeometry.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NavTransitionGeometry.kt new file mode 100644 index 000000000..df3f7a8fc --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NavTransitionGeometry.kt @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Copyright (C) 2026 ReSukiSU contributors +package com.resukisu.resukisu.ui.animation.predictiveback + +import kotlin.math.roundToInt + +internal fun snapScaleToPixelExtent(scale: Float, extent: Float): Float = + if (extent > 0f) (scale * extent).roundToInt() / extent else scale + +internal fun snapTranslationToPixelEdge( + translation: Float, + scale: Float, + extent: Float, + pivotFraction: Float = 0.5f, +): Float { + if (extent <= 0f) return translation + val scaledEdgeOffset = extent * pivotFraction * (1f - scale) + return (translation + scaledEdgeOffset).roundToInt() - scaledEdgeOffset +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NoPredictiveBackAnimation.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NoPredictiveBackAnimation.kt deleted file mode 100644 index 0a3cb2fa8..000000000 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NoPredictiveBackAnimation.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.resukisu.resukisu.ui.animation.predictiveback - -import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.ContentTransform -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.scene.Scene -import androidx.navigation3.ui.defaultPopTransitionSpec -import androidx.navigation3.ui.defaultTransitionSpec -import androidx.navigationevent.NavigationEventTransitionState -import com.resukisu.resukisu.ui.navigation.LocalNavigator - -class NoPredictiveBackAnimation : PredictiveBackAnimationHandler { - override suspend fun onBackPressed( - transitionState: NavigationEventTransitionState?, - currentPageKey: NavKey? - ) { - // Ignore predictive back gesture progress completely. - } - - @Composable - override fun Modifier.predictiveBackAnimationDecorator( - transitionState: NavigationEventTransitionState?, - contentPageKey: Any, - currentPageKey: NavKey?, - ): Modifier { - val navigator = LocalNavigator.current - - // Determine if there are pages to pop. - val canPop = navigator.backStack.size > 1 - - // Only intercept the back button when we can actually pop. - // If enabled is false, the system handles the back press (e.g., exits the Activity). - // Using BackHandler here completely intercepts the system predictive back dispatch, - // preventing the predictive gesture from starting. - BackHandler(enabled = canPop) { - navigator.pop() - } - - return this - } - - override fun AnimatedContentTransitionScope>.onPredictivePopTransitionSpec( - swipeEdge: Int - ): ContentTransform = ContentTransform( - // Keep predictive pop transition empty since it's disabled by BackHandler anyway. - targetContentEnter = EnterTransition.None, - initialContentExit = ExitTransition.None, - sizeTransform = null - ) - - override fun AnimatedContentTransitionScope>.onPopTransitionSpec(): ContentTransform = - // Sync with the default pop transition used in Miuix implementation - defaultPopTransitionSpec().invoke(this) - - override fun AnimatedContentTransitionScope>.onTransitionSpec(): ContentTransform = - // Sync with the default push transition used in Miuix implementation - defaultTransitionSpec().invoke(this) -} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NoPredictiveBackTransition.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NoPredictiveBackTransition.kt new file mode 100644 index 000000000..c16600655 --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NoPredictiveBackTransition.kt @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Copyright (C) 2026 ReSukiSU contributors +package com.resukisu.resukisu.ui.animation.predictiveback + +import androidx.compose.ui.graphics.GraphicsLayerScope +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.util.fastRoundToInt +import top.yukonga.miuix.kmp.nav.runtime.NavProgrammaticEasing +import top.yukonga.miuix.kmp.nav.transition.NavMotion +import top.yukonga.miuix.kmp.nav.transition.NavRole +import top.yukonga.miuix.kmp.nav.transition.NavSettle +import top.yukonga.miuix.kmp.nav.transition.NavSettlePhase +import top.yukonga.miuix.kmp.nav.transition.NavSettleSpec +import top.yukonga.miuix.kmp.nav.transition.NavTransition +import top.yukonga.miuix.kmp.nav.transition.NavTransitionScope +import top.yukonga.miuix.kmp.nav.transition.NavTransitions +import top.yukonga.miuix.kmp.nav.transition.navDirectionalTransition +import top.yukonga.miuix.kmp.nav.transition.navGraphicsTransition + +private const val NO_PREDICTIVE_POP_DURATION_MILLIS = 450 + +/** + * Keeps the page at the point where back interrupted it instead of handing the in-flight push to + * predictive progress. A committed back slowly plays the page out from that point; cancellation + * lets the interrupted push finish entering. + */ +private val NoPredictivePop: NavTransition = navGraphicsTransition( + opaqueDepth = 1f, + motion = NavMotion( + commit = NavSettleSpec.Tween( + durationMillis = NO_PREDICTIVE_POP_DURATION_MILLIS, + easing = NavProgrammaticEasing, + ), + cancel = NavSettleSpec.Tween( + durationMillis = NO_PREDICTIVE_POP_DURATION_MILLIS, + easing = NavProgrammaticEasing, + ), + ), + scrim = { scope -> 1f - noPredictiveVisualProgress(scope) }, +) { scope -> + applyNoPredictiveTransform(scope, noPredictiveVisualProgress(scope)) +} + +internal val NoPredictiveBackTransition: NavTransition = navDirectionalTransition( + push = NavTransitions.MiuixDefault, + pop = NavTransitions.MiuixDefault, + predictivePop = NoPredictivePop, +) + +/** + * Reconstructs the grab anchor hidden by the shared depth driver. While the finger is active the + * visual progress stays at that anchor. Commit and cancel then animate from the anchor rather than + * from the finger's predictive progress. + */ +private fun noPredictiveVisualProgress(scope: NavTransitionScope): Float { + val gesture = scope.gesture ?: return 0f + val topDepth = when (scope.role) { + NavRole.Covered -> scope.relativeDepth - 1f + NavRole.Top if scope.settle?.phase == NavSettlePhase.Commit -> + scope.relativeDepth - 1f + + else -> scope.relativeDepth + } + val totalProgress = -topDepth + val settle = scope.settle + return when (settle?.phase) { + null -> (totalProgress - gesture.progress).coerceIn(0f, 1f) + NavSettlePhase.Commit -> { + val settleProgress = noPredictiveSettleProgress(settle) + val remaining = 1f - settleProgress + if (remaining <= 0.001f) { + 1f + } else { + val anchor = ( + (totalProgress - settleProgress) / remaining - gesture.progress + ).coerceIn(0f, 1f) + anchor + (1f - anchor) * settleProgress + } + } + + NavSettlePhase.Cancel -> { + val settleProgress = noPredictiveSettleProgress(settle) + val remaining = 1f - settleProgress + if (remaining <= 0.001f) { + 0f + } else { + val anchor = (totalProgress / remaining - gesture.progress).coerceIn(0f, 1f) + anchor * remaining + } + } + + NavSettlePhase.Programmatic -> + (totalProgress - gesture.progress).coerceIn(0f, 1f) + } +} + +private fun noPredictiveSettleProgress(settle: NavSettle): Float { + val fraction = (settle.elapsedMillis / NO_PREDICTIVE_POP_DURATION_MILLIS).coerceIn(0f, 1f) + return NavProgrammaticEasing.transform(fraction).coerceIn(0f, 1f) +} + +private fun GraphicsLayerScope.applyNoPredictiveTransform( + scope: NavTransitionScope, + progress: Float, +) { + val widthPx = scope.layoutSize.width.toFloat() + val direction = if (scope.layoutDirection == LayoutDirection.Rtl) -1f else 1f + val isLowerEntry = scope.role == NavRole.Covered || + scope.role == NavRole.Top && scope.settle?.phase == NavSettlePhase.Commit + if (isLowerEntry) { + translationX = -direction * (1f - progress) * widthPx * 0.25f + alpha = 0.9f + 0.1f * progress + } else { + translationX = (direction * progress * widthPx).fastRoundToInt().toFloat() + } +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/PredictiveBackAnimationHandler.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/PredictiveBackAnimationHandler.kt deleted file mode 100644 index 28d431bd9..000000000 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/PredictiveBackAnimationHandler.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.resukisu.resukisu.ui.animation.predictiveback - -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.ContentTransform -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.scene.Scene -import androidx.navigationevent.NavigationEvent -import androidx.navigationevent.NavigationEventTransitionState -import kotlinx.coroutines.CoroutineScope - -interface PredictiveBackAnimationHandler { - /** - * Callback invoked when the back event is committed (e.g., gesture completed or button clicked). - * - * **Implementation Requirements:** - * - Implementation must check the current state of [transitionState]. - * - If a predictive back animation is active (in-progress), this method must play the animations - * to avoid page disappear without any Exit animations - * - This serves as the terminal lifecycle hook before the Navigation Manager - * officially removes the page from the backstack. - * - * @param transitionState The state tracking the current predictive back gesture/animation. - * @param currentPageKey The [NavKey] of the page currently being popped. - */ - suspend fun onBackPressed( - transitionState: NavigationEventTransitionState?, - currentPageKey: NavKey?, - ) - - /** - * Callback when page actually pop - * - * **NOTE:** the page will pop from view tree IMMEDIATELY - * after this callback completed - * - * @param contentPageKey The [NavKey] of the page being pop. - * @param animationScope An [CoroutineScope] for reset animation status ONLY - */ - fun onPagePop( - contentPageKey: Any, - animationScope: CoroutineScope - ) { - } - - /** - * A UI decorator applied to every page during the rendering process. - * * Allows for custom modifications to the page layout or graphics layer. - * - * @param transitionState The current state of the predictive back transition. - * @param contentPageKey The [NavKey] of the page being decorated. - * @param currentPageKey The [NavKey]'s toString of the page currently at the top of the stack. - * @return the Modifier will apply to the Box of the content - */ - @Composable - fun Modifier.predictiveBackAnimationDecorator( - transitionState: NavigationEventTransitionState?, - contentPageKey: Any, - currentPageKey: NavKey?, - ): Modifier - - /** - * Defines the transition specs specifically for a predictive back (swipe) gesture. - * @param swipeEdge The edge from which the swipe gesture originated (Left or Right). - * @return A [ContentTransform] defining the enter/exit animations. - */ - fun AnimatedContentTransitionScope>.onPredictivePopTransitionSpec( - @NavigationEvent.SwipeEdge swipeEdge: Int - ): ContentTransform - - /** - * Defines the transition specs for a standard pop navigation (e.g., non-gesture back). - */ - fun AnimatedContentTransitionScope>.onPopTransitionSpec(): ContentTransform - - /** - * Defines the default transition specs for forward navigation (push). - */ - fun AnimatedContentTransitionScope>.onTransitionSpec(): ContentTransform -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/ScaleNavTransition.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/ScaleNavTransition.kt new file mode 100644 index 000000000..a2a0636a4 --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/ScaleNavTransition.kt @@ -0,0 +1,122 @@ +package com.resukisu.resukisu.ui.animation.predictiveback + +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.ui.graphics.TransformOrigin +import com.resukisu.resukisu.ui.viewmodel.PredictiveBackExitDirection +import top.yukonga.miuix.kmp.nav.transition.NavGesture +import top.yukonga.miuix.kmp.nav.transition.NavMotion +import top.yukonga.miuix.kmp.nav.transition.NavRole +import top.yukonga.miuix.kmp.nav.transition.NavSettlePhase +import top.yukonga.miuix.kmp.nav.transition.NavSettleSpec +import top.yukonga.miuix.kmp.nav.transition.NavSwipeEdge +import top.yukonga.miuix.kmp.nav.transition.NavTransition +import top.yukonga.miuix.kmp.nav.transition.NavTransitionScope +import top.yukonga.miuix.kmp.nav.transition.NavTransitions +import top.yukonga.miuix.kmp.nav.transition.navDirectionalTransition +import top.yukonga.miuix.kmp.nav.transition.navGraphicsTransition + +private val ScaleExitMotion = NavMotion( + commit = NavSettleSpec.Tween( + durationMillis = 200, + easing = FastOutSlowInEasing, + ), + cancel = NavSettleSpec.Spring(stiffness = 1500f), + programmatic = NavSettleSpec.Tween( + durationMillis = 200, + easing = CubicBezierEasing(0.2f, 0f, 0f, 1f), + ), +) + +internal fun scaleNavTransition(exitDirection: PredictiveBackExitDirection): NavTransition { + val pop = navGraphicsTransition( + opaqueDepth = 1f, + motion = ScaleExitMotion, + scrim = { scope -> + when { + scope.settle?.phase == NavSettlePhase.Commit -> + (1f - (scope.settle?.elapsedMillis ?: 0f) / 200) + .coerceIn(0f, 1f) + + scope.gesture != null -> 1f + else -> coverProgress(scope.relativeDepth) + } + }, + ) { scope -> + val depth = scope.relativeDepth + val widthPx = scope.layoutSize.width.toFloat() + val heightPx = scope.layoutSize.height.toFloat() + val gesture = scope.gesture + val sign = exitDirectionSign(exitDirection, scope) + val committing = scope.settle?.phase == NavSettlePhase.Commit + val outgoingCommit = scope.role == NavRole.Outgoing && committing && gesture != null + if (depth <= 0f) { + val progress = topProgress(depth) + val pageScale = if (outgoingCommit) { + val releaseProgress = (1f - gesture.progress).coerceAtLeast(0.01f) + val post = (1f - progress / releaseProgress).coerceIn(0f, 1f) + val releaseEasedProgress = shapedTopProgress(releaseProgress, gesture) + val committedScale = 0.85f + (1f - 0.85f) * releaseEasedProgress + committedScale + (0.85f - committedScale) * post + } else { + val easedProgress = shapedTopProgress(progress, gesture) + 0.85f + (1f - 0.85f) * easedProgress + } + val pivotX = if (gesture?.swipeEdge == NavSwipeEdge.Left) 0.8f else 0.2f + val pivotY = gesturePivotY(gesture, heightPx) + scaleX = snapScaleToPixelExtent(pageScale, widthPx) + scaleY = scaleX + transformOrigin = TransformOrigin( + pivotFractionX = pivotX, + pivotFractionY = pivotY, + ) + val rawTranslationX = if (gesture != null && scope.settle == null) { + 0f + } else if (outgoingCommit) { + val releaseProgress = (1f - gesture.progress).coerceAtLeast(0.01f) + val post = (1f - progress / releaseProgress).coerceIn(0f, 1f) + sign * post * widthPx + } else { + sign * (1f - progress) * widthPx + } + translationX = snapTranslationToPixelEdge( + translation = rawTranslationX, + scale = scaleX, + extent = widthPx, + pivotFraction = pivotX, + ) + translationY = snapTranslationToPixelEdge( + translation = 0f, + scale = scaleY, + extent = heightPx, + pivotFraction = pivotY, + ) + } + } + return navDirectionalTransition( + push = NavTransitions.MiuixDefault, + pop = pop, + predictivePop = pop, + ) +} + +private fun shapedTopProgress(progress: Float, gesture: NavGesture?): Float = + if (gesture == null) progress else 1f - BackGestureEasing.transform((1f - progress).coerceIn(0f, 1f)) + +private fun exitDirectionSign( + direction: PredictiveBackExitDirection, + scope: NavTransitionScope, +): Float = when (direction) { + PredictiveBackExitDirection.FOLLOW_GESTURE -> + if (scope.gesture?.swipeEdge == NavSwipeEdge.Left) 1f else -1f + + PredictiveBackExitDirection.ALWAYS_RIGHT -> 1f + PredictiveBackExitDirection.ALWAYS_LEFT -> -1f +} + +private fun gesturePivotY(gesture: NavGesture?, height: Float): Float = + if (gesture != null && height > 0f) { + (gesture.touchY / height).coerceIn(0.1f, 0.9f) + } else { + 0.5f + } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/ScalePredictiveBackAnimation.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/ScalePredictiveBackAnimation.kt deleted file mode 100644 index 4fdbdb0f0..000000000 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/ScalePredictiveBackAnimation.kt +++ /dev/null @@ -1,195 +0,0 @@ -package com.resukisu.resukisu.ui.animation.predictiveback - -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.ContentTransform -import androidx.compose.animation.EnterExitState -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleOut -import androidx.compose.animation.slideInHorizontally -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.TransformOrigin -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalWindowInfo -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.scene.Scene -import androidx.navigation3.ui.LocalNavAnimatedContentScope -import androidx.navigationevent.NavigationEvent.Companion.EDGE_LEFT -import androidx.navigationevent.NavigationEventTransitionState -import androidx.navigationevent.NavigationEventTransitionState.InProgress -import com.resukisu.resukisu.ui.util.rememberDeviceCornerRadius -import com.resukisu.resukisu.ui.viewmodel.PredictiveBackExitDirection -import kotlinx.coroutines.CoroutineScope - -class ScalePredictiveBackAnimation( - private val exitDirection: PredictiveBackExitDirection = PredictiveBackExitDirection.ALWAYS_RIGHT -) : PredictiveBackAnimationHandler { - private var exitingPageKey: String? = null - private val exitAnimatable = Animatable(0f) - private var inPredictiveBackAnimation = false - - override suspend fun onBackPressed( - transitionState: NavigationEventTransitionState?, - currentPageKey: NavKey?, - ) { - if (inPredictiveBackAnimation && transitionState is InProgress) { - exitingPageKey = currentPageKey.toString() - exitAnimatable.animateTo( - targetValue = 1f, - animationSpec = tween( - durationMillis = 200, - easing = FastOutSlowInEasing - ) - ) - exitAnimatable.snapTo(0f) - } - } - - override fun onPagePop(contentPageKey: Any, animationScope: CoroutineScope) { - if (exitingPageKey == contentPageKey) { - exitingPageKey = null - } - } - - @Composable - override fun Modifier.predictiveBackAnimationDecorator( - transitionState: NavigationEventTransitionState?, - contentPageKey: Any, - currentPageKey: NavKey?, - ): Modifier { - val windowInfo = LocalWindowInfo.current - val navContent = LocalNavAnimatedContentScope.current - - val containerHeightPx = windowInfo.containerSize.height - val containerWidthPx = windowInfo.containerSize.width.toFloat() - val pageKey = contentPageKey.toString() - val transition = navContent.transition - val deviceCornerRadius = rememberDeviceCornerRadius() - - val modifier = - if (pageKey == currentPageKey.toString() || exitingPageKey == pageKey) { - // Calculate the page scale - val animatedScale by transition.animateFloat( - transitionSpec = { tween(300) }, - label = "PredictiveScale" - ) { state -> - when (state) { - EnterExitState.PostExit -> 0.85f - else -> 1f - } - } - - // navigation 3 break transition.targetState - // its state management is fully shit - // racing racing racing - // fuck fuck fuck - // so, We can't use LaunchedEffect to process that - // Just check transition.animateFloat's result to know currentStatus - inPredictiveBackAnimation = animatedScale != 1f - - // calculate WHERE is the scaled page - val progressInProgress = (transitionState as? InProgress) - val edge = progressInProgress?.latestEvent?.swipeEdge ?: 0 - val touchY = progressInProgress?.latestEvent?.touchY - - // scaled card Y calculation based on touch point - val currentPivotY = if (touchY != null && containerHeightPx > 0) { - (touchY / containerHeightPx).coerceIn(0.1f, 0.9f) - } else 0.5f - - // if the navigation gesture originates from the left edge, we let it scale to right - // otherwise, scale to left - val currentPivotX = if (edge == EDGE_LEFT) 0.8f else 0.2f - - // From the user settings, we use follow_gesture/right/left for the card's exit animation? - val directionMultiplier = when (exitDirection) { - // When user choice follow_gesture, we use this logic for calc them - // navigation gesture left -> exit to right - // navigation gesture right -> exit to left - PredictiveBackExitDirection.FOLLOW_GESTURE -> if (edge == EDGE_LEFT) 1f else -1f - PredictiveBackExitDirection.ALWAYS_RIGHT -> 1f - PredictiveBackExitDirection.ALWAYS_LEFT -> -1f - } - - // if we are playing the exit animation, calculate the scaled Page's TranslationX in here - val exitProgress = - if (pageKey != currentPageKey.toString()) 1f else exitAnimatable.value - val animatedTranslationX = containerWidthPx * exitProgress * directionMultiplier - - // render animation - val modifier = this - .graphicsLayer { - scaleX = animatedScale - scaleY = animatedScale - translationX = animatedTranslationX - transformOrigin = TransformOrigin(currentPivotX, currentPivotY) - } - .then( - if (transitionState is InProgress) { - Modifier.clip(RoundedCornerShape(deviceCornerRadius)) - } else { - Modifier - } - ) - - modifier - } else { - // We calculate the new page's black dim alpha in here - // If we are in PredictiveBackAnimation, always 0.5f dim - // If we are playing the exit animation, dynamic calculate the dim with exit animation's progress - // If we are in interrupting animation(have backState but not rendering predictiveBackAnimation) we shouldn't play dim - // Place 1f here to let dynamicAlpha calced with 0f - // alpha = 0.5 * (1f - animationProgress) (decrease alpha when increase progress) - // so, alpha will always in 0 - 0.5f - val modifier = if (transitionState is InProgress) { - val progress = if (!inPredictiveBackAnimation) 1f else exitAnimatable.value - val dynamicAlpha = 0.5f * (1f - progress) - - this - .graphicsLayer() - .drawWithContent { - drawContent() - drawRect(color = Color.Black.copy(alpha = dynamicAlpha)) - } - } else Modifier - - modifier - } - - return modifier - } - - override fun AnimatedContentTransitionScope>.onPredictivePopTransitionSpec( - swipeEdge: Int - ): ContentTransform = ContentTransform( - targetContentEnter = EnterTransition.None, - initialContentExit = ExitTransition.None, - sizeTransform = null - ) - - override fun AnimatedContentTransitionScope>.onPopTransitionSpec(): ContentTransform = - ContentTransform( - targetContentEnter = slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn(), - initialContentExit = scaleOut(targetScale = 0.9f) + fadeOut(), - sizeTransform = null - ) - - override fun AnimatedContentTransitionScope>.onTransitionSpec(): ContentTransform = - ContentTransform( - targetContentEnter = slideInHorizontally(initialOffsetX = { it }), - initialContentExit = fadeOut(), - sizeTransform = null - ) -} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SegmentedColumn.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SegmentedColumn.kt index 46edf7d0e..f92c9d676 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SegmentedColumn.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SegmentedColumn.kt @@ -88,6 +88,7 @@ class SegmentedColumnScope { content: @Composable (Shape) -> Unit ) { val resolvedForceFlatTop = forceFlatTop || isInsideExpandableBody + val resolvedForceFlatBottom = forceFlatBottom || isInsideExpandableBody val resolvedVisible = visible && parentVisibilityMask items.add( @@ -96,7 +97,7 @@ class SegmentedColumnScope { visible = resolvedVisible, customTopPadding = topPadding, forceFlatTop = resolvedForceFlatTop, - forceFlatBottom = forceFlatBottom, + forceFlatBottom = resolvedForceFlatBottom, content = content ) ) @@ -126,8 +127,16 @@ class SegmentedColumnScope { isInsideExpandableBody = true parentVisibilityMask = previousVisibilityMask && animatedVisibility && expanded + val headerIndex = items.lastIndex bottomContent() + if (!previousInsideBody) { + val lastGroupIndex = items.lastIndex + if (lastGroupIndex >= headerIndex) { + items[lastGroupIndex] = items[lastGroupIndex].copy(forceFlatBottom = false) + } + } + isInsideExpandableBody = previousInsideBody parentVisibilityMask = previousVisibilityMask } @@ -196,17 +205,22 @@ fun SegmentedColumn( val baseTopRadius = if (isFirst) 16.dp else 5.dp val baseBottomRadius = if (isLast) 16.dp else 5.dp - val targetTopRadius = if (itemData.forceFlatTop) 0.dp else baseTopRadius + // Blurred backgrounds must be rendered as one continuous group. Keep + // only the outer corners rounded, regardless of item-level overrides. + val forceFlatTop = + if (themeConfig.isEnableBlurExp) !isFirst else itemData.forceFlatTop + val forceFlatBottom = + if (themeConfig.isEnableBlurExp) !isLast else itemData.forceFlatBottom + + val targetTopRadius = if (forceFlatTop) 0.dp else baseTopRadius val targetBottomRadius = - if (itemData.forceFlatBottom) 0.dp else baseBottomRadius + if (forceFlatBottom) 0.dp else baseBottomRadius val isDynamicDpSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU - val currentTopRadius = if (isDynamicDpSupported) { animateDpAsState(targetTopRadius, dpSpring, label = "TopRadius").value } else targetTopRadius - val currentBottomRadius = if (isDynamicDpSupported) { animateDpAsState( targetBottomRadius, @@ -303,4 +317,4 @@ fun SegmentedColumn( } } } -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SettingsBaseWidget.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SettingsBaseWidget.kt index 4d9aaf6fb..62beab3db 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SettingsBaseWidget.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SettingsBaseWidget.kt @@ -19,8 +19,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CornerBasedShape @@ -48,7 +47,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.vector.ImageVector @@ -60,7 +61,9 @@ import androidx.compose.ui.semantics.disabled import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.resukisu.resukisu.ui.component.settings.material3internal.rememberAnimatedShape import com.resukisu.resukisu.ui.theme.CardConfig @@ -140,8 +143,14 @@ fun SettingsBaseWidget( val interactionSource = remember { MutableInteractionSource() } - val density = LocalDensity.current - val dynamicInternalPadding = (4 * density.fontScale).dp + /* + * Material 3 ListItem uses fixed 56dp/72dp minimum heights that do not shrink with fontScale, + * leaving excessive vertical space at smaller system font sizes. Recheck this workaround when + * updating Material 3 in case ListItem starts adapting its minimum height internally. + */ + val fontScale = LocalDensity.current.fontScale + val defaultMinHeight = if (description == null) 56.dp else 72.dp + val adaptiveMinHeight = (defaultMinHeight * fontScale).coerceAtLeast(48.dp) val baseShape = LocalSegmentedItemShape.current @@ -242,6 +251,32 @@ fun SettingsBaseWidget( ) } else RectangleShape + val safeClickShape = if (onClick != null || onLongClick != null) { + remember(clickShape) { + object : Shape { + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density, + ): Outline = clickShape.createOutline(size, layoutDirection, density) + } + } + } else { + RectangleShape + } + val listItemShapes = if (onClick != null || onLongClick != null) { + ListItemDefaults.shapes( + shape = safeClickShape, + selectedShape = safeClickShape, + pressedShape = safeClickShape, + focusedShape = safeClickShape, + hoveredShape = safeClickShape, + draggedShape = safeClickShape, + ) + } else { + shapes + } + val clipShape = if (onClick != null || onLongClick != null) { clickShape } else { @@ -249,6 +284,7 @@ fun SettingsBaseWidget( } var itemModifier = (if (fillMaxWidth) modifier.fillMaxWidth() else modifier) + .heightIn(min = adaptiveMinHeight) if (isOnBackground && themeConfig.isEnableBlurExp) itemModifier = itemModifier .clip(clipShape) @@ -295,10 +331,6 @@ fun SettingsBaseWidget( } descriptionColumnContent?.invoke(this) - - if (description != null || descriptionColumnContent != null) { - Spacer(Modifier.height(dynamicInternalPadding)) - } } } @@ -317,10 +349,6 @@ fun SettingsBaseWidget( Box( modifier = Modifier .alpha(alpha) - .padding( - top = dynamicInternalPadding, - bottom = if (description == null && descriptionColumnContent == null) dynamicInternalPadding else 0.dp - ) ) { Row( verticalAlignment = Alignment.CenterVertically @@ -367,7 +395,7 @@ fun SettingsBaseWidget( } else null, enabled = enabled, colors = colors, - shapes = shapes, + shapes = listItemShapes, verticalAlignment = Alignment.CenterVertically, leadingContent = finalLeadingContent, supportingContent = supportingContent, @@ -384,7 +412,6 @@ fun SettingsBaseWidget( * which incorrectly exposes the item as disabled and changes its visual state. */ ListItem( - headlineContent = headline, modifier = itemModifier .clip(baseShape) .then( @@ -394,10 +421,15 @@ fun SettingsBaseWidget( Modifier } ), + enabled = enabled, + verticalAlignment = Alignment.CenterVertically, + shapes = shapes, colors = colors, leadingContent = finalLeadingContent, supportingContent = supportingContent, - trailingContent = trailing + trailingContent = trailing, + contentPadding = ListItemDefaults.ContentPadding, + content = headline, ) } } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SettingsDropdownWidget.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SettingsDropdownWidget.kt index e03b506c8..310ffd9c7 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SettingsDropdownWidget.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SettingsDropdownWidget.kt @@ -3,11 +3,11 @@ package com.resukisu.resukisu.ui.component.settings import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.offset import androidx.compose.material3.DropdownMenuGroup -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuPopup import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MenuDefaults +import androidx.compose.material3.SelectableDropdownMenuItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -81,9 +81,7 @@ fun SettingsDropdownWidget( data.forEachIndexed { index, item -> val isSelected = index == choice - // Utilize the selectable variation of DropdownMenuItem - // MenuDefaults.itemShape(index, count) automatically handles the shapes - DropdownMenuItem( + SelectableDropdownMenuItem( selected = isSelected, onClick = { onChoiceChange(index) @@ -93,7 +91,7 @@ fun SettingsDropdownWidget( shapes = MenuDefaults.itemShape( index = index, count = data.size - ) + ), ) } } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/navigation/Navigator.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/navigation/Navigator.kt index 128008310..54ca8dfa2 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/navigation/Navigator.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/navigation/Navigator.kt @@ -3,24 +3,21 @@ package com.resukisu.resukisu.ui.navigation import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.listSaver -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.staticCompositionLocalOf -import androidx.navigation3.runtime.NavKey import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow +import top.yukonga.miuix.kmp.nav.core.NavBackStack +import top.yukonga.miuix.kmp.nav.core.NavKey /** * Simple navigation helper that owns a back stack and result channels. * Supports push/replace/pop/popUntil and result APIs: navigateForResult/setResult/observeResult/clearResult. */ class Navigator( - initialKey: NavKey + val backStack: NavBackStack ) { - val backStack: SnapshotStateList = mutableStateListOf(initialKey) + constructor(vararg initial: NavKey) : this(mutableStateListOf(*initial)) private val resultBus = mutableMapOf>() @@ -134,28 +131,8 @@ class Navigator( private fun ensureChannel(key: String): MutableSharedFlow { return resultBus.getOrPut(key) { MutableSharedFlow(replay = 1, extraBufferCapacity = 0) } } - - companion object { - val Saver: Saver = listSaver(save = { navigator -> - navigator.backStack.toList() - }, restore = { savedList -> - val initialKey = savedList.firstOrNull() ?: Route.Home - val navigator = Navigator(initialKey) - navigator.backStack.clear() - navigator.backStack.addAll(savedList) - navigator - }) - } -} - - -@Composable -fun rememberNavigator(startRoute: NavKey): Navigator { - return rememberSaveable(startRoute, saver = Navigator.Saver) { - Navigator(startRoute) - } } val LocalNavigator = staticCompositionLocalOf { error("LocalNavigator not provided") -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/navigation/Routes.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/navigation/Routes.kt index 441fe86a3..a726e4f84 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/navigation/Routes.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/navigation/Routes.kt @@ -1,14 +1,15 @@ package com.resukisu.resukisu.ui.navigation import android.os.Parcelable -import androidx.navigation3.runtime.NavKey import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable +import top.yukonga.miuix.kmp.nav.core.NavKey /** - * Type-safe navigation keys for Navigation3. + * Type-safe navigation keys for Navigation. * Each destination is a NavKey (data object/data class) and can be saved/restored in the back stack. */ +@Serializable sealed interface Route : NavKey, Parcelable { @Parcelize @Serializable diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/AppProfile.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/AppProfile.kt index 48a1a41fe..a9b959105 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/AppProfile.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/AppProfile.kt @@ -13,14 +13,11 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape @@ -39,9 +36,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarColors import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -87,6 +82,7 @@ import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.theme.renderBackgroundBlur import com.resukisu.resukisu.ui.util.ActivityResumeEffect import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.showReplacingSnackbar import com.resukisu.resukisu.ui.viewmodel.AppProfileUiAction import com.resukisu.resukisu.ui.viewmodel.AppProfileUiEvent @@ -160,27 +156,37 @@ fun AppProfileScreen( colorScheme.surfaceContainer } - LaunchedEffect(Unit) { - scrollBehavior.state.heightOffset = scrollBehavior.state.heightOffsetLimit - } - Scaffold( topBar = { - TopBar( - title = appGroup.mainApp.label, - packageName = appGroup.mainApp.displayIdentifier, + LargeFlexibleTopAppBar( + modifier = Modifier.blurEffect(), + title = { + Text( + text = appGroup.mainApp.label, + ) + }, + subtitle = { + Text( + text = appGroup.mainApp.displayIdentifier + ) + }, colors = TopAppBarDefaults.topAppBarColors( containerColor = cardColor, scrolledContainerColor = cardColor ), - onBack = dropUnlessResumed { navigator.pop() }, + navigationIcon = { + AppBackButton( + onClick = dropUnlessResumed { navigator.pop() } + ) + }, + windowInsets = TopAppBarDefaults.windowInsets.add(WindowInsets(left = 12.dp)), scrollBehavior = scrollBehavior, ) }, snackbarHost = { SwipeableSnackbarHost(hostState = snackBarHost) }, containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onSurface, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + contentWindowInsets = adaptiveScaffoldWindowInsets() ) { paddingValues -> AppProfileInner( modifier = Modifier @@ -188,6 +194,7 @@ fun AppProfileScreen( .nestedScroll(scrollBehavior.nestedScrollConnection) .blurSource(), topPadding = paddingValues.calculateTopPadding(), + bottomPadding = paddingValues.calculateBottomPadding(), appGroup = appGroup, isSpecial = isSpecial, appIcon = { @@ -234,6 +241,7 @@ fun AppProfileScreen( private fun AppProfileInner( modifier: Modifier = Modifier, topPadding: Dp, + bottomPadding: Dp = 0.dp, appGroup: InstalledAppGroup, isSpecial: Boolean = false, appIcon: @Composable () -> Unit, @@ -488,7 +496,11 @@ private fun AppProfileInner( } item { - Spacer(modifier = Modifier.height(6.dp + 48.dp + 6.dp /* SnackBar height */)) + Spacer( + modifier = Modifier.height( + bottomPadding + 6.dp + 48.dp + 6.dp /* SnackBar height */ + ) + ) } } } @@ -500,39 +512,6 @@ private enum class Mode(@param:StringRes private val res: Int) { @Composable get() = stringResource(res) } -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) -@Composable -private fun TopBar( - title: String, - packageName: String, - onBack: () -> Unit, - colors: TopAppBarColors, - scrollBehavior: TopAppBarScrollBehavior? = null, -) { - LargeFlexibleTopAppBar( - modifier = Modifier.blurEffect( - ), - title = { - Text( - text = title, - ) - }, - subtitle = { - Text( - text = packageName - ) - }, - colors = colors, - navigationIcon = { - AppBackButton( - onClick = onBack - ) - }, - windowInsets = TopAppBarDefaults.windowInsets.add(WindowInsets(left = 12.dp)), - scrollBehavior = scrollBehavior, - ) -} - @Composable private fun ProfileBox( mode: Mode, @@ -542,6 +521,7 @@ private fun ProfileBox( Column { SettingsBaseWidget( icon = Icons.TwoTone.AccountCircle, + iconColor = MaterialTheme.colorScheme.onSurface, title = stringResource(R.string.profile), description = mode.text, isOnBackground = false, diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/DynamicManagerScreen.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/DynamicManagerScreen.kt index e253b0ed7..f63324942 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/DynamicManagerScreen.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/DynamicManagerScreen.kt @@ -63,6 +63,7 @@ import com.resukisu.resukisu.ui.navigation.LocalNavigator import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.util.ActivityResumeEffect import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.showReplacingSnackbar import com.resukisu.resukisu.ui.viewmodel.DynamicManagerAppItem import com.resukisu.resukisu.ui.viewmodel.DynamicManagerOperation @@ -151,6 +152,7 @@ fun DynamicManagerScreen() { } Scaffold( + contentWindowInsets = adaptiveScaffoldWindowInsets(), topBar = { SearchAppBar( title = stringResource(R.string.dynamic_manager_title), diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/ExecuteModuleAction.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/ExecuteModuleAction.kt index c0d655ee4..bdfae687e 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/ExecuteModuleAction.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/ExecuteModuleAction.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons @@ -51,6 +50,7 @@ import com.resukisu.resukisu.ui.theme.ThemeConfig import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.showReplacingSnackbar import com.resukisu.resukisu.ui.viewmodel.ExecuteModuleActionUiAction import com.resukisu.resukisu.ui.viewmodel.ExecuteModuleActionUiEvent @@ -141,7 +141,7 @@ fun ExecuteModuleActionScreen(moduleId: String) { }, containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onSurface, - contentWindowInsets = WindowInsets.safeDrawing, + contentWindowInsets = adaptiveScaffoldWindowInsets(), snackbarHost = { SwipeableSnackbarHost(hostState = snackBarHost) } ) { innerPadding -> KeyEventBlocker { diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Flash.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Flash.kt index 664ec1add..56b96c74f 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Flash.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Flash.kt @@ -69,7 +69,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -97,6 +96,7 @@ import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.theme.renderBackgroundBlur import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.showReplacingSnackbar import com.resukisu.resukisu.ui.viewmodel.FlashUiAction import com.resukisu.resukisu.ui.viewmodel.FlashViewModel @@ -441,6 +441,7 @@ fun FlashScreen(flashIt: FlashIt) { } Scaffold( + contentWindowInsets = adaptiveScaffoldWindowInsets(), topBar = { TopBar( flashUiState.flashingStatus, diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Install.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Install.kt index cae45fa05..462e79557 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Install.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Install.kt @@ -91,6 +91,7 @@ import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.theme.getCardColors import com.resukisu.resukisu.ui.theme.getCardElevation import com.resukisu.resukisu.ui.theme.renderBackgroundBlur +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.viewmodel.InstallUiEvent import com.resukisu.resukisu.ui.viewmodel.InstallViewModel import org.koin.compose.koinInject @@ -263,6 +264,7 @@ fun InstallScreen( } Scaffold( + contentWindowInsets = adaptiveScaffoldWindowInsets(), topBar = { TopBar( onBack = { navigator.pop() }, diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/SulogScreen.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/SulogScreen.kt index 4d81e03aa..0b73483ee 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/SulogScreen.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/SulogScreen.kt @@ -6,16 +6,9 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.captionBar import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.rememberLazyListState @@ -30,7 +23,6 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.DropdownMenuGroup -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuPopup import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -40,6 +32,7 @@ import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MenuDefaults import androidx.compose.material3.Scaffold +import androidx.compose.material3.SelectableDropdownMenuItem import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -91,6 +84,7 @@ import com.resukisu.resukisu.ui.theme.CardConfig import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.util.ActivityResumeEffect import com.resukisu.resukisu.ui.util.LocalBlurState +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.viewmodel.SulogActions import com.resukisu.resukisu.ui.viewmodel.SulogFileSelector import com.resukisu.resukisu.ui.viewmodel.SulogScreenState @@ -210,17 +204,17 @@ private fun SulogScreenContent( Spacer(modifier = Modifier.height(2.dp)) SulogEventFilter.entries.forEachIndexed { index, filter -> - DropdownMenuItem( + SelectableDropdownMenuItem( selected = filter in state.selectedFilters, - text = { Text(sulogFilterLabel(filter)) }, onClick = { haptic.performHapticFeedback(HapticFeedbackType.VirtualKey) actions.onToggleFilter(filter) }, + text = { Text(sulogFilterLabel(filter)) }, shapes = MenuDefaults.itemShape( index = index, count = SulogEventFilter.entries.size - ) + ), ) Spacer(modifier = Modifier.height(2.dp)) } @@ -232,7 +226,7 @@ private fun SulogScreenContent( searchBarPlaceHolderText = stringResource(R.string.sulog_search_placeholder) ) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + contentWindowInsets = adaptiveScaffoldWindowInsets(), containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onSurface ) { innerPadding -> @@ -318,13 +312,7 @@ private fun SulogScreenContent( item { Spacer( - Modifier.height( - WindowInsets.navigationBars.asPaddingValues() - .calculateBottomPadding() + - WindowInsets.captionBar.asPaddingValues() - .calculateBottomPadding() + - 16.dp - ) + Modifier.height(innerPadding.calculateBottomPadding() + 16.dp) ) } } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Template.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Template.kt index 14400a3cf..bd191ad8f 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Template.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Template.kt @@ -1,8 +1,5 @@ package com.resukisu.resukisu.ui.screen -import org.koin.compose.koinInject -import com.resukisu.resukisu.ui.theme.CardConfig -import com.resukisu.resukisu.ui.theme.ThemeConfig import android.content.ClipData import android.content.ClipboardManager import android.widget.Toast @@ -19,7 +16,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -67,7 +63,6 @@ import androidx.compose.ui.unit.dp import androidx.core.content.getSystemService import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.dropUnlessResumed -import org.koin.compose.viewmodel.koinViewModel import com.resukisu.resukisu.R import com.resukisu.resukisu.domain.model.ProfileTemplate import com.resukisu.resukisu.ui.component.NetworkRefreshContent @@ -77,13 +72,18 @@ import com.resukisu.resukisu.ui.component.settings.lazySegmentColumn import com.resukisu.resukisu.ui.navigation.LocalNavigator import com.resukisu.resukisu.ui.navigation.Navigator import com.resukisu.resukisu.ui.navigation.Route +import com.resukisu.resukisu.ui.theme.CardConfig +import com.resukisu.resukisu.ui.theme.ThemeConfig import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.util.ActivityResumeEffect +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets +import com.resukisu.resukisu.ui.viewmodel.TemplateUiAction import com.resukisu.resukisu.ui.viewmodel.TemplateUiEvent import com.resukisu.resukisu.ui.viewmodel.TemplateViewModel -import com.resukisu.resukisu.ui.viewmodel.TemplateUiAction import kotlinx.coroutines.launch +import org.koin.compose.koinInject +import org.koin.compose.viewmodel.koinViewModel /** * @author weishu @@ -203,7 +203,7 @@ fun AppProfileTemplateScreen() { }, containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onSurface, - contentWindowInsets = WindowInsets.safeDrawing, + contentWindowInsets = adaptiveScaffoldWindowInsets(), ) { innerPadding -> if (uiState.templateList.isEmpty()) { LazyColumn( @@ -403,7 +403,7 @@ private fun TopBar( shapes = MenuDefaults.groupShapes() ) { DropdownMenuItem( - selected = false, + shape = MenuDefaults.itemShape(0, 2).shape, text = { Text(stringResource(id = R.string.app_profile_import_from_clipboard)) }, @@ -411,10 +411,9 @@ private fun TopBar( onImport() showDropdown = false }, - shapes = MenuDefaults.itemShape(index = 0, count = 2) ) DropdownMenuItem( - selected = false, + shape = MenuDefaults.itemShape(1, 2).shape, text = { Text(stringResource(id = R.string.app_profile_export_to_clipboard)) }, @@ -422,7 +421,6 @@ private fun TopBar( onExport() showDropdown = false }, - shapes = MenuDefaults.itemShape(index = 1, count = 2) ) } } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/TemplateEditor.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/TemplateEditor.kt index a497ae717..9132487bf 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/TemplateEditor.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/TemplateEditor.kt @@ -1,24 +1,21 @@ package com.resukisu.resukisu.ui.screen import android.widget.Toast -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.twotone.DeleteForever import androidx.compose.material.icons.twotone.Save +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LargeFlexibleTopAppBar @@ -33,8 +30,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -45,22 +42,23 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.dropUnlessResumed import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.compose.dropUnlessResumed import com.resukisu.resukisu.Natives.Profile.RootProfileFlag import com.resukisu.resukisu.R import com.resukisu.resukisu.domain.model.AppProfile import com.resukisu.resukisu.domain.model.ProfileTemplate import com.resukisu.resukisu.toRawFlags import com.resukisu.resukisu.toRootProfileFlags -import com.resukisu.resukisu.ui.component.profile.rootProfileConfig import com.resukisu.resukisu.ui.component.NetworkRefreshContent +import com.resukisu.resukisu.ui.component.profile.rootProfileConfig import com.resukisu.resukisu.ui.component.settings.AppBackButton import com.resukisu.resukisu.ui.component.settings.SegmentedColumn import com.resukisu.resukisu.ui.component.settings.SettingsTextFieldWidget import com.resukisu.resukisu.ui.navigation.LocalNavigator import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.viewmodel.TemplateEditorUiAction import com.resukisu.resukisu.ui.viewmodel.TemplateEditorUiEvent import com.resukisu.resukisu.ui.viewmodel.TemplateEditorViewModel @@ -143,7 +141,7 @@ fun TemplateEditorScreen( scrollBehavior = scrollBehavior ) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + contentWindowInsets = adaptiveScaffoldWindowInsets(), containerColor = Color.Transparent, ) { innerPadding -> LazyColumn( diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/UmountManagerScreen.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/UmountManagerScreen.kt index 3a01b8565..328d72135 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/UmountManagerScreen.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/UmountManagerScreen.kt @@ -67,6 +67,7 @@ import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.util.ActivityResumeEffect import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.showReplacingSnackbar import com.resukisu.resukisu.ui.viewmodel.UmountManagerScreenViewModel import com.resukisu.resukisu.ui.viewmodel.UmountManagerUiAction @@ -112,6 +113,7 @@ fun UmountManagerScreen() { } Scaffold( + contentWindowInsets = adaptiveScaffoldWindowInsets(), topBar = { LargeFlexibleTopAppBar( modifier = Modifier diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/about/About.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/about/About.kt index 8dc01db22..90d537657 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/about/About.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/about/About.kt @@ -1,8 +1,5 @@ package com.resukisu.resukisu.ui.screen.about -import org.koin.compose.koinInject -import com.resukisu.resukisu.ui.theme.CardConfig -import com.resukisu.resukisu.ui.theme.ThemeConfig import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -62,9 +59,13 @@ import com.resukisu.resukisu.ui.component.settings.SettingsJumpPageWidget import com.resukisu.resukisu.ui.navigation.LocalNavigator import com.resukisu.resukisu.ui.navigation.Navigator import com.resukisu.resukisu.ui.navigation.Route +import com.resukisu.resukisu.ui.theme.CardConfig +import com.resukisu.resukisu.ui.theme.ThemeConfig import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.theme.renderBackgroundBlur +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets +import org.koin.compose.koinInject @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @@ -81,6 +82,7 @@ fun AboutScreen() { ) Scaffold( + contentWindowInsets = adaptiveScaffoldWindowInsets(), topBar = { LargeFlexibleTopAppBar( modifier = Modifier.blurEffect( diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/about/OpenSourceLicenseScreen.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/about/OpenSourceLicenseScreen.kt index 958cb1a49..a0d4ee7d1 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/about/OpenSourceLicenseScreen.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/about/OpenSourceLicenseScreen.kt @@ -30,37 +30,41 @@ import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.contentColorFor import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties +import com.mikepenz.aboutlibraries.Libs import com.mikepenz.aboutlibraries.entity.Library -import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults -import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries -import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer -import com.mikepenz.aboutlibraries.ui.compose.m3.chipColors -import com.mikepenz.aboutlibraries.ui.compose.m3.libraryColors +import com.mikepenz.aboutlibraries.ui.compose.util.author +import com.mikepenz.aboutlibraries.util.withJson import com.resukisu.resukisu.R import com.resukisu.resukisu.ui.component.WarningCard import com.resukisu.resukisu.ui.component.settings.AppBackButton +import com.resukisu.resukisu.ui.component.settings.SettingsBaseWidget +import com.resukisu.resukisu.ui.component.settings.lazySegmentColumn import com.resukisu.resukisu.ui.navigation.LocalNavigator +import com.resukisu.resukisu.ui.screen.LabelText import com.resukisu.resukisu.ui.theme.CardConfig import com.resukisu.resukisu.ui.theme.ThemeConfig import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource -import com.resukisu.resukisu.ui.theme.renderBackgroundBlur +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.koin.compose.koinInject @@ -82,13 +86,17 @@ fun OpenSourceLicenseScreen() { } // from https://github.com/mikepenz/AboutLibraries#setup - // Android: Provide resource identifier for the `R.raw.aboutlibraries` file. - // This file is generated by the AboutLibraries Gradle plugin. - val libraries by produceLibraries(R.raw.aboutlibraries) + val context = LocalContext.current + val libraries by produceState(initialValue = Libs(emptyList(), emptySet()), context) { + value = withContext(Dispatchers.IO) { + Libs.Builder().withJson(context, R.raw.aboutlibraries).build() + } + } var selectedLibrary by remember { mutableStateOf(null) } Scaffold( + contentWindowInsets = adaptiveScaffoldWindowInsets(), modifier = Modifier .fillMaxSize() .nestedScroll(scrollBehavior.nestedScrollConnection), @@ -96,8 +104,7 @@ fun OpenSourceLicenseScreen() { contentColor = MaterialTheme.colorScheme.onSurface, topBar = { LargeFlexibleTopAppBar( - modifier = Modifier.blurEffect( - ), + modifier = Modifier.blurEffect(), windowInsets = TopAppBarDefaults.windowInsets.add(WindowInsets(left = 12.dp)), title = { Text(text = stringResource(id = R.string.open_source_license)) }, scrollBehavior = scrollBehavior, @@ -126,35 +133,34 @@ fun OpenSourceLicenseScreen() { ) }, ) { paddingValues -> - val cornerRadius = 16.dp - LibrariesContainer( - libraries = libraries, - libraryModifier = Modifier - .padding(vertical = 4.dp) - .clip(RoundedCornerShape(cornerRadius)) - .renderBackgroundBlur(), + LazyColumn( modifier = Modifier .fillMaxSize() - .padding(horizontal = 16.dp) .blurSource(), - contentPadding = paddingValues,// PaddingValues(horizontal = 16.dp), - colors = LibraryDefaults.libraryColors( - libraryBackgroundColor = if (themeConfig.isEnableBlurExp) Color.Transparent else MaterialTheme.colorScheme.surfaceBright.copy( - alpha = cardConfig.cardAlpha - ), - libraryContentColor = MaterialTheme.colorScheme.onSurface, - // To maintain the original appearance, explicitly set the license chip colors - // to match the old function's default badge colors. - licenseChipColors = LibraryDefaults.chipColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = contentColorFor(MaterialTheme.colorScheme.primary) - ) - ), - onLibraryClick = { library -> - selectedLibrary = library + contentPadding = paddingValues + ) { + lazySegmentColumn(libraries.libraries) { _, lib -> + SettingsBaseWidget( + iconPlaceholder = false, + title = lib.name, + description = lib.author, + descriptionColumnContent = { + Row { + lib.licenses.forEach { + LabelText(it.name) + } + } + }, + onClick = { + selectedLibrary = lib + } + ) { + lib.artifactVersion?.let { + Text(it) + } + } } - ) - + } if (selectedLibrary != null) { val library = selectedLibrary!! val uriHandler = LocalUriHandler.current diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/kernelFlash/KernelFlash.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/kernelFlash/KernelFlash.kt index bf08f5710..da7a08535 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/kernelFlash/KernelFlash.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/kernelFlash/KernelFlash.kt @@ -10,14 +10,10 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -64,6 +60,7 @@ import com.resukisu.resukisu.ui.navigation.LocalNavigator import com.resukisu.resukisu.ui.theme.CardConfig import com.resukisu.resukisu.ui.theme.MonospaceFontFamily import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.showReplacingSnackbar import com.resukisu.resukisu.ui.viewmodel.KernelFlashUiAction import com.resukisu.resukisu.ui.viewmodel.KernelFlashUiEvent @@ -189,7 +186,7 @@ fun KernelFlashScreen( } }, snackbarHost = { SwipeableSnackbarHost(hostState = snackBarHost) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + contentWindowInsets = adaptiveScaffoldWindowInsets(), containerColor = MaterialTheme.colorScheme.background ) { innerPadding -> KeyEventBlocker { @@ -400,7 +397,7 @@ private fun TopBar( ) } }, - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + windowInsets = adaptiveScaffoldWindowInsets(includeBottom = false), scrollBehavior = scrollBehavior ) } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/HomePage.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/HomePage.kt index cf03690db..7f08bf75d 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/HomePage.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/HomePage.kt @@ -17,25 +17,34 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.twotone.MenuBook +import androidx.compose.material.icons.twotone.Android import androidx.compose.material.icons.twotone.Block +import androidx.compose.material.icons.twotone.DeveloperBoard import androidx.compose.material.icons.twotone.Error +import androidx.compose.material.icons.twotone.Extension +import androidx.compose.material.icons.twotone.FilterList +import androidx.compose.material.icons.twotone.Group import androidx.compose.material.icons.twotone.Info +import androidx.compose.material.icons.twotone.Memory import androidx.compose.material.icons.twotone.PowerSettingsNew +import androidx.compose.material.icons.twotone.Security +import androidx.compose.material.icons.twotone.Settings +import androidx.compose.material.icons.twotone.Smartphone +import androidx.compose.material.icons.twotone.Tag import androidx.compose.material.icons.twotone.TaskAlt import androidx.compose.material.icons.twotone.Tune +import androidx.compose.material.icons.twotone.VolunteerActivism import androidx.compose.material.icons.twotone.Warning import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -53,9 +62,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior -import androidx.compose.material3.pulltorefresh.PullToRefreshBox -import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults -import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -64,7 +70,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color @@ -100,6 +105,7 @@ import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.util.LocalPermissionRequestInterface import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.downloader.downloadManagerUpdate import com.resukisu.resukisu.ui.viewmodel.HomeUiAction import com.resukisu.resukisu.ui.viewmodel.HomeUiEvent @@ -143,7 +149,6 @@ fun HomePage( if (!uiState.isInitialDataLoaded) return - val pullRefreshState = rememberPullToRefreshState() val topAppBarState = rememberTopAppBarState() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(topAppBarState) val scrollState = rememberScrollState() @@ -161,9 +166,7 @@ fun HomePage( }, containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onSurface, - contentWindowInsets = WindowInsets.safeDrawing.only( - WindowInsetsSides.Top + WindowInsetsSides.Horizontal - ), + contentWindowInsets = adaptiveScaffoldWindowInsets(includeBottom = false), snackbarHost = { SwipeableSnackbarHost( modifier = Modifier.padding(bottom = bottomPadding), @@ -171,39 +174,25 @@ fun HomePage( ) } ) { innerPadding -> - PullToRefreshBox( - state = pullRefreshState, - isRefreshing = uiState.isRefreshing, - onRefresh = { viewModel.dispatch(HomeUiAction.Refresh()) }, + Column( modifier = Modifier .fillMaxSize() - .blurSource(), - indicator = { - PullToRefreshDefaults.LoadingIndicator( - modifier = Modifier - .padding(top = innerPadding.calculateTopPadding()) - .align(Alignment.TopCenter), - state = pullRefreshState, - isRefreshing = uiState.isRefreshing, - ) - }, + .blurSource() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .verticalScroll(scrollState) + .padding( + top = innerPadding.calculateTopPadding() + 2.dp, + start = 16.dp, + end = 16.dp + ), + verticalArrangement = Arrangement.spacedBy(0.dp) ) { - Column( - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) - .verticalScroll(scrollState) - .padding( - top = innerPadding.calculateTopPadding() + 2.dp, - start = 16.dp, - end = 16.dp - ), - verticalArrangement = Arrangement.spacedBy(0.dp) - ) { // 状态卡片 if (uiState.isCoreDataLoaded) { if (uiState.systemStatus.isManager && !uiState.systemStatus.isFullFeatured) { - if ((uiState.systemStatus.ksuVersion ?: 0) > BuildConfig.VERSION_CODE) { + if ((uiState.systemStatus.kernelUAPIVersion + ?: 1) > uiState.systemStatus.managerUAPIVersion + ) { WarningCard( message = stringResource(R.string.require_manager_version), icon = { @@ -369,17 +358,17 @@ fun HomePage( systemStatus = uiState.systemStatus, systemInfo = uiState.systemInfo, isSimpleMode = uiState.isSimpleMode, + showHomeCardIcons = uiState.showHomeCardIcons, ) } // 链接卡片 if (!uiState.isSimpleMode) { - DonateCard() - LearnMoreCard() + DonateCard(uiState.showHomeCardIcons) + LearnMoreCard(uiState.showHomeCardIcons) } Spacer(Modifier.height(bottomPadding)) - } } } } @@ -476,13 +465,9 @@ fun RebootDropdownItems( ) { items.onEachIndexed { index, (id, reason) -> DropdownMenuItem( - selected = false, + shape = MenuDefaults.itemShape(index, items.size).shape, text = { Text(stringResource(id)) }, onClick = { onReboot(reason) }, - shapes = MenuDefaults.itemShape( - index = index, - count = items.size - ) ) } } @@ -533,7 +518,8 @@ private fun TopBar( // 重启按钮 var showDropdown by remember { mutableStateOf(false) } - KsuIsValid(uiState.systemStatus) { -> if (uiState.systemStatus.isRootAvailable) { + KsuIsValid(uiState.systemStatus) { + if (uiState.systemStatus.isRootAvailable) { IconButton(onClick = { showDropdown = true }) { @@ -682,7 +668,9 @@ private fun StatusCard( } @Composable -fun LearnMoreCard() { +fun LearnMoreCard( + showIcon: Boolean, +) { val uriHandler = LocalUriHandler.current val url = stringResource(R.string.home_learn_kernelsu_url) @@ -693,6 +681,7 @@ fun LearnMoreCard() { ) { item { SettingsBaseWidget( + icon = Icons.AutoMirrored.TwoTone.MenuBook.takeIf { showIcon }, iconPlaceholder = false, title = stringResource(R.string.home_learn_kernelsu), description = stringResource(R.string.home_click_to_learn_kernelsu), @@ -705,7 +694,9 @@ fun LearnMoreCard() { } @Composable -fun DonateCard() { +fun DonateCard( + showIcon: Boolean, +) { val uriHandler = LocalUriHandler.current SegmentedColumn( modifier = Modifier.fillMaxWidth(), @@ -714,6 +705,7 @@ fun DonateCard() { ) { item { SettingsBaseWidget( + icon = Icons.TwoTone.VolunteerActivism.takeIf { showIcon }, iconPlaceholder = false, title = stringResource(R.string.home_support_title), description = stringResource(R.string.home_support_content), @@ -730,6 +722,7 @@ private fun InfoCard( systemStatus: KernelStatus, systemInfo: HomeSystemInfo, isSimpleMode: Boolean, + showHomeCardIcons: Boolean, ) { val managersList = systemInfo.managersList @@ -740,6 +733,7 @@ private fun InfoCard( ) { item { SettingsBaseWidget( + icon = Icons.TwoTone.Smartphone.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_device_model), description = systemInfo.deviceModel, @@ -748,6 +742,7 @@ private fun InfoCard( item { SettingsBaseWidget( + icon = Icons.TwoTone.DeveloperBoard.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_kernel), description = systemInfo.kernelRelease, @@ -758,6 +753,7 @@ private fun InfoCard( visible = !isSimpleMode ) { SettingsBaseWidget( + icon = Icons.TwoTone.Android.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_android_version), description = systemInfo.androidVersion, @@ -769,6 +765,7 @@ private fun InfoCard( visible = systemStatus.isManager ) { SettingsBaseWidget( + icon = Icons.TwoTone.Memory.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_kernel_version), description = systemStatus.ksuFullVersion.orEmpty(), @@ -777,6 +774,7 @@ private fun InfoCard( item { SettingsBaseWidget( + icon = Icons.TwoTone.Tag.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_manager_version), description = "${systemInfo.managerVersion.first} (${systemInfo.managerVersion.second}/${systemInfo.managerVersion.third})", @@ -787,6 +785,7 @@ private fun InfoCard( visible = !isSimpleMode && systemInfo.susfsEnabled && systemInfo.susfsVersion.isNotEmpty() ) { SettingsBaseWidget( + icon = Icons.TwoTone.Settings.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_susfs_version), description = systemInfo.susfsVersion, @@ -801,6 +800,7 @@ private fun InfoCard( ) { item { SettingsBaseWidget( + icon = Icons.TwoTone.Security.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_selinux_status), description = systemInfo.selinuxStatus, @@ -817,6 +817,7 @@ private fun InfoCard( } SettingsBaseWidget( + icon = Icons.TwoTone.FilterList.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_seccomp_status), description = seccompDisplay, @@ -849,6 +850,7 @@ private fun InfoCard( }.trimEnd(' ', '|') SettingsBaseWidget( + icon = Icons.TwoTone.Group.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.multi_manager_list), description = managersText.ifEmpty { stringResource(R.string.no_active_manager) }, @@ -859,6 +861,7 @@ private fun InfoCard( visible = !isSimpleMode && systemStatus.isFullFeatured ) { SettingsBaseWidget( + icon = Icons.TwoTone.Tune.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_hook_type), description = systemStatus.hookType, @@ -869,6 +872,7 @@ private fun InfoCard( visible = !isSimpleMode && systemInfo.zygiskImplement.isNotEmpty() && systemInfo.zygiskImplement != "None" ) { SettingsBaseWidget( + icon = Icons.TwoTone.Extension.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_zygisk_implement), description = systemInfo.zygiskImplement, @@ -879,6 +883,7 @@ private fun InfoCard( visible = !isSimpleMode && systemInfo.metaModuleImplement.isNotEmpty() && systemInfo.metaModuleImplement != "None" ) { SettingsBaseWidget( + icon = Icons.TwoTone.Extension.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_meta_module_implement), description = systemInfo.metaModuleImplement, diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/MainScreen.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/MainScreen.kt index ea27d7e22..a4df10b11 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/MainScreen.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/MainScreen.kt @@ -2,9 +2,9 @@ package com.resukisu.resukisu.ui.screen.main import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.HorizontalPager @@ -39,6 +39,7 @@ import com.resukisu.resukisu.ui.util.LocalBlurState import com.resukisu.resukisu.ui.util.LocalHandlePageChange import com.resukisu.resukisu.ui.util.LocalPagerPage import com.resukisu.resukisu.ui.util.LocalPagerState +import com.resukisu.resukisu.ui.util.LocalPortraitState import com.resukisu.resukisu.ui.util.LocalSelectedPage import com.resukisu.resukisu.ui.util.LocalSnackbarHost import com.resukisu.resukisu.ui.viewmodel.HomeViewModel @@ -119,86 +120,84 @@ fun MainScreen() { LocalHandlePageChange provides handlePageChange, LocalSelectedPage provides uiSelectedPage ) { - BoxWithConstraints( - modifier = Modifier.fillMaxSize() - ) { - val isPortrait = maxWidth < maxHeight || (maxHeight / maxWidth > 1.4f) - val content = @Composable { paddingBottom: Dp -> - HorizontalPager( - modifier = Modifier - .fillMaxSize(), - state = pagerState, - userScrollEnabled = userScrollEnabled, - beyondViewportPageCount = 1, - ) { pageIndex -> - if (pages.isEmpty()) return@HorizontalPager + val content = @Composable { paddingBottom: Dp -> + HorizontalPager( + modifier = Modifier + .fillMaxSize(), + state = pagerState, + userScrollEnabled = userScrollEnabled, + beyondViewportPageCount = 1, + ) { pageIndex -> + if (pages.isEmpty()) return@HorizontalPager - val snackBarHostState = remember { SnackbarHostState() } - CompositionLocalProvider( - LocalSnackbarHost provides snackBarHostState, - LocalPagerPage provides pageIndex, - LocalBlurState provides rememberMaterial3BlurBackdrop( - enableBlur = themeConfig.isEnableBlur, - pagerState = pagerState, - pagerPage = pageIndex, - ), - ) { - val destination = pages[pageIndex] - destination.direction(paddingBottom) - } + val snackBarHostState = remember { SnackbarHostState() } + CompositionLocalProvider( + LocalSnackbarHost provides snackBarHostState, + LocalPagerPage provides pageIndex, + LocalBlurState provides rememberMaterial3BlurBackdrop( + enableBlur = themeConfig.isEnableBlur, + pagerState = pagerState, + pagerPage = pageIndex, + ), + ) { + val destination = pages[pageIndex] + destination.direction(paddingBottom) } } + } - if (isPortrait) { - Scaffold( - modifier = Modifier.fillMaxSize(), - bottomBar = { - NavigationBar( - destinations = pages, - isBottomBar = true, - ) - }, - containerColor = Color.Transparent, - ) { innerPadding -> - Box( - modifier = Modifier.blurSource() - ) { - content(innerPadding.calculateBottomPadding()) - } + if (LocalPortraitState.current) { + Scaffold( + // The child pages own their top-bar insets. The outer scaffold only reserves the + // measured bottom navigation bar height for the pager content. + contentWindowInsets = WindowInsets(), + modifier = Modifier.fillMaxSize(), + bottomBar = { + NavigationBar( + destinations = pages, + isBottomBar = true, + ) + }, + containerColor = Color.Transparent, + ) { innerPadding -> + Box( + modifier = Modifier.blurSource() + ) { + content(innerPadding.calculateBottomPadding()) } - } else { - var navWidth by remember { mutableIntStateOf(0) } - val density = LocalDensity.current + } + } else { + var navWidth by remember { mutableIntStateOf(0) } + val density = LocalDensity.current - Box( - modifier = Modifier.fillMaxSize() + Box( + modifier = Modifier.fillMaxSize() + ) { + Row( + modifier = Modifier + .fillMaxSize() + .blurSource() ) { - Row( - modifier = Modifier - .fillMaxSize() - .blurSource() - ) { - Spacer( - modifier = Modifier.width( - with(density) { navWidth.toDp() } - ) + Spacer( + modifier = Modifier.width( + with(density) { navWidth.toDp() } ) + ) - Box(Modifier.weight(1f)) { - content(0.dp) - } + Box(Modifier.weight(1f)) { + content(0.dp) } - - NavigationBar( - modifier = Modifier - .align(Alignment.CenterStart) - .onSizeChanged { - navWidth = it.width - }, - destinations = pages, - isBottomBar = false, - ) } + + NavigationBar( + modifier = Modifier + .align(Alignment.CenterStart) + .onSizeChanged { + navWidth = it.width + }, + destinations = pages, + isBottomBar = false, + ) } } } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/ModulePage.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/ModulePage.kt index 877b83695..01d2afeb8 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/ModulePage.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/ModulePage.kt @@ -27,15 +27,11 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState @@ -62,8 +58,8 @@ import androidx.compose.material.icons.twotone.Warning import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CheckableDropdownMenuItem import androidx.compose.material3.DropdownMenuGroup -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuPopup import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -159,6 +155,7 @@ import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.theme.renderBackgroundBlur import com.resukisu.resukisu.ui.util.LocalPermissionRequestInterface import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.downloader.download import com.resukisu.resukisu.ui.util.module.Shortcut import com.resukisu.resukisu.ui.util.showReplacingSnackbar @@ -374,9 +371,7 @@ fun ModulePage(bottomPadding: Dp) { }, containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onSurface, - contentWindowInsets = WindowInsets.safeDrawing.only( - WindowInsetsSides.Top + WindowInsetsSides.Horizontal - ), + contentWindowInsets = adaptiveScaffoldWindowInsets(includeBottom = false), snackbarHost = { SwipeableSnackbarHost( hostState = snackBarHost @@ -499,11 +494,11 @@ private fun ModuleDropdown( DropdownMenuGroup( shapes = MenuDefaults.groupShapes(), ) { - DropdownMenuItem( + CheckableDropdownMenuItem( checked = uiState.sortActionFirst, - onCheckedChange = { checked -> + onCheckedChange = { viewModel.dispatch( - ModuleUiAction.Sort(uiState.sortEnabledFirst, checked) + ModuleUiAction.Sort(uiState.sortEnabledFirst, it) ) }, text = { Text(stringResource(R.string.module_sort_action_first)) }, @@ -512,11 +507,11 @@ private fun ModuleDropdown( count = 2, ), ) - DropdownMenuItem( + CheckableDropdownMenuItem( checked = uiState.sortEnabledFirst, - onCheckedChange = { checked -> + onCheckedChange = { viewModel.dispatch( - ModuleUiAction.Sort(checked, uiState.sortActionFirst) + ModuleUiAction.Sort(it, uiState.sortActionFirst) ) }, text = { Text(stringResource(R.string.module_sort_enabled_first)) }, diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt index 59b15b4e0..926479b3f 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt @@ -16,13 +16,10 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape @@ -39,11 +36,11 @@ import androidx.compose.material.icons.twotone.Fence import androidx.compose.material.icons.twotone.FolderDelete import androidx.compose.material.icons.twotone.FolderOff import androidx.compose.material.icons.twotone.Info -import androidx.compose.material.icons.twotone.Language import androidx.compose.material.icons.twotone.Policy import androidx.compose.material.icons.twotone.RemoveCircle import androidx.compose.material.icons.twotone.RemoveModerator import androidx.compose.material.icons.twotone.Save +import androidx.compose.material.icons.twotone.Science import androidx.compose.material.icons.twotone.Security import androidx.compose.material.icons.twotone.Settings import androidx.compose.material.icons.twotone.Share @@ -97,6 +94,7 @@ import com.resukisu.resukisu.ui.theme.ThemeConfig import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.showReplacingSnackbar import com.resukisu.resukisu.ui.viewmodel.HomeViewModel import com.resukisu.resukisu.ui.viewmodel.SettingsUiAction @@ -145,7 +143,7 @@ fun SettingsPage(bottomPadding: Dp) { }, containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onSurface, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + contentWindowInsets = adaptiveScaffoldWindowInsets(includeBottom = false) ) { innerPadding -> val loadingDialog = rememberLoadingDialog() var showBottomsheet by remember { mutableStateOf(false) } @@ -381,6 +379,7 @@ fun SettingsPage(bottomPadding: Dp) { topPadding = 1.dp ) { SettingsSwitchWidget( + icon = Icons.TwoTone.Science, title = stringResource(R.string.settings_check_beta_update), description = stringResource(R.string.settings_check_beta_update_summary), checked = uiState.checkBetaUpdate, @@ -438,7 +437,7 @@ fun SettingsPage(bottomPadding: Dp) { onClick = { showBottomsheet = true } - ) {} + ) } if (homeState.systemStatus.isFullFeatured) { diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SuperUserPage.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SuperUserPage.kt index 837bde05a..fedf331c3 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SuperUserPage.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SuperUserPage.kt @@ -10,14 +10,10 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState @@ -28,7 +24,6 @@ import androidx.compose.material.icons.twotone.ChevronRight import androidx.compose.material.icons.twotone.MoreVert import androidx.compose.material.icons.twotone.SearchOff import androidx.compose.material3.DropdownMenuGroup -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuPopup import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -38,6 +33,7 @@ import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MenuDefaults import androidx.compose.material3.Scaffold +import androidx.compose.material3.SelectableDropdownMenuItem import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults @@ -58,10 +54,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -82,6 +75,7 @@ import com.resukisu.resukisu.ui.navigation.Route import com.resukisu.resukisu.ui.screen.LabelText import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.showReplacingSnackbar import com.resukisu.resukisu.ui.viewmodel.SortType import com.resukisu.resukisu.ui.viewmodel.SuperUserUiAction @@ -185,9 +179,6 @@ fun SuperUserPage(bottomPadding: Dp) { } Scaffold( - modifier = Modifier - .testTag(SUPER_USER_SCREEN_TEST_TAG) - .semantics { testTagsAsResourceId = true }, topBar = { SearchAppBar( title = stringResource(R.string.superuser), @@ -236,7 +227,7 @@ fun SuperUserPage(bottomPadding: Dp) { hostState = snackBarHostState ) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + contentWindowInsets = adaptiveScaffoldWindowInsets(includeBottom = false), ) { innerPadding -> SuperUserContent( innerPadding = innerPadding, @@ -355,7 +346,6 @@ private fun SuperUserContent( state = listState, modifier = Modifier .fillMaxSize() - .testTag(SUPER_USER_LIST_TEST_TAG) .nestedScroll(scrollBehavior.nestedScrollConnection), ) { item { @@ -364,7 +354,7 @@ private fun SuperUserContent( lazySegmentColumn( items = uiState.appGroupList, key = { _, appGroup -> "${appGroup.uid}-${appGroup.profileKey}" }, - contentType = { _, _ -> "AppGroupItem" } + contentType = { _, appGroup -> "${appGroup.uid}-${appGroup.profileKey}" }, ) { _, appGroup -> AppGroupItem( appGroup = appGroup @@ -380,9 +370,6 @@ private fun SuperUserContent( } } -private const val SUPER_USER_LIST_TEST_TAG = "super_user_app_list" -private const val SUPER_USER_SCREEN_TEST_TAG = "super_user_screen" - @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable private fun SuperUserDropdown( @@ -425,12 +412,12 @@ private fun SuperUserDropdown( shapes = MenuDefaults.groupShapes(), ) { SortType.entries.forEachIndexed { index, sortType -> - DropdownMenuItem( + SelectableDropdownMenuItem( selected = uiState.currentSortType == sortType, - text = { Text(stringResource(sortType.displayNameRes)) }, onClick = { viewModel.dispatch(SuperUserUiAction.SetSort(sortType)) }, + text = { Text(stringResource(sortType.displayNameRes)) }, shapes = MenuDefaults.itemShape( index = index, count = SortType.entries.size, @@ -445,13 +432,13 @@ private fun SuperUserDropdown( shapes = MenuDefaults.groupShapes(), ) { menuItems.forEachIndexed { index, menuItem -> - DropdownMenuItem( + SelectableDropdownMenuItem( selected = menuItem.checked, - text = { Text(stringResource(menuItem.titleRes)) }, onClick = { onDismissRequest() menuItem.onClick() }, + text = { Text(stringResource(menuItem.titleRes)) }, shapes = MenuDefaults.itemShape( index = index, count = menuItems.size, diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/moduleRepo/ModuleRepo.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/moduleRepo/ModuleRepo.kt index 188337e4a..97a435eef 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/moduleRepo/ModuleRepo.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/moduleRepo/ModuleRepo.kt @@ -10,15 +10,11 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn @@ -32,8 +28,8 @@ import androidx.compose.material.icons.twotone.Extension import androidx.compose.material.icons.twotone.MoreVert import androidx.compose.material.icons.twotone.Star import androidx.compose.material.icons.twotone.WebAsset +import androidx.compose.material3.CheckableDropdownMenuItem import androidx.compose.material3.DropdownMenuGroup -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuPopup import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api @@ -107,6 +103,7 @@ import com.resukisu.resukisu.ui.theme.renderBackgroundBlur import com.resukisu.resukisu.ui.util.ActivityResumeEffect import com.resukisu.resukisu.ui.util.LocalPermissionRequestInterface import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.downloader.download import com.resukisu.resukisu.ui.viewmodel.ModuleRepoUiAction import com.resukisu.resukisu.ui.viewmodel.ModuleRepoUiState @@ -196,9 +193,7 @@ fun ModuleRepoScreen() { }, containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onSurface, - contentWindowInsets = WindowInsets.safeDrawing.only( - WindowInsetsSides.Top + WindowInsetsSides.Horizontal - ), + contentWindowInsets = adaptiveScaffoldWindowInsets(), snackbarHost = { SwipeableSnackbarHost(hostState = snackBarHost) } ) { innerPadding -> if (isLoading) { @@ -303,10 +298,10 @@ private fun ModuleRepoDropdown( DropdownMenuGroup( shapes = MenuDefaults.groupShapes(), ) { - DropdownMenuItem( + CheckableDropdownMenuItem( checked = uiState.sortStargazerCountFirst, - onCheckedChange = { checked -> - viewModel.dispatch(ModuleRepoUiAction.SetStarsFirst(checked)) + onCheckedChange = { + viewModel.dispatch(ModuleRepoUiAction.SetStarsFirst(it)) }, text = { Text(stringResource(R.string.module_sort_star_first)) }, shapes = MenuDefaults.itemShape( diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/moduleRepo/OnlineModuleDetail.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/moduleRepo/OnlineModuleDetail.kt index 0353a8b4a..e4e709061 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/moduleRepo/OnlineModuleDetail.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/moduleRepo/OnlineModuleDetail.kt @@ -19,14 +19,11 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -104,6 +101,7 @@ import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.theme.renderBackgroundBlur import com.resukisu.resukisu.ui.util.LocalPermissionRequestInterface import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.viewmodel.ModuleDetailUiAction import com.resukisu.resukisu.ui.viewmodel.ModuleDetailViewModel import com.resukisu.resukisu.ui.viewmodel.formatFileSize @@ -238,9 +236,7 @@ private fun OnlineModuleDetailContent(module: CatalogModule) { }, containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onSurface, - contentWindowInsets = WindowInsets.safeDrawing.only( - WindowInsetsSides.Top + WindowInsetsSides.Horizontal - ), + contentWindowInsets = adaptiveScaffoldWindowInsets(), snackbarHost = { SwipeableSnackbarHost(hostState = snackBarHost) } ) { innerPadding -> Column( diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/susfs/SuSFSConfig.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/susfs/SuSFSConfig.kt index 4c7e076fe..65b5f3fe5 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/susfs/SuSFSConfig.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/susfs/SuSFSConfig.kt @@ -9,13 +9,10 @@ import androidx.compose.animation.shrinkHorizontally import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState @@ -62,6 +59,7 @@ import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.util.ActivityResumeEffect import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.showReplacingSnackbar import com.resukisu.resukisu.ui.viewmodel.SuSFSUiAction import com.resukisu.resukisu.ui.viewmodel.SuSFSUiEvent @@ -305,9 +303,7 @@ fun SuSFSConfigScreen() { }, containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onSurface, - contentWindowInsets = WindowInsets.safeDrawing.only( - WindowInsetsSides.Top + WindowInsetsSides.Horizontal - ), + contentWindowInsets = adaptiveScaffoldWindowInsets(), snackbarHost = { SwipeableSnackbarHost(hostState = snackBarHost) } ) { innerPadding -> PullToRefreshBox( diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/ThemeSettings.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/ThemeSettings.kt index d5957f724..9c69f767a 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/ThemeSettings.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/ThemeSettings.kt @@ -33,6 +33,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.twotone.Android import androidx.compose.material.icons.twotone.Animation +import androidx.compose.material.icons.twotone.Badge import androidx.compose.material.icons.twotone.BlurOn import androidx.compose.material.icons.twotone.Brush import androidx.compose.material.icons.twotone.Check @@ -47,6 +48,7 @@ import androidx.compose.material.icons.twotone.Info import androidx.compose.material.icons.twotone.LightMode import androidx.compose.material.icons.twotone.Opacity import androidx.compose.material.icons.twotone.Palette +import androidx.compose.material.icons.twotone.Pin import androidx.compose.material.icons.twotone.Style import androidx.compose.material.icons.twotone.SwapHoriz import androidx.compose.material.icons.twotone.Translate @@ -81,7 +83,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.core.content.FileProvider import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation3.ui.LocalNavAnimatedContentScope import com.materialkolor.PaletteStyle import com.materialkolor.dynamiccolor.ColorSpec import com.resukisu.resukisu.R @@ -107,6 +108,7 @@ import com.resukisu.resukisu.ui.theme.ThemeConfig import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.theme.renderBackgroundBlur +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.viewmodel.HomeUiAction import com.resukisu.resukisu.ui.viewmodel.HomeUiState import com.resukisu.resukisu.ui.viewmodel.HomeViewModel @@ -130,8 +132,7 @@ import kotlin.math.roundToInt @SuppressLint( - "LocalContextConfigurationRead", "LocalContextResourcesRead", "ObsoleteSdkInt", - "RestrictedApi" + "LocalContextConfigurationRead", "LocalContextResourcesRead", "ObsoleteSdkInt" ) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -294,6 +295,7 @@ fun ThemeSettingsScreen( } Scaffold( + contentWindowInsets = adaptiveScaffoldWindowInsets(), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { LargeFlexibleTopAppBar( @@ -352,31 +354,18 @@ fun ThemeSettingsScreen( item { // Predictive Back Settings - val transition = LocalNavAnimatedContentScope.current.transition - SegmentedColumn( title = stringResource(R.string.predictive_back_settings) ) { item { PredictiveBackAnimationWidget(settingsState) { animation -> - // Hey Google - // Why you keep playing the animation even we are already play completed? - - // This is very dirty, We are using RestrictedApi, but we don't have other choice - transition.setPlaytimeAfterInitialAndTargetStateEstablished( - transition.targetState, - transition.targetState, - transition.playTimeNanos - ) - settingsViewModel.dispatch( SettingsUiAction.SetPredictiveBackAnimation(animation) ) } } item( - visible = settingsState.predictiveBackAnimation == PredictiveBackAnimation.Scale || - settingsState.predictiveBackAnimation == PredictiveBackAnimation.AOSP + visible = settingsState.predictiveBackAnimation == PredictiveBackAnimation.Scale ) { PredictiveBackAnimationDirectionWidget(settingsState) { direction -> settingsViewModel.dispatch( @@ -516,25 +505,27 @@ private fun AppearanceSettings( ) } - item { - // 动态颜色开关 - SettingsSwitchWidget( - icon = Icons.TwoTone.ColorLens, - title = stringResource(R.string.dynamic_color_title), - description = stringResource(R.string.dynamic_color_summary), - checked = state.useDynamicColor, - onCheckedChange = { enabled -> - viewModel.dispatch(SettingsUiAction.SetDynamicColor(enabled)) - } - ) - } - - item( - visible = !state.useDynamicColor, - topPadding = 1.dp, + expandableItem( + expanded = !state.useDynamicColor, + topContent = { + SettingsSwitchWidget( + icon = Icons.TwoTone.ColorLens, + title = stringResource(R.string.dynamic_color_title), + description = stringResource(R.string.dynamic_color_summary), + checked = state.useDynamicColor, + onCheckedChange = { enabled -> + viewModel.dispatch(SettingsUiAction.SetDynamicColor(enabled)) + } + ) + } ) { - // 主题色选择 - ThemeColorSelection(viewModel = viewModel) + item( + visible = !state.useDynamicColor, + topPadding = 1.dp, + ) { + // 主题色选择 + ThemeColorSelection(viewModel = viewModel) + } } item { @@ -571,7 +562,9 @@ private fun AppearanceSettings( ) } - item { + item( + forceFlatBottom = true, + ) { SettingsBaseWidget( icon = Icons.TwoTone.FormatSize, title = stringResource(R.string.app_dpi_title), @@ -588,6 +581,7 @@ private fun AppearanceSettings( item( topPadding = 1.dp, + forceFlatTop = true, ) { shape -> Surface( modifier = Modifier @@ -596,7 +590,6 @@ private fun AppearanceSettings( color = if (themeConfig.isEnableBlurExp) Color.Transparent else MaterialTheme.colorScheme.surfaceBright.copy( alpha = cardConfig.cardAlpha ), - shape = shape ) { Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { DpiSliderControls( @@ -689,6 +682,30 @@ private fun CustomizationSettings( } ) } + + item { + SettingsSwitchWidget( + icon = Icons.TwoTone.Pin, + title = stringResource(R.string.navigation_bar_badge), + description = stringResource(R.string.navigation_bar_badge_summary), + checked = homeUiState.showNavigationBarBadge, + onCheckedChange = { enabled -> + homeViewModel.dispatch(HomeUiAction.SetNavigationBarBadge(enabled)) + } + ) + } + + item { + SettingsSwitchWidget( + icon = Icons.TwoTone.Badge, + title = stringResource(R.string.home_card_icons), + description = stringResource(R.string.home_card_icons_summary), + checked = homeUiState.showHomeCardIcons, + onCheckedChange = { enabled -> + homeViewModel.dispatch(HomeUiAction.SetHomeCardIcons(enabled)) + } + ) + } } } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/crop/BackgroundCropActivity.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/crop/BackgroundCropActivity.kt index a9af7749c..880bdc9ae 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/crop/BackgroundCropActivity.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/crop/BackgroundCropActivity.kt @@ -72,6 +72,7 @@ import androidx.compose.ui.window.PopupPositionProvider import com.resukisu.resukisu.R import com.resukisu.resukisu.ui.component.KeyPointSlider import com.resukisu.resukisu.ui.theme.KernelSUTheme +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.yalantis.ucrop.UCrop import com.yalantis.ucrop.callback.BitmapCropCallback import com.yalantis.ucrop.view.OverlayView @@ -252,6 +253,7 @@ private fun BackgroundCropScreen( } Scaffold( + contentWindowInsets = adaptiveScaffoldWindowInsets(), topBar = { LargeFlexibleTopAppBar( title = { Text(stringResource(R.string.background_crop_title)) }, diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/util/CompositionProvider.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/util/CompositionProvider.kt index bdc5a7878..e77dea6a1 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/util/CompositionProvider.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/util/CompositionProvider.kt @@ -18,6 +18,7 @@ val LocalBlurState = compositionLocalOf { } val LocalPagerState = compositionLocalOf { error("No pager state") } +val LocalPortraitState = compositionLocalOf { error("No portrait state") } val LocalPagerPage = staticCompositionLocalOf { null } val LocalHandlePageChange = compositionLocalOf<(Int) -> Unit> { error("No handle page change") } val LocalSelectedPage = compositionLocalOf { error("No selected page") } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/util/ScaffoldWindowInsets.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/util/ScaffoldWindowInsets.kt new file mode 100644 index 000000000..5a3bbbf7f --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/util/ScaffoldWindowInsets.kt @@ -0,0 +1,16 @@ +package com.resukisu.resukisu.ui.util + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.runtime.Composable + +@Composable +fun adaptiveScaffoldWindowInsets(includeBottom: Boolean = true): WindowInsets { + return if (includeBottom) { + WindowInsets.safeDrawing + } else { + WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + } +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/HomeViewModel.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/HomeViewModel.kt index 6a277028f..a6ff83de1 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/HomeViewModel.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/HomeViewModel.kt @@ -33,6 +33,8 @@ sealed interface HomeUiAction { data object AwaitInitialData : HomeUiAction data class Refresh(val showIndicator: Boolean = true) : HomeUiAction data class SetSimpleMode(val enabled: Boolean) : HomeUiAction + data class SetNavigationBarBadge(val enabled: Boolean) : HomeUiAction + data class SetHomeCardIcons(val enabled: Boolean) : HomeUiAction data class Reboot(val reason: String) : HomeUiAction } @@ -89,7 +91,13 @@ class HomeViewModel( it.copy(systemStatus = kernelStatus, isCoreDataLoaded = true) } - val basic = async { getBasicInfo(kernelStatus.managerUAPIVersion) } + val includeSelinuxStatus = !state.value.isInitialDataLoaded + val basic = async { + getBasicInfo( + managerUapiVersion = kernelStatus.managerUAPIVersion, + includeSelinuxStatus = includeSelinuxStatus, + ) + } val module = async { getModuleOverview() } val superusers = async { getSuperuserCount() } val managers = async { getManagerRuntimeInfo() } @@ -106,7 +114,12 @@ class HomeViewModel( androidVersion = basicInfo.androidVersion, deviceModel = basicInfo.deviceModel, managerVersion = basicInfo.managerVersion, - selinuxStatus = basicInfo.selinuxStatus, + // SELinux status is intentionally kept from the initial load. A + // refresh can briefly fail to read the sysfs node and report a + // false "Disabled" state. + selinuxStatus = current.systemInfo.selinuxStatus.ifEmpty { + basicInfo.selinuxStatus + }, susfsEnabled = susfsInfo.enabled, susfsVersionSupported = susfsInfo.enabled, susfsVersion = susfsInfo.version, @@ -138,11 +151,23 @@ class HomeViewModel( fun handleSimpleModeChange(enabled: Boolean) = updatePreference(PREF_SIMPLE_MODE, enabled) { it.copy(isSimpleMode = enabled) } + fun handleNavigationBarBadgeChange(enabled: Boolean) = + updatePreference(PREF_SHOW_NAVIGATION_BAR_BADGE, enabled) { + it.copy(showNavigationBarBadge = enabled) + } + + fun handleHomeCardIconsChange(enabled: Boolean) = + updatePreference(PREF_SHOW_HOME_CARD_ICONS, enabled) { + it.copy(showHomeCardIcons = enabled) + } + fun dispatch(action: HomeUiAction) { when (action) { HomeUiAction.AwaitInitialData -> viewModelScope.launch { awaitInitialData() } is HomeUiAction.Refresh -> refreshData(action.showIndicator) is HomeUiAction.SetSimpleMode -> handleSimpleModeChange(action.enabled) + is HomeUiAction.SetNavigationBarBadge -> handleNavigationBarBadgeChange(action.enabled) + is HomeUiAction.SetHomeCardIcons -> handleHomeCardIconsChange(action.enabled) is HomeUiAction.Reboot -> viewModelScope.launch { reboot(action.reason).onFailure { mutableEvents.tryEmit(HomeUiEvent.Error(it.message.orEmpty())) @@ -189,6 +214,11 @@ class HomeViewModel( homeStateRepository.update { it.copy( isSimpleMode = getBooleanPreference(PREF_SIMPLE_MODE), + showNavigationBarBadge = getBooleanPreference( + PREF_SHOW_NAVIGATION_BAR_BADGE, + true, + ), + showHomeCardIcons = getBooleanPreference(PREF_SHOW_HOME_CARD_ICONS), ) } } @@ -208,5 +238,7 @@ class HomeViewModel( const val PREF_CHECK_UPDATE = "check_update" const val PREF_CHECK_BETA_UPDATE = "check_beta_update" const val PREF_SIMPLE_MODE = "is_simple_mode" + const val PREF_SHOW_NAVIGATION_BAR_BADGE = "show_navigation_bar_badge" + const val PREF_SHOW_HOME_CARD_ICONS = "show_home_card_icons" } } diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index b932ff0e7..ce784e46c 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -234,6 +234,10 @@ 使用内置等宽字体显示日志,解决某些 ROM 上字体对齐异常的问题 简洁模式 开启后将隐藏不必要的卡片 + 导航栏角标 + 在导航栏展示超级用户 / 模块数量 + 主页卡片显示图标 + 主页信息卡片添加图标展示 主题模式 跟随系统 浅色 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index bd163181c..72b6d6647 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -237,6 +237,10 @@ Use built-in JetBrains Mono font for log display to avoid system monospace issues Simplicity mode Hides unnecessary cards when turned on + Navigation bar badges + Show superuser / module counts in the navigation bar + Show icons on home cards + Add icons to the home information cards Theme Follow system Light diff --git a/manager/gradle/libs.versions.toml b/manager/gradle/libs.versions.toml index 4af8c16a3..29543a87e 100644 --- a/manager/gradle/libs.versions.toml +++ b/manager/gradle/libs.versions.toml @@ -1,37 +1,35 @@ [versions] accompanist-drawablepainter = "0.37.3" -agp = "9.3.0" +agp = "9.3.2" gson = "2.14.0" -kotlin = "2.4.0" -materialKolor = "4.1.1" +kotlin = "2.4.10" +materialKolor = "5.0.0" monetCompat = "0.4.1" materialComponents = "1.14.0" capsule = "2.1.3" -compose-bom = "2026.05.01" +compose-bom = "2026.08.00" lifecycle = "2.10.0" -navigation3 = "1.2.0-alpha04" -navigationevent = "1.1.1" activity-compose = "1.13.0" core-splashscreen = "1.2.0" kotlinx-coroutines = "1.11.0" coil-compose = "2.7.0" ucrop = "2.2.11" markdown = "4.6.2" -webkit = "1.16.0" +webkit = "1.17.0" appiconloader = "1.5.0" hiddenapibypass = "6.1" parcelablelist = "2.0.1" libsu = "6.0.0" apksign = "1.4" -compose-material3 = "1.5.0-alpha21" +compose-material3 = "1.5.0-alpha27" compose-ui = "1.11.2" documentfile = "1.1.0" ndk = "29.0.14206865" foundation = "1.11.2" -aboutLibraries = "14.2.1" -miuix = "0.9.2" +aboutLibraries = "15.1.1" +miuix = "0.9.4-rc01" datastore = "1.2.1" -benchmark = "1.5.0-alpha07" +benchmark = "1.5.0-rc02" profileinstaller = "1.4.1" koin-bom = "4.2.2" @@ -74,7 +72,6 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } -androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycle" } androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } @@ -95,10 +92,8 @@ me-zhanghai-android-appiconloader = { group = "me.zhanghai.android.appiconloader me-zhanghai-android-appiconloader-coil = { group = "me.zhanghai.android.appiconloader", name = "appiconloader-coil", version.ref = "appiconloader" } org-lsposed-hiddenapibypass = { group = "org.lsposed.hiddenapibypass", name = "hiddenapibypass", version.ref = "hiddenapibypass" } -androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3" } -miuix-navigation = { group = "top.yukonga.miuix.kmp", name = "miuix-navigation3-ui-android", version.ref = "miuix" } +miuix-nav = { group = "top.yukonga.miuix.kmp", name = "miuix-nav", version.ref = "miuix" } miuix-blur = { group = "top.yukonga.miuix.kmp", name = "miuix-blur-android", version.ref = "miuix" } -androidx-navigationevent = { module = "androidx.navigationevent:navigationevent", version.ref = "navigationevent" } markdown = { group = "io.noties.markwon", name = "core", version.ref = "markdown" } @@ -113,4 +108,4 @@ androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profi # Core module (required for accessing library data) aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutLibraries" } # Compose UI modules (choose one or both) -aboutlibraries-compose-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutLibraries" } # Material 3 UI +aboutlibraries-compose-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutLibraries" } diff --git a/manager/gradle/wrapper/gradle-wrapper.jar b/manager/gradle/wrapper/gradle-wrapper.jar index b1b8ef56b44f16b14dc800fa8103a6d89abb526f..eddabd2eef8d94a5437d6168ff9c87a78ff725b3 100644 GIT binary patch delta 39079 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 F{Rc_uT_ykk delta 40101 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 diff --git a/manager/gradle/wrapper/gradle-wrapper.properties b/manager/gradle/wrapper/gradle-wrapper.properties index 52ad5e715..c56b367ba 100644 --- a/manager/gradle/wrapper/gradle-wrapper.properties +++ b/manager/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/manager/gradlew b/manager/gradlew index b9bb139f7..249efbb03 100755 --- a/manager/gradlew +++ b/manager/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: diff --git a/manager/gradlew.bat b/manager/gradlew.bat index 24c62d56f..a51ec4f58 100644 --- a/manager/gradlew.bat +++ b/manager/gradlew.bat @@ -19,7 +19,7 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## @@ -72,7 +72,7 @@ echo location of your Java installation. 1>&2 -@rem Execute Gradle +@rem Execute gradlew @rem endlocal doesn't take effect until after the line is parsed and variables are expanded @rem which allows us to clear the local environment before executing the java command endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel From fa1da13f890a19335d3f8f5c62bb5bde466fc384 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Sat, 12 Sep 2026 02:52:28 +0200 Subject: [PATCH 17/34] manager: update translations from Weblate (#402) Translations updated in [Hosted Weblate](https://hosted.weblate.org) for [ReSukiSU/ReSukiSU](https://hosted.weblate.org/projects/resukisu/resukisu/). Translation status: ![Weblate translation status](https://hosted.weblate.org/widget/resukisu/resukisu/matrix-auto.svg) Co-authored-by: kuklux --- manager/app/src/main/res/values-uk/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manager/app/src/main/res/values-uk/strings.xml b/manager/app/src/main/res/values-uk/strings.xml index 8158c1737..b697d9ef6 100644 --- a/manager/app/src/main/res/values-uk/strings.xml +++ b/manager/app/src/main/res/values-uk/strings.xml @@ -513,8 +513,8 @@ Очистити динамічний менеджер Ви впевнені, що хочете очистити налаштування динамічного менеджера? Керування менеджерами - Інформація про версію - Інформація про стан + Інформація + Стан Суперкористувач: %1$d, Модулі: %2$d Версія драйвера ядра Жодного збігу не знайдено From 3380d41f2043644d0ef6c0e0e91be6b229024d00 Mon Sep 17 00:00:00 2001 From: Tools-cx-app Date: Sat, 12 Sep 2026 19:40:37 +0800 Subject: [PATCH 18/34] ksud: sync upstream commit https://gitlab.com/simonpunk/susfs4ksu/-/commit/d4ea3dd764b3a9a5e0a93de53025b8952b8fe923 --- userspace/ksud/src/android/susfs/cli.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/userspace/ksud/src/android/susfs/cli.rs b/userspace/ksud/src/android/susfs/cli.rs index d0748e904..2ad039c7e 100644 --- a/userspace/ksud/src/android/susfs/cli.rs +++ b/userspace/ksud/src/android/susfs/cli.rs @@ -78,7 +78,7 @@ pub enum SuSFSSubCommands { /// This command must be completed with later after the added path is bind mounted or overlayed. /// /// * Important Notes * - /// - Only effective for umounted process with uid >= 10000. + /// - Effective for all processes with uid >= 10000 #[command(name = "add_sus_kstat")] AddSusKstat { /// Path of file or directory @@ -90,7 +90,7 @@ pub enum SuSFSSubCommands { /// This updates the target ino, but size and blocks are remained the same as current stat. /// /// * Important Notes * - /// - Only effective for umounted process with uid >= 10000. + /// - Effective for all processes with uid >= 10000 #[command(name = "update_sus_kstat")] UpdateSusKstat { /// Path of file or directory @@ -102,7 +102,7 @@ pub enum SuSFSSubCommands { /// This updates the target ino only, other stat members are remained the same as the original stat. /// /// * Important Notes * - /// - Only effective for umounted process with uid >= 10000. + /// - Effective for all processes with uid >= 10000 #[command(name = "update_sus_kstat_full_clone")] UpdateSusKstatFullClone { /// Path of file or directory @@ -112,7 +112,7 @@ pub enum SuSFSSubCommands { /// Spoof the kstat of a file or directory by static fields. /// /// * Important Notes * - /// - Only effective for umounted process with uid >= 10000. + /// - Effective for all processes with uid >= 10000. #[command(name = "add_sus_kstat_statically")] AddSusKstatStatically { /// Path of file or directory From b1954220ce1a1754a1e36677c7c18fbf47d525bb Mon Sep 17 00:00:00 2001 From: AlexLiuDev233 Date: Sun, 13 Sep 2026 17:44:05 +0800 Subject: [PATCH 19/34] manager: workaround for ListItem crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Google's Material3 Expressive ListItem has bug supportingContent will cause RectList broken and cause application crash. We let supportingContent in headlineContent for workaround this issue Signed-off-by: AlexLiuDev233 Signed-off-by: AlexLiuDev233 --- .../component/settings/SettingsBaseWidget.kt | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SettingsBaseWidget.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SettingsBaseWidget.kt index 62beab3db..43a303a35 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SettingsBaseWidget.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SettingsBaseWidget.kt @@ -29,11 +29,13 @@ import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ListItemShapes +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocal +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.compositionLocalOf @@ -365,6 +367,26 @@ fun SettingsBaseWidget( } } + // M3E ListItem has bug, supportingContent will cause RectList broken + // and cause application crash. + + // We use headlineContent + Column + supportingContent for workaround, + // Hope Google fix this problem in their new version.... + val expressiveContent: @Composable () -> Unit = { + Column { + headline() + CompositionLocalProvider( + LocalContentColor provides colors.supportingContentColor( + enabled = enabled, + selected = selected, + dragged = false, + ), + ) { + supportingContent() + } + } + } + if (onClick != null || onLongClick != null) { var touchPoint by remember { mutableStateOf(Offset.Zero) } @@ -398,10 +420,9 @@ fun SettingsBaseWidget( shapes = listItemShapes, verticalAlignment = Alignment.CenterVertically, leadingContent = finalLeadingContent, - supportingContent = supportingContent, trailingContent = trailing, interactionSource = interactionSource, - content = headline + content = expressiveContent ) } else { /* @@ -426,10 +447,9 @@ fun SettingsBaseWidget( shapes = shapes, colors = colors, leadingContent = finalLeadingContent, - supportingContent = supportingContent, trailingContent = trailing, contentPadding = ListItemDefaults.ContentPadding, - content = headline, + content = expressiveContent, ) } } From 601f6d2af4801492339f74f622e1a4ae3a445250 Mon Sep 17 00:00:00 2001 From: AlexLiuDev233 Date: Sun, 13 Sep 2026 18:28:26 +0800 Subject: [PATCH 20/34] manager: refine home page state get reuse superuser / modules repository for superuser count/ module count TODO: refine every page, make only 1 data in application data get migrate to repository, state repository subscribe other repository for implement that Signed-off-by: AlexLiuDev233 --- .../resukisu/resukisu/data/shell/KsuCli.kt | 5 +- .../data/system/HomeRuntimeRepository.kt | 19 ------- .../com/resukisu/resukisu/di/AppModules.kt | 4 -- .../resukisu/domain/model/HomeRuntime.kt | 2 +- .../domain/usecase/HomeRuntimeUseCases.kt | 8 --- .../com/resukisu/resukisu/ui/MainActivity.kt | 2 +- .../resukisu/ui/screen/main/MainScreen.kt | 2 +- .../resukisu/ui/screen/main/ModulePage.kt | 2 +- .../resukisu/ui/screen/main/SettingsPage.kt | 2 +- .../resukisu/ui/viewmodel/HomeViewModel.kt | 53 +++++++++++-------- 10 files changed, 38 insertions(+), 61 deletions(-) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/data/shell/KsuCli.kt b/manager/app/src/main/java/com/resukisu/resukisu/data/shell/KsuCli.kt index 2a133dfd7..2aa64e390 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/data/shell/KsuCli.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/data/shell/KsuCli.kt @@ -620,15 +620,12 @@ class KsuCliRepository(context: Context) { fun getZygiskImplement(): String { val zygiskModuleIds = listOf( "zygisksu", - "rezygisk", - "shirokozygisk" + "rezygisk" ) for (moduleId in zygiskModuleIds) { - // 忽略禁用/即将删除 if (SuFile.open("/data/adb/modules/$moduleId/disable").isFile || SuFile.open("/data/adb/modules/$moduleId/remove").isFile) continue - // 读取prop val propFile = SuFile.open("/data/adb/modules/$moduleId/module.prop") if (!propFile.isFile) continue diff --git a/manager/app/src/main/java/com/resukisu/resukisu/data/system/HomeRuntimeRepository.kt b/manager/app/src/main/java/com/resukisu/resukisu/data/system/HomeRuntimeRepository.kt index 005bc6719..c9e8967c6 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/data/system/HomeRuntimeRepository.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/data/system/HomeRuntimeRepository.kt @@ -5,15 +5,12 @@ import android.app.Application import android.os.Build import android.system.Os import com.resukisu.resukisu.BuildConfig -import com.resukisu.resukisu.data.shell.KsuCliRepository import com.resukisu.resukisu.domain.model.HomeBasicInfo -import com.resukisu.resukisu.domain.model.HomeModuleOverview import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class HomeRuntimeRepository( private val application: Application, - private val ksuCliRepository: KsuCliRepository, ) { suspend fun getBasicInfo( managerUapiVersion: Int, @@ -39,22 +36,6 @@ class HomeRuntimeRepository( ) } - suspend fun getModuleOverview(): HomeModuleOverview = withContext(Dispatchers.IO) { - HomeModuleOverview( - count = runCatching { ksuCliRepository.getModuleCount() }.getOrDefault(0), - zygiskImplementation = runCatching { - ksuCliRepository.getZygiskImplement() - }.getOrDefault("None"), - metaModuleImplementation = runCatching { - ksuCliRepository.getMetaModuleImplement() - }.getOrDefault("None"), - ) - } - - suspend fun getSuperuserCount(): Int = withContext(Dispatchers.IO) { - runCatching { ksuCliRepository.getSuperuserCount() }.getOrDefault(0) - } - @SuppressLint("PrivateApi") private fun getDeviceModel(): String = runCatching { val systemProperties = Class.forName("android.os.SystemProperties") diff --git a/manager/app/src/main/java/com/resukisu/resukisu/di/AppModules.kt b/manager/app/src/main/java/com/resukisu/resukisu/di/AppModules.kt index f4783ca01..aaafed1df 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/di/AppModules.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/di/AppModules.kt @@ -71,8 +71,6 @@ import com.resukisu.resukisu.domain.usecase.GetBooleanPreferenceUseCase import com.resukisu.resukisu.domain.usecase.GetCatalogModuleUseCase import com.resukisu.resukisu.domain.usecase.GetDefaultUmountModulesUseCase import com.resukisu.resukisu.domain.usecase.GetHomeBasicInfoUseCase -import com.resukisu.resukisu.domain.usecase.GetHomeModuleOverviewUseCase -import com.resukisu.resukisu.domain.usecase.GetHomeSuperuserCountUseCase import com.resukisu.resukisu.domain.usecase.GetInstallEnvironmentUseCase import com.resukisu.resukisu.domain.usecase.GetKernelFeatureSettingsUseCase import com.resukisu.resukisu.domain.usecase.GetKernelStatusUseCase @@ -295,8 +293,6 @@ val repositoryModule = module { val useCaseModule = module { factoryOf(::InitializeApplicationUseCase) factoryOf(::GetHomeBasicInfoUseCase) - factoryOf(::GetHomeModuleOverviewUseCase) - factoryOf(::GetHomeSuperuserCountUseCase) factoryOf(::IsNetworkAvailableUseCase) factoryOf(::LoadSettingsPlatformUseCase) factoryOf(::UpdateAppearanceUseCase) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/HomeRuntime.kt b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/HomeRuntime.kt index b35a8641a..0a892a177 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/HomeRuntime.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/HomeRuntime.kt @@ -49,4 +49,4 @@ data class HomeDashboardState( val isCoreDataLoaded: Boolean = false, val isExtendedDataLoaded: Boolean = false, val isRefreshing: Boolean = false, -) +) \ No newline at end of file diff --git a/manager/app/src/main/java/com/resukisu/resukisu/domain/usecase/HomeRuntimeUseCases.kt b/manager/app/src/main/java/com/resukisu/resukisu/domain/usecase/HomeRuntimeUseCases.kt index e1170e73f..1372c53ad 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/domain/usecase/HomeRuntimeUseCases.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/domain/usecase/HomeRuntimeUseCases.kt @@ -10,14 +10,6 @@ class GetHomeBasicInfoUseCase(private val repository: HomeRuntimeRepository) { ) = repository.getBasicInfo(managerUapiVersion, includeSelinuxStatus) } -class GetHomeModuleOverviewUseCase(private val repository: HomeRuntimeRepository) { - suspend operator fun invoke() = repository.getModuleOverview() -} - -class GetHomeSuperuserCountUseCase(private val repository: HomeRuntimeRepository) { - suspend operator fun invoke() = repository.getSuperuserCount() -} - class IsNetworkAvailableUseCase(private val repository: NetworkStatusRepository) { operator fun invoke() = repository.isAvailable() } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/MainActivity.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/MainActivity.kt index a139c8cf0..b2429eb1f 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/MainActivity.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/MainActivity.kt @@ -80,7 +80,7 @@ class MainActivity : ComponentActivity() { splashScreen.setKeepOnScreenCondition { shouldKeepStartupSplash( startupState = startupState.value, - homeInitialDataLoaded = homeViewModel.state.value.isInitialDataLoaded, + homeInitialDataLoaded = homeViewModel.homeStateRepository.state.value.isInitialDataLoaded, ) } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/MainScreen.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/MainScreen.kt index a4df10b11..9cb8de059 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/MainScreen.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/MainScreen.kt @@ -53,7 +53,7 @@ import org.koin.compose.viewmodel.koinViewModel fun MainScreen() { val themeConfig: ThemeConfig = koinInject() val homeViewModel = koinViewModel() - val homeState by homeViewModel.state.collectAsStateWithLifecycle() + val homeState by homeViewModel.uiState.collectAsStateWithLifecycle() val pages = remember(homeState.systemStatus.isFullFeatured) { BottomBarDestination.getPages(homeState.systemStatus.isFullFeatured) } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/ModulePage.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/ModulePage.kt index 01d2afeb8..65732c94a 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/ModulePage.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/ModulePage.kt @@ -194,7 +194,7 @@ fun ModulePage(bottomPadding: Dp) { val context = LocalContext.current val viewModel = koinViewModel() val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val homeState by koinViewModel().state.collectAsStateWithLifecycle() + val homeState by koinViewModel().uiState.collectAsStateWithLifecycle() val snackBarHost = LocalSnackbarHost.current val scope = rememberCoroutineScope() var lastClickTime by remember { mutableStateOf(0L) } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt index 926479b3f..488c147b6 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt @@ -125,7 +125,7 @@ fun SettingsPage(bottomPadding: Dp) { val homeViewModel = koinViewModel() val generateBugreport = koinInject() val uiState by settingsViewModel.uiState.collectAsStateWithLifecycle() - val homeState by homeViewModel.state.collectAsStateWithLifecycle() + val homeState by homeViewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(Unit) { settingsViewModel.dispatch(SettingsUiAction.LoadFeatureSettings) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/HomeViewModel.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/HomeViewModel.kt index a6ff83de1..c316199c6 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/HomeViewModel.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/HomeViewModel.kt @@ -2,6 +2,9 @@ package com.resukisu.resukisu.ui.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.resukisu.resukisu.data.module.ModuleRepository +import com.resukisu.resukisu.data.packageinfo.SuperUserRepository +import com.resukisu.resukisu.data.shell.KsuCliRepository import com.resukisu.resukisu.data.system.HomeStateRepository import com.resukisu.resukisu.domain.model.HomeDashboardState import com.resukisu.resukisu.domain.model.HomeSystemInfo @@ -9,8 +12,6 @@ import com.resukisu.resukisu.domain.model.ManagerUpdateChannel import com.resukisu.resukisu.domain.usecase.CheckManagerUpdateUseCase import com.resukisu.resukisu.domain.usecase.GetBooleanPreferenceUseCase import com.resukisu.resukisu.domain.usecase.GetHomeBasicInfoUseCase -import com.resukisu.resukisu.domain.usecase.GetHomeModuleOverviewUseCase -import com.resukisu.resukisu.domain.usecase.GetHomeSuperuserCountUseCase import com.resukisu.resukisu.domain.usecase.GetKernelStatusUseCase import com.resukisu.resukisu.domain.usecase.GetManagerRuntimeInfoUseCase import com.resukisu.resukisu.domain.usecase.GetSuSFSStatusUseCase @@ -22,7 +23,10 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -43,21 +47,39 @@ sealed interface HomeUiEvent { } class HomeViewModel( - private val homeStateRepository: HomeStateRepository, + val homeStateRepository: HomeStateRepository, + superUserRepository: SuperUserRepository, + moduleRepository: ModuleRepository, + private val ksuCliRepository: KsuCliRepository, private val checkManagerUpdate: CheckManagerUpdateUseCase, private val getKernelStatus: GetKernelStatusUseCase, private val getManagerRuntimeInfo: GetManagerRuntimeInfoUseCase, private val getSuSFSStatus: GetSuSFSStatusUseCase, private val getBasicInfo: GetHomeBasicInfoUseCase, - private val getModuleOverview: GetHomeModuleOverviewUseCase, - private val getSuperuserCount: GetHomeSuperuserCountUseCase, private val isNetworkAvailable: IsNetworkAvailableUseCase, private val getBooleanPreference: GetBooleanPreferenceUseCase, private val setBooleanPreference: SetBooleanPreferenceUseCase, private val reboot: RebootUseCase, ) : ViewModel() { - val state = homeStateRepository.state - val uiState = state + val uiState = combine( + homeStateRepository.state, + superUserRepository.state, + moduleRepository.installedModules, + ) { homeState, superUserState, moduleState -> + homeState.copy( + systemInfo = homeState.systemInfo.copy( + moduleCount = moduleState.modules.size, + superuserCount = superUserState.groups.filter { it.allowSu }.size, + zygiskImplement = ksuCliRepository.getZygiskImplement(), + metaModuleImplement = ksuCliRepository.getMetaModuleImplement(), + ) + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = HomeUiState() + ) + private val mutableEvents = MutableSharedFlow(extraBufferCapacity = 1) val events: SharedFlow = mutableEvents.asSharedFlow() @@ -77,7 +99,7 @@ class HomeViewModel( fun refreshData(refreshUI: Boolean = false): Job { if (!refreshUI) { refreshJob?.takeIf(Job::isActive)?.let { return it } - if (state.value.isInitialDataLoaded) return completedJob() + if (uiState.value.isInitialDataLoaded) return completedJob() } refreshManagerUpdates(force = refreshUI) return viewModelScope.launch { @@ -86,25 +108,21 @@ class HomeViewModel( try { applyUserSettings() val kernelStatus = runCatching { getKernelStatus() } - .getOrElse { state.value.systemStatus } + .getOrElse { uiState.value.systemStatus } homeStateRepository.update { it.copy(systemStatus = kernelStatus, isCoreDataLoaded = true) } - val includeSelinuxStatus = !state.value.isInitialDataLoaded + val includeSelinuxStatus = !uiState.value.isInitialDataLoaded val basic = async { getBasicInfo( managerUapiVersion = kernelStatus.managerUAPIVersion, includeSelinuxStatus = includeSelinuxStatus, ) } - val module = async { getModuleOverview() } - val superusers = async { getSuperuserCount() } val managers = async { getManagerRuntimeInfo() } val susfs = async { getSuSFSStatus() } val basicInfo = basic.await() - val moduleInfo = module.await() - val superuserCount = superusers.await() val managerInfo = managers.await() val susfsInfo = susfs.await() homeStateRepository.update { current -> @@ -114,9 +132,6 @@ class HomeViewModel( androidVersion = basicInfo.androidVersion, deviceModel = basicInfo.deviceModel, managerVersion = basicInfo.managerVersion, - // SELinux status is intentionally kept from the initial load. A - // refresh can briefly fail to read the sysfs node and report a - // false "Disabled" state. selinuxStatus = current.systemInfo.selinuxStatus.ifEmpty { basicInfo.selinuxStatus }, @@ -124,12 +139,8 @@ class HomeViewModel( susfsVersionSupported = susfsInfo.enabled, susfsVersion = susfsInfo.version, susfsFeatures = susfsInfo.enabledFeatures, - superuserCount = superuserCount, - moduleCount = moduleInfo.count, managersList = managerInfo, isDynamicSignEnabled = managerInfo.dynamicSignatureEnabled, - zygiskImplement = moduleInfo.zygiskImplementation, - metaModuleImplement = moduleInfo.metaModuleImplementation, seccompStatus = basicInfo.seccompStatus, ), isInitialDataLoaded = true, From c04159fcbdfdb71b0c3765ecaaf23c4e0a498c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?YC=E9=85=B1luyancib?= Date: Sun, 13 Sep 2026 21:25:59 +0800 Subject: [PATCH 21/34] manager: improve superuser sort (#405) --- .../data/packageinfo/SuperUserRepository.kt | 2 + .../domain/model/InstalledAppGroup.kt | 1 + .../resukisu/ui/screen/main/SuperUserPage.kt | 25 ++++++- .../ui/viewmodel/SuperUserViewModel.kt | 70 ++++++++++++++----- .../app/src/main/res/values-ar/strings.xml | 6 -- .../app/src/main/res/values-fr/strings.xml | 6 -- .../app/src/main/res/values-hu/strings.xml | 6 -- .../app/src/main/res/values-in/strings.xml | 6 -- .../app/src/main/res/values-ja/strings.xml | 6 -- .../app/src/main/res/values-ko/strings.xml | 2 - .../app/src/main/res/values-pl/strings.xml | 6 -- .../src/main/res/values-pt-rBR/strings.xml | 6 -- .../app/src/main/res/values-ru/strings.xml | 6 -- .../app/src/main/res/values-tr/strings.xml | 6 -- .../app/src/main/res/values-uk/strings.xml | 6 -- .../app/src/main/res/values-vi/strings.xml | 6 -- .../src/main/res/values-zh-rCN/strings.xml | 11 ++- .../src/main/res/values-zh-rHK/strings.xml | 6 -- .../src/main/res/values-zh-rTW/strings.xml | 6 -- manager/app/src/main/res/values/strings.xml | 11 ++- 20 files changed, 86 insertions(+), 114 deletions(-) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/data/packageinfo/SuperUserRepository.kt b/manager/app/src/main/java/com/resukisu/resukisu/data/packageinfo/SuperUserRepository.kt index 7d9ff71f5..7cb066148 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/data/packageinfo/SuperUserRepository.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/data/packageinfo/SuperUserRepository.kt @@ -76,6 +76,7 @@ class SuperUserRepository( uid = applicationInfo.uid, isSystem = applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0, firstInstallTime = info.firstInstallTime, + lastUpdateTime = info.lastUpdateTime, ) } val normalGroups = apps.groupBy(InstalledApp::uid).map { (uid, uidApps) -> @@ -229,6 +230,7 @@ class SuperUserRepository( uid = info?.uid ?: fallbackUid, isSystem = info?.flags?.and(ApplicationInfo.FLAG_SYSTEM) != 0, firstInstallTime = firstInstallTime, + lastUpdateTime = lastUpdateTime, ) } } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/InstalledAppGroup.kt b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/InstalledAppGroup.kt index 5826a2290..d1c866a1b 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/InstalledAppGroup.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/InstalledAppGroup.kt @@ -9,6 +9,7 @@ data class InstalledApp( val uid: Int, val isSystem: Boolean = false, val firstInstallTime: Long = 0L, + val lastUpdateTime: Long = 0L, val profileKey: String = packageName, val special: Boolean = false, ) { diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SuperUserPage.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SuperUserPage.kt index fedf331c3..599cbca40 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SuperUserPage.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SuperUserPage.kt @@ -92,7 +92,8 @@ import java.util.Locale private data class SuperUserMenuItem( val checked: Boolean = false, val titleRes: Int, - val onClick: () -> Unit + val onClick: () -> Unit, + val closeOnClick: Boolean = true, ) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @@ -357,7 +358,8 @@ private fun SuperUserContent( contentType = { _, appGroup -> "${appGroup.uid}-${appGroup.profileKey}" }, ) { _, appGroup -> AppGroupItem( - appGroup = appGroup + appGroup = appGroup, + isManager = appGroup.uid in uiState.managerUids, ) { navigator.push(Route.AppProfile(appGroup.uid, appGroup.profileKey)) } @@ -382,13 +384,23 @@ private fun SuperUserDropdown( ) { val menuItems = remember( uiState.showSystemApps, + uiState.reverseOrder, onBackupAllowlist, onRestoreAllowlist, ) { listOf( + SuperUserMenuItem( + checked = uiState.reverseOrder, + titleRes = R.string.reverse_order, + closeOnClick = false, + onClick = { + viewModel.dispatch(SuperUserUiAction.SetReverseOrder(!uiState.reverseOrder)) + } + ), SuperUserMenuItem( checked = uiState.showSystemApps, titleRes = R.string.show_system_apps, + closeOnClick = false, onClick = { viewModel.dispatch(SuperUserUiAction.SetShowSystemApps(!uiState.showSystemApps)) } @@ -435,7 +447,7 @@ private fun SuperUserDropdown( SelectableDropdownMenuItem( selected = menuItem.checked, onClick = { - onDismissRequest() + if (menuItem.closeOnClick) onDismissRequest() menuItem.onClick() }, text = { Text(stringResource(menuItem.titleRes)) }, @@ -453,6 +465,7 @@ private fun SuperUserDropdown( @Composable private fun AppGroupItem( appGroup: InstalledAppGroup, + isManager: Boolean, onClick: () -> Unit, ) { val mainApp = appGroup.mainApp @@ -494,6 +507,12 @@ private fun AppGroupItem( containerColor = MaterialTheme.colorScheme.primaryContainer ) } + if (isManager) { + LabelText( + label = "MANAGER", + containerColor = MaterialTheme.colorScheme.errorContainer, + ) + } if (appGroup.apps.size > 1) { appGroup.userName?.let { LabelText( diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/SuperUserViewModel.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/SuperUserViewModel.kt index ae5ad7eda..000f05e57 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/SuperUserViewModel.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/SuperUserViewModel.kt @@ -7,6 +7,7 @@ import com.resukisu.resukisu.domain.model.AllowlistOperationResult import com.resukisu.resukisu.domain.model.InstalledAppGroup import com.resukisu.resukisu.domain.usecase.BackupAllowlistUseCase import com.resukisu.resukisu.domain.usecase.GetBooleanPreferenceUseCase +import com.resukisu.resukisu.domain.usecase.GetManagerRuntimeInfoUseCase import com.resukisu.resukisu.domain.usecase.GetStringPreferenceUseCase import com.resukisu.resukisu.domain.usecase.ImportAllowlistUseCase import com.resukisu.resukisu.domain.usecase.ObserveSuperUserStateUseCase @@ -27,16 +28,14 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch enum class SortType(val displayNameRes: Int, val persistKey: String) { - NAME_ASC(R.string.sort_name_asc, "NAME_ASC"), - NAME_DESC(R.string.sort_name_desc, "NAME_DESC"), - INSTALL_TIME_NEW(R.string.sort_install_time_new, "INSTALL_TIME_NEW"), - INSTALL_TIME_OLD(R.string.sort_install_time_old, "INSTALL_TIME_OLD"), - SIZE_DESC(R.string.sort_size_desc, "SIZE_DESC"), - SIZE_ASC(R.string.sort_size_asc, "SIZE_ASC"), + NAME(R.string.sort_name, "NAME"), + INSTALL_TIME(R.string.sort_install_time, "INSTALL_TIME"), + UPDATE_TIME(R.string.sort_update_time, "UPDATE_TIME"), + SIZE(R.string.sort_size, "SIZE"), USAGE_FREQ(R.string.sort_usage_freq, "USAGE_FREQ"); companion object { - fun fromPersistKey(key: String): SortType = entries.find { it.persistKey == key } ?: NAME_ASC + fun fromPersistKey(key: String): SortType = entries.find { it.persistKey == key } ?: NAME } } @@ -44,7 +43,9 @@ data class SuperUserUiState( val appGroupList: List = emptyList(), val search: String = "", val showSystemApps: Boolean = false, - val currentSortType: SortType = SortType.NAME_ASC, + val currentSortType: SortType = SortType.NAME, + val reverseOrder: Boolean = false, + val managerUids: Set = emptySet(), val isRefreshing: Boolean = false, ) @@ -55,6 +56,7 @@ sealed interface SuperUserUiAction { data class Search(val query: String) : SuperUserUiAction data class SetShowSystemApps(val enabled: Boolean) : SuperUserUiAction data class SetSort(val sortType: SortType) : SuperUserUiAction + data class SetReverseOrder(val enabled: Boolean) : SuperUserUiAction data object StatusChanged : SuperUserUiAction } @@ -69,7 +71,8 @@ sealed interface SuperUserUiEvent { private data class SuperUserControls( val search: String = "", val showSystemApps: Boolean = false, - val sortType: SortType = SortType.NAME_ASC, + val sortType: SortType = SortType.NAME, + val reverseOrder: Boolean = false, ) class SuperUserViewModel( @@ -82,32 +85,48 @@ class SuperUserViewModel( private val setBooleanPreference: SetBooleanPreferenceUseCase, private val setStringPreference: SetStringPreferenceUseCase, private val transliterateText: TransliterateTextUseCase, + private val getManagerRuntimeInfo: GetManagerRuntimeInfoUseCase, ) : ViewModel() { private val sourceState = observeSuperUserState() private val controls = MutableStateFlow( SuperUserControls( showSystemApps = getBooleanPreference(KEY_SHOW_SYSTEM_APPS, false), sortType = SortType.fromPersistKey( - getStringPreference(KEY_CURRENT_SORT_TYPE, SortType.NAME_ASC.persistKey) - ?: SortType.NAME_ASC.persistKey + getStringPreference(KEY_CURRENT_SORT_TYPE, SortType.NAME.persistKey) + ?: SortType.NAME.persistKey ), + reverseOrder = getBooleanPreference(KEY_REVERSE_ORDER, false), ) ) private val mutableEvents = MutableSharedFlow(extraBufferCapacity = 1) private var refreshJob: Job? = null val events: SharedFlow = mutableEvents.asSharedFlow() - val state: StateFlow = combine(sourceState, controls) { source, local -> + private val managerUids = MutableStateFlow>(emptySet()) + + init { + viewModelScope.launch { + val info = runCatching { getManagerRuntimeInfo() }.getOrNull() + managerUids.value = info?.managers?.map { it.uid }?.toSet().orEmpty() + } + } + + val state: StateFlow = combine( + sourceState, controls, managerUids, + ) { source, local, uids -> SuperUserUiState( appGroupList = buildAppGroupList( groups = source.groups, search = local.search, showSystemApps = local.showSystemApps, currentSortType = local.sortType, + reverseOrder = local.reverseOrder, ), search = local.search, showSystemApps = local.showSystemApps, currentSortType = local.sortType, + reverseOrder = local.reverseOrder, + managerUids = uids, isRefreshing = source.refreshing, ) }.stateIn(viewModelScope, SharingStarted.Eagerly, SuperUserUiState()) @@ -151,6 +170,11 @@ class SuperUserViewModel( controls.value = controls.value.copy(sortType = action.sortType) } + is SuperUserUiAction.SetReverseOrder -> { + setBooleanPreference(KEY_REVERSE_ORDER, action.enabled) + controls.value = controls.value.copy(reverseOrder = action.enabled) + } + SuperUserUiAction.StatusChanged -> notifySuperuserStatusChanged() } } @@ -177,6 +201,7 @@ class SuperUserViewModel( search: String, showSystemApps: Boolean, currentSortType: SortType, + reverseOrder: Boolean, ): List = groups .filter { group -> group.apps.any { app -> @@ -193,17 +218,23 @@ class SuperUserViewModel( if (priority != 0) { priority } else { - when (currentSortType) { - SortType.NAME_ASC -> first.mainApp.label.compareTo(second.mainApp.label, true) - SortType.NAME_DESC -> second.mainApp.label.compareTo(first.mainApp.label, true) - SortType.INSTALL_TIME_NEW -> - second.mainApp.firstInstallTime.compareTo(first.mainApp.firstInstallTime) + val base = when (currentSortType) { + SortType.NAME -> + first.mainApp.label.compareTo(second.mainApp.label, true) - SortType.INSTALL_TIME_OLD -> + SortType.INSTALL_TIME -> first.mainApp.firstInstallTime.compareTo(second.mainApp.firstInstallTime) - else -> first.mainApp.label.compareTo(second.mainApp.label, true) + SortType.UPDATE_TIME -> + first.mainApp.lastUpdateTime.compareTo(second.mainApp.lastUpdateTime) + + SortType.SIZE -> + first.mainApp.label.compareTo(second.mainApp.label, true) + + SortType.USAGE_FREQ -> + first.mainApp.label.compareTo(second.mainApp.label, true) } + if (reverseOrder) -base else base } } @@ -217,5 +248,6 @@ class SuperUserViewModel( private companion object { const val KEY_SHOW_SYSTEM_APPS = "show_system_apps" const val KEY_CURRENT_SORT_TYPE = "current_sort_type" + const val KEY_REVERSE_ORDER = "reverse_order" } } diff --git a/manager/app/src/main/res/values-ar/strings.xml b/manager/app/src/main/res/values-ar/strings.xml index 181333424..bcedccc8b 100644 --- a/manager/app/src/main/res/values-ar/strings.xml +++ b/manager/app/src/main/res/values-ar/strings.xml @@ -287,12 +287,6 @@ الإضافة %s معطلّة، او بإنتظار الإزالة تعديل ظلمة الخلفية تحتاج لـ Metamodule - ترتيب اسماء تصاعدي - الاسم تنازليًا - وقت التثبيت (جديد) - وقت التثبيت (قديم) - ترتيب تنازلي حسب الحجم - ترتيب تصاعدي حسب الحجم تكرار الاستخدام لا يوجد تطبيق في هذه الفئة مستمر diff --git a/manager/app/src/main/res/values-fr/strings.xml b/manager/app/src/main/res/values-fr/strings.xml index 7b63f9067..ce56ec83d 100644 --- a/manager/app/src/main/res/values-fr/strings.xml +++ b/manager/app/src/main/res/values-fr/strings.xml @@ -307,12 +307,6 @@ Module %s désactivé ou en attente de suppression Métamodule requis Ce module tente de monter le volume /system, montage géré par métamodule. Sans métamodule, ce module risque de ne pas fonctionner - Par nom - Par nom (décroissant) - Par date d\'installation (décroissant) - Par date d\'installation - Par taille (décroissant) - Par taille Par fréquence d\'utilisation Aucune application dans cette catégorie Persistante diff --git a/manager/app/src/main/res/values-hu/strings.xml b/manager/app/src/main/res/values-hu/strings.xml index e6828725d..bd52c13ed 100644 --- a/manager/app/src/main/res/values-hu/strings.xml +++ b/manager/app/src/main/res/values-hu/strings.xml @@ -278,12 +278,6 @@ Kernel telepítés Metamodul szükséges Ez a modul adatot akar csatolni a fájlrendszer /system névterébe, ehhez metamodul szükséges. Másképp a megfelelő működés nem garantált - ABC sorrendben növekvő - ABC sorrendben csökkenő - Telepítés ideje szerint (újabbak előre) - Telepítés ideje szerint (régebbiek előre) - Tárhelyben elfoglalt méretük szerint csökkenő - Tárhelyben elfoglalt méretük szerint növekvő Leggyakrabban használtak Ebben a kategóriában applikáció nem található Keresés naplóban diff --git a/manager/app/src/main/res/values-in/strings.xml b/manager/app/src/main/res/values-in/strings.xml index aa7be8956..d33138bdb 100644 --- a/manager/app/src/main/res/values-in/strings.xml +++ b/manager/app/src/main/res/values-in/strings.xml @@ -193,12 +193,6 @@ Modul yang dipasang %1$d/%2$d %d Gagal memasang modul baru Memasang Kernel - Urutan nama dari A-Z - Urutan nama dari Z-A - Waktu pemasangan (baru) - Waktu pemasangan (lama) - Urutan ukuran dari terkecil - Urutan ukuran dari terbesar Frekuensi penggunaan Tidak ada aplikasi dalam kategori ini Konfigurasi SuSFS diff --git a/manager/app/src/main/res/values-ja/strings.xml b/manager/app/src/main/res/values-ja/strings.xml index 150375e5e..89f0794c3 100644 --- a/manager/app/src/main/res/values-ja/strings.xml +++ b/manager/app/src/main/res/values-ja/strings.xml @@ -200,12 +200,6 @@ カーネルをフラッシュ中 メタモジュールが必要です このモジュールは /system をマウントしようとしますが、メタモジュールがそれを処理します。そうでなければ動作しない可能性があります - 名前の昇順 - 名前の降順 - インストール日時 (新しい) - インストール日時 (古い) - サイズの降順 - サイズの昇順 使用頻度 このカテゴリーにアプリはありません SuSFS の構成 diff --git a/manager/app/src/main/res/values-ko/strings.xml b/manager/app/src/main/res/values-ko/strings.xml index 0b905a04c..18c7f8f13 100644 --- a/manager/app/src/main/res/values-ko/strings.xml +++ b/manager/app/src/main/res/values-ko/strings.xml @@ -210,8 +210,6 @@ 커널 플래싱 메타 모듈 필요 이 모듈은 /system 마운트를 시도하며, 메타 모듈이 이를 처리합니다. 메타 모듈이 없으면 작동하지 않을 수 있습니다. - 이름 오름차순 - 이름 내림차순 이 카테고리에 앱이 없습니다. 영구적 일시적 diff --git a/manager/app/src/main/res/values-pl/strings.xml b/manager/app/src/main/res/values-pl/strings.xml index abb6035b6..bda325f14 100644 --- a/manager/app/src/main/res/values-pl/strings.xml +++ b/manager/app/src/main/res/values-pl/strings.xml @@ -303,12 +303,6 @@ %d nie udało się zainstalować nowego modułu Wymagany meta moduł Ten moduł montuje /system i wymaga meta modułu, aby działać poprawnie - Alfabetycznie (A-Z) - Alfabetycznie (Z-A) - Czas instalacji (Nowe) - Czas instalacji (Stare) - Rozmiar - malejąco - Rozmiar - rosnąco Częstotliwość używania Brak aplikacji w tej kategorii Trwały diff --git a/manager/app/src/main/res/values-pt-rBR/strings.xml b/manager/app/src/main/res/values-pt-rBR/strings.xml index 736f319c0..05f0a399b 100644 --- a/manager/app/src/main/res/values-pt-rBR/strings.xml +++ b/manager/app/src/main/res/values-pt-rBR/strings.xml @@ -310,11 +310,6 @@ Instalando o kernel Módulo Meta necessário Este módulo deseja montar /system, o que é gerenciado pelo metamódulo. Sem ele, o módulo pode não funcionar - Ordem crescente de nomes - Nome em ordem decrescente - Tempo de instalação (Novo) - Tempo de instalação (Antigo) - Ordem decrescente de tamanho Frequência de utilização Não há candidaturas nesta categoria Persistente @@ -534,5 +529,4 @@ Versão: %1$s (%2$d)\nArquitetura: %3$s Confira as atualizações beta Verificação automática de versões beta a partir da ramificação principal - Ordem decrescente de tamanho diff --git a/manager/app/src/main/res/values-ru/strings.xml b/manager/app/src/main/res/values-ru/strings.xml index 34dda9aba..dc6c01422 100644 --- a/manager/app/src/main/res/values-ru/strings.xml +++ b/manager/app/src/main/res/values-ru/strings.xml @@ -284,12 +284,6 @@ Прошить ядро Требуется мета-модуль Этот модуль пытается смонтировать /system, что обрабатывается мета-модулем. Без него модуль может не работать - Название (по возрастанию) - Название (по убыванию) - Время установки (новые) - Время установки (старые) - Размер (по убыванию) - Размер (по возрастанию) Частота использования В этой категории нет приложений Постоянный diff --git a/manager/app/src/main/res/values-tr/strings.xml b/manager/app/src/main/res/values-tr/strings.xml index 67ac2dc4b..db400ef56 100644 --- a/manager/app/src/main/res/values-tr/strings.xml +++ b/manager/app/src/main/res/values-tr/strings.xml @@ -261,12 +261,6 @@ Çekirdek Yükleniyor Meta modül gerektirir Bu modül, meta modül tarafından yönetilen /system bölümünü bağlamak istiyor. Bu modül meta modül olmadan çalışmayabilir - İsme göre artan sırada - İsme göre azalan sırada - Kurulum zamanı (yeni) - Kurulum zamanı (eski) - Boyuta göre azalan sırada - Boyuta göre artan sırada Kullanım sıklığına göre Bu kategoride uygulama yok Kalıcı diff --git a/manager/app/src/main/res/values-uk/strings.xml b/manager/app/src/main/res/values-uk/strings.xml index b697d9ef6..5fa4ed8d3 100644 --- a/manager/app/src/main/res/values-uk/strings.xml +++ b/manager/app/src/main/res/values-uk/strings.xml @@ -303,12 +303,6 @@ Прошивка ядра Потрібен метамодуль Цей модуль хоче монтувати /system; цим займатиметься метамодуль. Без нього модуль може не працювати - Назва за зростанням - Назва за спаданням - Час встановлення (нові) - Час встановлення (старі) - Розмір за спаданням - Розмір за зростанням Частота використання Немає додатків у цій категорії Постійний diff --git a/manager/app/src/main/res/values-vi/strings.xml b/manager/app/src/main/res/values-vi/strings.xml index 0e462f8b6..5c187e5ed 100644 --- a/manager/app/src/main/res/values-vi/strings.xml +++ b/manager/app/src/main/res/values-vi/strings.xml @@ -204,12 +204,6 @@ Cài đặt module %d thất bại Yêu cầu Meta-module Module này muốn mount /system, Meta-module sẽ xử lý việc đó. Nếu không, nó có thể không hoạt động - Tên (Tăng dần) - Tên (Giảm dần) - Thời gian cài đặt (Mới) - Thời gian cài đặt (Cũ) - Kích thước (Giảm dần) - Kích thước (Tăng dần) Tần suất sử dụng Không có ứng dụng nào trong danh mục này Cấu hình SuSFS diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index ce784e46c..a6eda5888 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -314,13 +314,12 @@ 内核刷写 需要元模块 这个模块需要挂载一些文件。需要安装元模块,使它正常工作 - 名称升序 - 名称降序 - 安装时间(新) - 安装时间(旧) - 大小降序 - 大小升序 + 名称 + 安装时间 + 更新时间 + 大小 使用频率 + 倒序 此分类中没有应用 持久 临时 diff --git a/manager/app/src/main/res/values-zh-rHK/strings.xml b/manager/app/src/main/res/values-zh-rHK/strings.xml index fd58f2307..4845e60bd 100644 --- a/manager/app/src/main/res/values-zh-rHK/strings.xml +++ b/manager/app/src/main/res/values-zh-rHK/strings.xml @@ -266,12 +266,6 @@ 核心刷寫 需要元模組 此模組想掛載 /system,此操作由 meta 模組處理。若缺少它,模組可能無法正常運作 - 名稱升序 - 名稱降序 - 安裝時間(新) - 安裝時間(舊) - 大小降序 - 大小升序 使用頻率 此分類中冇應用程式 持久 diff --git a/manager/app/src/main/res/values-zh-rTW/strings.xml b/manager/app/src/main/res/values-zh-rTW/strings.xml index fcb1b5328..a3cb85dd2 100644 --- a/manager/app/src/main/res/values-zh-rTW/strings.xml +++ b/manager/app/src/main/res/values-zh-rTW/strings.xml @@ -297,12 +297,6 @@ 正在刷寫內核 需要元模組 這個模組需要元模組掛載 /system。否則,它將無法運作 - 名稱遞增 - 名稱遞減 - 安裝時間(新) - 安裝時間(舊) - 大小遞減 - 大小遞增 使用頻率 無此分類中的應用程式 永久 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 72b6d6647..3de262274 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -318,13 +318,12 @@ Kernel Flashing Require Meta module This module wants to mount /system, which is handled by the meta module. Without it, the module might not work - Ascending order of name - Name descending - Installation time (New) - Installation time (Old) - Descending order of size - Ascending order of size + Name + Install time + Update time + Size Frequency of use + Reverse order No application in this category Persistent Temporary From 7741f87849f7ad7cebf82bbb4c5b89880e61e37e Mon Sep 17 00:00:00 2001 From: u9521 <63995396+u9521@users.noreply.github.com> Date: Mon, 14 Sep 2026 02:45:20 +0800 Subject: [PATCH 22/34] kernel: retain capabilities across execve for non-root profiles (https://github.com/tiann/KernelSU/pull/3710) Non-root profiles lose capabilities after exec su because non-root processes require ambient capabilities to inherit privileges. Populate `cap_inheritable` and `cap_ambient` when `profile->uid != 0` to preserve configured capabilities across execve. [cherry-picked from upstream commit https://github.com/tiann/KernelSU/commit/665fd96bc848615fb775cc24ff47f76ec3438def] Signed-off-by: u9521 <63995396+u9521@users.noreply.github.com> --- kernel/policy/app_profile.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kernel/policy/app_profile.c b/kernel/policy/app_profile.c index d9acd8e28..540b721a1 100644 --- a/kernel/policy/app_profile.c +++ b/kernel/policy/app_profile.c @@ -236,6 +236,10 @@ int escape_with_root_profile(void) memcpy(&cred->cap_effective, &cap_for_ksud, sizeof(cred->cap_effective)); memcpy(&cred->cap_permitted, &profile->capabilities.effective, sizeof(cred->cap_permitted)); memcpy(&cred->cap_bset, &profile->capabilities.effective, sizeof(cred->cap_bset)); + if (profile->uid != 0) { + memcpy(&cred->cap_inheritable, &profile->capabilities.effective, sizeof(cred->cap_inheritable)); + memcpy(&cred->cap_ambient, &profile->capabilities.effective, sizeof(cred->cap_ambient)); + } setup_groups(profile, cred); setup_selinux(profile->selinux_domain, cred); From 7e92d45ed5c7e0ed6e3e0f7e87d1cea510d068ea Mon Sep 17 00:00:00 2001 From: AlexLiuDev233 Date: Mon, 14 Sep 2026 19:24:02 +0800 Subject: [PATCH 23/34] ksud, ksuinit: use our forks for dependencies (#412) Upstream's org (https://github.com/Kernel-SU) flagged by GitHub Signed-off-by: AlexLiuDev233 --- userspace/ksud/Cargo.lock | 14 +++++++------- userspace/ksud/Cargo.toml | 8 ++++---- userspace/ksuinit/Cargo.lock | 4 ++-- userspace/ksuinit/Cargo.toml | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/userspace/ksud/Cargo.lock b/userspace/ksud/Cargo.lock index bdc024589..b81108969 100644 --- a/userspace/ksud/Cargo.lock +++ b/userspace/ksud/Cargo.lock @@ -5,7 +5,7 @@ version = 4 [[package]] name = "adb_client" version = "3.1.1" -source = "git+https://github.com/Kernel-SU/adb_client#d97a966435bebaa55017834869dec08150826aa7" +source = "git+https://github.com/ReSukiSU/adb_client#d97a966435bebaa55017834869dec08150826aa7" dependencies = [ "byteorder", "log", @@ -44,7 +44,7 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-bootimg" version = "0.1.0" -source = "git+https://github.com/5ec1cff/android_bootimg?rev=150425b027c76ea104c82e408571651f2181b2c2#150425b027c76ea104c82e408571651f2181b2c2" +source = "git+https://github.com/ReSukiSU/android_bootimg?rev=150425b027c76ea104c82e408571651f2181b2c2#150425b027c76ea104c82e408571651f2181b2c2" dependencies = [ "anyhow", "bytemuck", @@ -899,7 +899,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "java-properties" version = "2.0.0" -source = "git+https://github.com/Kernel-SU/java-properties.git?branch=master#42a4aa941b70ded2dd3be9e9f892471023e70229" +source = "git+https://github.com/ReSukiSU/java-properties.git?branch=master#42a4aa941b70ded2dd3be9e9f892471023e70229" dependencies = [ "encoding_rs", "lazy_static", @@ -940,7 +940,7 @@ dependencies = [ [[package]] name = "kernlog" version = "0.3.1" -source = "git+https://github.com/kstep/kernlog.rs#68caa7bf1e27baea35b00ebba786cafae0bca90f" +source = "git+https://github.com/ReSukiSU/kernlog.rs#68caa7bf1e27baea35b00ebba786cafae0bca90f" dependencies = [ "libc", "log", @@ -1366,7 +1366,7 @@ dependencies = [ [[package]] name = "prop-rs" version = "0.2.0" -source = "git+https://github.com/Kernel-SU/ksu_props?rev=ddb6ee7294467f7f25bad2118e9e24eee104144b#ddb6ee7294467f7f25bad2118e9e24eee104144b" +source = "git+https://github.com/ReSukiSU/ksu_props?rev=ddb6ee7294467f7f25bad2118e9e24eee104144b#ddb6ee7294467f7f25bad2118e9e24eee104144b" dependencies = [ "prost", ] @@ -1374,7 +1374,7 @@ dependencies = [ [[package]] name = "prop-rs-android" version = "0.2.0" -source = "git+https://github.com/Kernel-SU/ksu_props?rev=ddb6ee7294467f7f25bad2118e9e24eee104144b#ddb6ee7294467f7f25bad2118e9e24eee104144b" +source = "git+https://github.com/ReSukiSU/ksu_props?rev=ddb6ee7294467f7f25bad2118e9e24eee104144b#ddb6ee7294467f7f25bad2118e9e24eee104144b" dependencies = [ "libc", "log", @@ -1544,7 +1544,7 @@ checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustix" version = "0.38.34" -source = "git+https://github.com/Kernel-SU/rustix.git?rev=4a53fbc#4a53fbc7cb7a07cabe87125cc21dbc27db316259" +source = "git+https://github.com/ReSukiSU/rustix.git?rev=4a53fbc#4a53fbc7cb7a07cabe87125cc21dbc27db316259" dependencies = [ "bitflags 2.13.1", "errno 0.3.14", diff --git a/userspace/ksud/Cargo.toml b/userspace/ksud/Cargo.toml index a6334456a..9ae76dfcd 100644 --- a/userspace/ksud/Cargo.toml +++ b/userspace/ksud/Cargo.toml @@ -20,7 +20,7 @@ sha256 = "1" tempfile = "3" chrono = "0.4" regex-lite = "0.1" -android-bootimg = { git = "https://github.com/5ec1cff/android_bootimg", rev = "150425b027c76ea104c82e408571651f2181b2c2" } +android-bootimg = { git = "https://github.com/ReSukiSU/android_bootimg", rev = "150425b027c76ea104c82e408571651f2181b2c2" } memmap2 = "0.9.10" bitflags = "2.11.0" base16ct = { version = "1.0.0", features = ["alloc"] } @@ -43,7 +43,7 @@ zip = { version = "8", features = [ "lzma", "xz", ], default-features = false } -java-properties = { git = "https://github.com/Kernel-SU/java-properties.git", branch = "master", default-features = false } +java-properties = { git = "https://github.com/ReSukiSU/java-properties.git", branch = "master", default-features = false } serde_json = "1" encoding_rs = "0.8" humansize = "2" @@ -56,9 +56,9 @@ derive-new = "0.7" getopts = "0.2" serde = { version = "1.0", features = ["derive"] } ksuinit = { path = "../ksuinit" } -adb_client = { git = "https://github.com/Kernel-SU/adb_client" } +adb_client = { git = "https://github.com/ReSukiSU/adb_client" } num_enum = "0.7" -prop-rs-android = { git = "https://github.com/Kernel-SU/ksu_props", rev = "ddb6ee7294467f7f25bad2118e9e24eee104144b" } +prop-rs-android = { git = "https://github.com/ReSukiSU/ksu_props", rev = "ddb6ee7294467f7f25bad2118e9e24eee104144b" } [target.'cfg(not(target_os = "android"))'.dependencies] env_logger = { version = "0.11.10", default-features = false } diff --git a/userspace/ksuinit/Cargo.lock b/userspace/ksuinit/Cargo.lock index 84ff40fa4..136cef58d 100644 --- a/userspace/ksuinit/Cargo.lock +++ b/userspace/ksuinit/Cargo.lock @@ -38,7 +38,7 @@ dependencies = [ [[package]] name = "kernlog" version = "0.3.1" -source = "git+https://github.com/kstep/kernlog.rs#68caa7bf1e27baea35b00ebba786cafae0bca90f" +source = "git+https://github.com/ReSukiSU/kernlog.rs#68caa7bf1e27baea35b00ebba786cafae0bca90f" dependencies = [ "libc", "log", @@ -102,7 +102,7 @@ dependencies = [ [[package]] name = "rustix" version = "0.38.34" -source = "git+https://github.com/Kernel-SU/rustix.git?rev=4a53fbc#4a53fbc7cb7a07cabe87125cc21dbc27db316259" +source = "git+https://github.com/ReSukiSU/rustix.git?rev=4a53fbc#4a53fbc7cb7a07cabe87125cc21dbc27db316259" dependencies = [ "bitflags", "errno", diff --git a/userspace/ksuinit/Cargo.toml b/userspace/ksuinit/Cargo.toml index bded45dbe..70dd3aa51 100644 --- a/userspace/ksuinit/Cargo.toml +++ b/userspace/ksuinit/Cargo.toml @@ -12,7 +12,7 @@ goblin = "0.10" scroll = "0.13" anyhow = "1" -rustix = { git = "https://github.com/Kernel-SU/rustix.git", rev = "4a53fbc", features = ["mount", "fs", "runtime", "system", "process"] } +rustix = { git = "https://github.com/ReSukiSU/rustix.git", rev = "4a53fbc", features = ["mount", "fs", "runtime", "system", "process"] } syscalls = { version = "0.8", default-features = false, features = [ "aarch64", @@ -21,7 +21,7 @@ syscalls = { version = "0.8", default-features = false, features = [ # for kmsg logging log = "0.4" -kernlog = { git = "https://github.com/kstep/kernlog.rs" } +kernlog = { git = "https://github.com/ReSukiSU/kernlog.rs" } [profile.release] strip = true From 3341f9c4859a71b30d4d7a05c866a04ea8a33458 Mon Sep 17 00:00:00 2001 From: Bouteillepleine <76688492+Bouteillepleine@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:54:39 +0200 Subject: [PATCH 24/34] manager: Add floating navigation bar with a customization toggle (#334) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: AlexLiuDev233 Co-authored-by: AlexLiuDev233 Co-authored-by: YC酱luyancib --- .../settings/SettingsPlatformRepository.kt | 1 - .../ui/activity/component/NavigationBar.kt | 76 ++- .../ui/component/FloatingBottomBar.kt | 518 ++++++++++++++++++ .../resukisu/ui/component/SearchBar.kt | 4 +- .../ui/component/liquid/CombinedBackdrop.kt | 39 ++ .../ui/component/liquid/InnerShadow.kt | 160 ++++++ .../resukisu/ui/component/liquid/Lens.kt | 216 ++++++++ .../resukisu/ui/component/liquid/Vibrancy.kt | 15 + .../miuix/animation/DampedDragAnimation.kt | 157 ++++++ .../miuix/animation/InteractiveHighlight.kt | 113 ++++ .../miuix/modifier/DragGestureInspector.kt | 84 +++ .../ui/screen/themeSettings/ThemeSettings.kt | 78 +-- .../resukisu/ui/theme/BottomBarStyle.kt | 11 + .../com/resukisu/resukisu/ui/theme/Theme.kt | 18 +- .../src/main/res/values-zh-rCN/strings.xml | 2 + .../src/main/res/values-zh-rHK/strings.xml | 2 + .../src/main/res/values-zh-rTW/strings.xml | 2 + manager/app/src/main/res/values/strings.xml | 2 + 18 files changed, 1458 insertions(+), 40 deletions(-) create mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/component/FloatingBottomBar.kt create mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/CombinedBackdrop.kt create mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/InnerShadow.kt create mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/Lens.kt create mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/Vibrancy.kt create mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/component/miuix/animation/DampedDragAnimation.kt create mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/component/miuix/animation/InteractiveHighlight.kt create mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/component/miuix/modifier/DragGestureInspector.kt create mode 100644 manager/app/src/main/java/com/resukisu/resukisu/ui/theme/BottomBarStyle.kt diff --git a/manager/app/src/main/java/com/resukisu/resukisu/data/settings/SettingsPlatformRepository.kt b/manager/app/src/main/java/com/resukisu/resukisu/data/settings/SettingsPlatformRepository.kt index 0468db6ec..0e0ca891b 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/data/settings/SettingsPlatformRepository.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/data/settings/SettingsPlatformRepository.kt @@ -255,7 +255,6 @@ class SettingsPlatformRepository( cardConfig.save() themeConfig.preventBackgroundRefresh = false backgroundManager.saveBackgroundDim(0f) - backgroundManager.saveEnableBlur(false) backgroundManager.saveEnableBlurExp(false) backgroundManager.saveUseBackgroundSeedColor(false) backgroundManager.saveEnableHighContrastMode(false) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/activity/component/NavigationBar.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/activity/component/NavigationBar.kt index 6f443c060..4f7be090c 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/activity/component/NavigationBar.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/activity/component/NavigationBar.kt @@ -4,10 +4,16 @@ import android.annotation.SuppressLint import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.Badge @@ -16,6 +22,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FlexibleBottomAppBar import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Text @@ -25,22 +32,30 @@ import androidx.compose.material3.WideNavigationRailDefaults import androidx.compose.material3.WideNavigationRailItem import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.resukisu.resukisu.ui.component.FloatingBottomBar +import com.resukisu.resukisu.ui.component.FloatingBottomBarItem import com.resukisu.resukisu.ui.screen.BottomBarDestination +import com.resukisu.resukisu.ui.theme.BottomBarStyle import com.resukisu.resukisu.ui.theme.CardConfig import com.resukisu.resukisu.ui.theme.ThemeConfig import com.resukisu.resukisu.ui.theme.blurEffect +import com.resukisu.resukisu.ui.util.LocalBlurState import com.resukisu.resukisu.ui.util.LocalHandlePageChange +import com.resukisu.resukisu.ui.util.LocalPagerState import com.resukisu.resukisu.ui.util.LocalSelectedPage import com.resukisu.resukisu.ui.viewmodel.HomeViewModel import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel +import top.yukonga.miuix.kmp.blur.rememberLayerBackdrop -// TODO Add FloatingBottomBar as an choice to user @SuppressLint("ContextCastToActivity") @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @@ -59,8 +74,65 @@ fun NavigationBar( val showNavigationBarBadge = uiState.showNavigationBarBadge val page = LocalSelectedPage.current val handlePageChange = LocalHandlePageChange.current + val pagerState = LocalPagerState.current - if (isBottomBar) { + if (isBottomBar && themeConfig.bottomBarStyle == BottomBarStyle.FLOATING) { + Box( + modifier = Modifier + .fillMaxWidth() + .windowInsetsPadding( + WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal) + ) + .padding( + bottom = 12.dp + WindowInsets.navigationBars.asPaddingValues() + .calculateBottomPadding() + ), + contentAlignment = Alignment.Center + ) { + FloatingBottomBar( + selectedIndex = pagerState.targetPage, + onSelected = { handlePageChange(it) }, + tabsCount = destinations.size, + isBlurEnabled = LocalBlurState.current != null, + ) { activateTab -> + destinations.forEachIndexed { index, destination -> + FloatingBottomBarItem( + selected = index == pagerState.targetPage, + onClick = { activateTab(index) }, + modifier = Modifier.defaultMinSize(minWidth = 76.dp) + ) { + val contentColor = LocalContentColor.current + val count = when (destination) { + BottomBarDestination.SuperUser -> superuserCount + BottomBarDestination.Module -> moduleCount + else -> 0 + } + val icon: @Composable () -> Unit = { + Icon( + imageVector = destination.iconSelected, + contentDescription = stringResource(destination.label), + tint = contentColor + ) + } + if (count > 0) { + BadgedBox(badge = { Badge { Text(count.toString()) } }) { icon() } + } else { + icon() + } + Text( + text = stringResource(destination.label), + color = contentColor, + fontSize = 11.sp, + lineHeight = 14.sp, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Visible + ) + } + } + } + } + } else if (isBottomBar) { FlexibleBottomAppBar( modifier = modifier .windowInsetsPadding( diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/FloatingBottomBar.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/FloatingBottomBar.kt new file mode 100644 index 000000000..d29ca3427 --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/FloatingBottomBar.kt @@ -0,0 +1,518 @@ +// Adapted from compose-miuix-ui example (IosLiquidGlassNavigationBar) — Apache 2.0. + +package com.resukisu.resukisu.ui.component + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.EaseOut +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.dropShadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.shadow.Shadow +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.selected +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastCoerceIn +import androidx.compose.ui.util.lerp +import kotlinx.coroutines.launch +import com.resukisu.resukisu.ui.component.liquid.InnerShadow +import com.resukisu.resukisu.ui.component.liquid.innerShadow +import com.resukisu.resukisu.ui.component.liquid.lens +import com.resukisu.resukisu.ui.component.liquid.rememberCombinedBackdrop +import com.resukisu.resukisu.ui.component.liquid.vibrancy +import com.resukisu.resukisu.ui.component.miuix.animation.DampedDragAnimation +import com.resukisu.resukisu.ui.component.miuix.animation.InteractiveHighlight +import com.resukisu.resukisu.ui.theme.ThemeConfig +import com.resukisu.resukisu.ui.theme.isInDarkTheme +import com.resukisu.resukisu.ui.util.LocalBlurState +import org.koin.compose.koinInject +import top.yukonga.miuix.kmp.blur.Backdrop +import top.yukonga.miuix.kmp.blur.blur +import top.yukonga.miuix.kmp.blur.drawBackdrop +import top.yukonga.miuix.kmp.blur.highlight.BloomStroke +import top.yukonga.miuix.kmp.blur.highlight.Highlight +import top.yukonga.miuix.kmp.blur.highlight.LightPosition +import top.yukonga.miuix.kmp.blur.highlight.LightSource +import top.yukonga.miuix.kmp.blur.layerBackdrop +import top.yukonga.miuix.kmp.blur.rememberLayerBackdrop +import top.yukonga.miuix.kmp.blur.sensor.rememberDeviceTilt +import kotlin.math.PI +import kotlin.math.abs +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.roundToInt +import kotlin.math.sign +import kotlin.math.sin + +val LocalFloatingBottomBarTabScale = staticCompositionLocalOf { { 1f } } + +private val iosIndicatorSpecular: Highlight = Highlight( + width = 1.dp, + alpha = 1f, + style = BloomStroke( + color = Color.White.copy(alpha = 0.12f), + innerBlurRadius = 2.0.dp, + primaryLight = LightSource( + position = LightPosition(0.5f, -0.3f, -0.05f), + color = Color.White, + intensity = 1f, + ), + secondaryLight = LightSource( + position = LightPosition(0.5f, 0.8f, -0.5f), + color = Color.White, + intensity = 0.4f, + ), + dualPeak = true, + ), +) + +// Mirrors miuix-blur HighlightStyle's LIGHT_REF — keep in sync. +private const val LIGHT_REF_X = 0.5f +private const val LIGHT_REF_Y = 0.7f +private const val GRAVITY_DIR_THRESHOLD_SQ = 0.01f // |g_xy| > 0.1, ≈ 6° tilt +private const val GRAVITY_ANGLE_STEP_RAD = (3.0 * PI / 180.0).toFloat() + +/** Tracks gravity for a `dualPeak` highlight's primary light, with an extra UV-clockwise offset on top. */ +@Composable +private fun rememberQuantizedGravityAngle(): State { + val tiltState = rememberDeviceTilt() + return remember(tiltState) { + derivedStateOf { + val tilt = tiltState.value + val magnitudeSquared = tilt.gravityX * tilt.gravityX + tilt.gravityY * tilt.gravityY + if (magnitudeSquared > GRAVITY_DIR_THRESHOLD_SQ) { + (atan2(tilt.gravityY, tilt.gravityX) / GRAVITY_ANGLE_STEP_RAD).roundToInt() * GRAVITY_ANGLE_STEP_RAD + } else { + (-PI / 2).toFloat() + } + } + } +} + +@Composable +private fun rememberGravityRotatedHighlight( + base: Highlight, + extraDegrees: Float = 0f, +): State { + val baseStyle = base.style as BloomStroke + val angle = rememberQuantizedGravityAngle() + return remember(angle, base, extraDegrees) { + derivedStateOf { + val basePrimary = baseStyle.primaryLight + val rad = angle.value + (extraDegrees * PI / 180.0).toFloat() + base.copy( + style = baseStyle.copy( + primaryLight = basePrimary.copy( + position = LightPosition( + x = LIGHT_REF_X + cos(rad), + y = LIGHT_REF_Y + sin(rad), + z = basePrimary.position.z, + ), + ), + ), + ) + } + } +} + +@Composable +fun RowScope.FloatingBottomBarItem( + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit +) { + val scale = LocalFloatingBottomBarTabScale.current + Column( + modifier + .semantics(mergeDescendants = true) { + this.selected = selected + role = Role.Tab + onClick { + onClick() + true + } + } + .onKeyEvent { event -> + val activationKey = event.key == Key.Enter || + event.key == Key.NumPadEnter || event.key == Key.Spacebar + if (activationKey) { + if (event.type == KeyEventType.KeyUp) onClick() + true + } else false + } + .focusable() + .fillMaxHeight() + .weight(1f) + .graphicsLayer { + val scale = scale() + scaleX = scale + scaleY = scale + }, + verticalArrangement = Arrangement.spacedBy(1.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + content = content + ) +} + +@Composable +fun FloatingBottomBar( + modifier: Modifier = Modifier, + selectedIndex: Int, + onSelected: (index: Int) -> Unit, + tabsCount: Int, + isBlurEnabled: Boolean = true, + content: @Composable RowScope.((Int) -> Unit) -> Unit +) { + val themeConfig: ThemeConfig = koinInject() + val isInDark = isInDarkTheme(themeConfig.forceDarkMode) + val pillShape = remember { CircleShape } + val accentColor = MaterialTheme.colorScheme.primary + val tabContentColor = MaterialTheme.colorScheme.onSurface + val surfaceContainer = MaterialTheme.colorScheme.surfaceContainer + val containerColor = if (isBlurEnabled) surfaceContainer.copy(alpha = 0.4f) else surfaceContainer + + val backdrop: Backdrop = LocalBlurState.current ?: rememberLayerBackdrop() + val tabsBackdrop = rememberLayerBackdrop() + val density = LocalDensity.current + val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr + val animationScope = rememberCoroutineScope() + + var tabWidthPx by remember { mutableFloatStateOf(0f) } + var totalWidthPx by remember { mutableFloatStateOf(0f) } + + val offsetAnimation = remember { Animatable(0f) } + val rubberBandPx = with(density) { 4.dp.toPx() } + val panelOffset by remember(rubberBandPx) { + derivedStateOf { + if (totalWidthPx == 0f) { + 0f + } else { + val fraction = (offsetAnimation.value / totalWidthPx).fastCoerceIn(-1f, 1f) + rubberBandPx * fraction.sign * EaseOut.transform(abs(fraction)) + } + } + } + + var currentIndex by remember { mutableIntStateOf(selectedIndex) } + val onSelectedUpdated by rememberUpdatedState(onSelected) + + fun indexAt(positionX: Float): Int { + if (tabWidthPx == 0f) return currentIndex + val horizontalPaddingPx = with(density) { 4.dp.toPx() } + val logicalX = if (isLtr) positionX else totalWidthPx - positionX + return ((logicalX - horizontalPaddingPx) / tabWidthPx) + .toInt() + .coerceIn(0, tabsCount - 1) + } + + val dampedDragAnimation = remember(animationScope, tabsCount, density, isLtr) { + DampedDragAnimation( + animationScope = animationScope, + initialValue = selectedIndex.toFloat(), + valueRange = 0f..(tabsCount - 1).toFloat(), + visibilityThreshold = 0.001f, + initialScale = 1f, + pressedScale = 78f / 56f, + canDrag = { offset -> + offset.x in 0f..totalWidthPx + }, + onDragStarted = { position -> + updateValue(indexAt(position.x).toFloat()) + }, + onDragStopped = { + val targetIndex = targetValue.roundToInt().coerceIn(0, tabsCount - 1) + if (currentIndex != targetIndex) { + currentIndex = targetIndex + onSelectedUpdated(targetIndex) + } + updateValue(targetIndex.toFloat()) + animationScope.launch { + offsetAnimation.animateTo(0f, spring(1f, 300f, 0.5f)) + } + }, + onDragCancelled = { + updateValue(currentIndex.toFloat()) + animationScope.launch { + offsetAnimation.animateTo(0f, spring(1f, 300f, 0.5f)) + } + }, + onDrag = { _, dragAmount -> + if (tabWidthPx > 0f && dragAmount.x != 0f) { + updateValue( + (targetValue + dragAmount.x / tabWidthPx * if (isLtr) 1f else -1f) + .coerceIn(0f, (tabsCount - 1).toFloat()), + ) + animationScope.launch { + offsetAnimation.snapTo(offsetAnimation.value + dragAmount.x) + } + } + } + ) + } + + LaunchedEffect(selectedIndex) { + if (currentIndex != selectedIndex) { + currentIndex = selectedIndex + dampedDragAnimation.animateToValue(selectedIndex.toFloat()) + } + } + + fun activateTab(index: Int) { + if (index !in 0 until tabsCount) return + if (currentIndex != index) { + currentIndex = index + onSelectedUpdated(index) + } + dampedDragAnimation.animateToValue(index.toFloat()) + } + + val interactiveHighlight = remember(animationScope, tabWidthPx, dampedDragAnimation) { + InteractiveHighlight( + animationScope = animationScope, + position = { size, _ -> + Offset( + if (isLtr) (dampedDragAnimation.value + 0.5f) * tabWidthPx + panelOffset + else size.width - (dampedDragAnimation.value + 0.5f) * tabWidthPx + panelOffset, + size.height / 2f + ) + } + ) + } + + val baseHighlight = rememberGravityRotatedHighlight(iosIndicatorSpecular, extraDegrees = -45f) + val pillHighlight = rememberGravityRotatedHighlight(iosIndicatorSpecular, extraDegrees = 90f) + + val combinedBackdrop = rememberCombinedBackdrop(backdrop, tabsBackdrop) + + Box( + modifier = modifier.width(IntrinsicSize.Min), + contentAlignment = Alignment.CenterStart + ) { + Row( + Modifier + .onGloballyPositioned { coords -> + totalWidthPx = coords.size.width.toFloat() + val contentWidthPx = totalWidthPx - with(density) { 8.dp.toPx() } + tabWidthPx = (contentWidthPx / tabsCount).coerceAtLeast(0f) + } + .selectableGroup() + .graphicsLayer { translationX = panelOffset } + .dropShadow( + shape = pillShape, + shadow = Shadow( + radius = 10.dp, + color = Color.Black, + alpha = if (isInDark) 0.2f else 0.1f, + ), + ) + .then( + if (isBlurEnabled) { + Modifier.drawBackdrop( + backdrop = backdrop, + shape = { pillShape }, + effects = { + padding = maxOf(padding, 40.dp.toPx()) + vibrancy() + blur(4.dp.toPx(), 4.dp.toPx()) + lens( + refractionHeight = 24.dp.toPx(), + refractionAmount = 24.dp.toPx(), + ) + }, + highlight = { baseHighlight.value.copy(alpha = 0.75f) }, + layerBlock = { + val width = size.width.coerceAtLeast(1f) + val s = lerp(1f, 1f + 16.dp.toPx() / width, dampedDragAnimation.pressProgress) + scaleX = s + scaleY = s + }, + onDrawSurface = { drawRect(containerColor) }, + ) + } else { + Modifier.background(containerColor, pillShape) + } + ) + .then( + if (isBlurEnabled) { + interactiveHighlight.modifier.then(interactiveHighlight.gestureModifier) + } else Modifier + ) + .then(dampedDragAnimation.modifier) + .height(64.dp) + .padding(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CompositionLocalProvider(LocalContentColor provides tabContentColor) { + content(::activateTab) + } + } + + if (isBlurEnabled) { + CompositionLocalProvider( + LocalFloatingBottomBarTabScale provides { + lerp(1f, 1.2f, dampedDragAnimation.pressProgress) + }, + LocalContentColor provides accentColor, + ) { + Row( + Modifier + .clearAndSetSemantics {} + .alpha(0f) + .layerBackdrop(tabsBackdrop) + .graphicsLayer { translationX = panelOffset } + .drawBackdrop( + backdrop = backdrop, + shape = { pillShape }, + effects = { + vibrancy() + blur(4.dp.toPx(), 4.dp.toPx()) + lens( + refractionHeight = 24.dp.toPx(), + refractionAmount = 24.dp.toPx(), + ) + }, + onDrawSurface = { drawRect(containerColor) }, + ) + .then(interactiveHighlight.modifier) + .height(56.dp) + .padding(horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + content = { content(::activateTab) } + ) + } + } + + if (tabWidthPx > 0f) { + val tabWidthDp = with(density) { tabWidthPx.toDp() } + if (isBlurEnabled) { + Box( + Modifier + .padding(horizontal = 4.dp) + .graphicsLayer { + val progressOffset = dampedDragAnimation.value * tabWidthPx + translationX = if (isLtr) progressOffset + panelOffset else -progressOffset + panelOffset + } + .drawBackdrop( + backdrop = combinedBackdrop, + shape = { pillShape }, + effects = { + val progress = dampedDragAnimation.pressProgress + lens( + refractionHeight = 10.dp.toPx() * progress, + refractionAmount = 14.dp.toPx() * progress, + depthEffect = true, + chromaticAberration = 0.5f, + ) + }, + highlight = { pillHighlight.value.copy(alpha = dampedDragAnimation.pressProgress) }, + layerBlock = { + scaleX = dampedDragAnimation.scaleX + scaleY = dampedDragAnimation.scaleY + val velocity = dampedDragAnimation.velocity / 10f + scaleX /= 1f - (velocity * 0.75f).fastCoerceIn(-0.2f, 0.2f) + scaleY *= 1f - (velocity * 0.25f).fastCoerceIn(-0.2f, 0.2f) + }, + onDrawSurface = { + val progress = dampedDragAnimation.pressProgress + drawRect( + color = if (!isInDark) Color.Black.copy(alpha = 0.1f) else Color.White.copy(alpha = 0.1f), + alpha = 1f - progress, + ) + drawRect(Color.Black.copy(alpha = 0.03f * progress)) + }, + ) + .innerShadow(shape = pillShape) { + InnerShadow( + radius = 8.dp * dampedDragAnimation.pressProgress, + color = Color.Black.copy(alpha = 0.15f), + alpha = dampedDragAnimation.pressProgress, + ) + } + .height(56.dp) + .width(tabWidthDp) + ) + } else { + Box( + Modifier + .padding(horizontal = 4.dp) + .graphicsLayer { + val progressOffset = dampedDragAnimation.value * tabWidthPx + translationX = if (isLtr) progressOffset + panelOffset else -progressOffset + panelOffset + } + .clip(pillShape) + .background(accentColor.copy(alpha = 0.15f), pillShape) + .height(56.dp) + .width(tabWidthDp), + contentAlignment = Alignment.CenterStart, + ) { + CompositionLocalProvider(LocalContentColor provides accentColor) { + Row( + Modifier + .clearAndSetSemantics {} + .wrapContentWidth(align = Alignment.Start, unbounded = true) + .requiredWidth(with(density) { (totalWidthPx - 8.dp.toPx()).toDp() }) + .height(56.dp) + .graphicsLayer { + val progressOffset = dampedDragAnimation.value * tabWidthPx + translationX = if (isLtr) -progressOffset else progressOffset + }, + verticalAlignment = Alignment.CenterVertically, + content = { content(::activateTab) }, + ) + } + } + } + } + } +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/SearchBar.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/SearchBar.kt index 6f15b45b0..1eceecfc8 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/SearchBar.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/SearchBar.kt @@ -478,10 +478,10 @@ fun SearchAppBar( windowInsets = TopAppBarDefaults.windowInsets.add(WindowInsets(left = 12.dp)), colors = TopAppBarDefaults.topAppBarColors( containerColor = - if (themeConfig.isEnableBlurExp) Color.Transparent + if (themeConfig.isEnableBlur) Color.Transparent else MaterialTheme.colorScheme.surfaceContainer.copy(alpha = cardConfig.cardAlpha), scrolledContainerColor = - if (themeConfig.isEnableBlurExp) Color.Transparent + if (themeConfig.isEnableBlur) Color.Transparent else MaterialTheme.colorScheme.surfaceContainer.copy(alpha = cardConfig.cardAlpha), ), ) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/CombinedBackdrop.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/CombinedBackdrop.kt new file mode 100644 index 000000000..e5706312c --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/CombinedBackdrop.kt @@ -0,0 +1,39 @@ +// Adapted from Kyant0/AndroidLiquidGlass — https://github.com/Kyant0/AndroidLiquidGlass (Apache 2.0). +// Mirrored from compose-miuix-ui example. + +package com.resukisu.resukisu.ui.component.liquid + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.GraphicsLayerScope +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.unit.Density +import top.yukonga.miuix.kmp.blur.Backdrop + +@Stable +class CombinedBackdrop( + val first: Backdrop, + val second: Backdrop, +) : Backdrop { + + override val isCoordinatesDependent: Boolean = first.isCoordinatesDependent || second.isCoordinatesDependent + + override val offsetResidualX: Float get() = first.offsetResidualX + override val offsetResidualY: Float get() = first.offsetResidualY + + override fun DrawScope.drawBackdrop( + density: Density, + coordinates: LayoutCoordinates?, + layerBlock: (GraphicsLayerScope.() -> Unit)?, + downscaleFactor: Int, + ) { + with(first) { drawBackdrop(density, coordinates, layerBlock, downscaleFactor) } + with(second) { drawBackdrop(density, coordinates, layerBlock, downscaleFactor) } + } +} + +@Composable +fun rememberCombinedBackdrop(first: Backdrop, second: Backdrop): Backdrop = + remember(first, second) { CombinedBackdrop(first, second) } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/InnerShadow.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/InnerShadow.kt new file mode 100644 index 000000000..669ab5e8d --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/InnerShadow.kt @@ -0,0 +1,160 @@ +// Adapted from Kyant0/AndroidLiquidGlass — https://github.com/Kyant0/AndroidLiquidGlass (Apache 2.0). +// Mirrored from compose-miuix-ui example. + +package com.resukisu.resukisu.ui.component.liquid + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.BlurEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.layer.CompositingStrategy +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.invalidateDraw +import androidx.compose.ui.node.requireGraphicsContext +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp + +@Immutable +data class InnerShadow( + val radius: Dp = 24.dp, + val offset: DpOffset = DpOffset(0.dp, radius), + val color: Color = Color.Black.copy(alpha = 0.15f), + val alpha: Float = 1f, + val blendMode: BlendMode = DrawScope.DefaultBlendMode, +) { + companion object { + @Stable + val Default: InnerShadow = InnerShadow() + } +} + +fun Modifier.innerShadow( + shape: Shape, + shadow: () -> InnerShadow?, +): Modifier = this then InnerShadowElement(shape, shadow) + +private class InnerShadowElement( + val shape: Shape, + val shadow: () -> InnerShadow?, +) : ModifierNodeElement() { + + override fun create(): InnerShadowNode = InnerShadowNode(shape, shadow) + + override fun update(node: InnerShadowNode) { + node.shape = shape + node.shadow = shadow + node.invalidateDraw() + } + + override fun InspectorInfo.inspectableProperties() { + name = "innerShadow" + properties["shape"] = shape + properties["shadow"] = shadow + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is InnerShadowElement) return false + if (shape != other.shape) return false + if (shadow != other.shadow) return false + return true + } + + override fun hashCode(): Int { + var result = shape.hashCode() + result = 31 * result + shadow.hashCode() + return result + } +} + +private class InnerShadowNode( + var shape: Shape, + var shadow: () -> InnerShadow?, +) : Modifier.Node(), + DrawModifierNode { + + override val shouldAutoInvalidate: Boolean = false + + private var shadowLayer: GraphicsLayer? = null + private val paint = Paint() + private val clipPath = Path() + private var prevRadius = Float.NaN + + override fun ContentDrawScope.draw() { + drawContent() + + val shadow = shadow() ?: return + val layer = shadowLayer ?: return + + val radius = shadow.radius.toPx() + val offsetX = shadow.offset.x.toPx() + val offsetY = shadow.offset.y.toPx() + + val outline = shape.createOutline(size, layoutDirection, this) + clipPath.reset() + when (outline) { + is Outline.Rectangle -> clipPath.addRect(outline.rect) + is Outline.Rounded -> clipPath.addRoundRect(outline.roundRect) + is Outline.Generic -> clipPath.addPath(outline.path) + } + + paint.color = shadow.color + layer.alpha = shadow.alpha + layer.blendMode = shadow.blendMode + if (prevRadius != radius) { + layer.renderEffect = if (radius > 0f) BlurEffect(radius, radius, TileMode.Decal) else null + prevRadius = radius + } + + layer.record { + drawContext.canvas.let { canvas -> + canvas.save() + canvas.clipPath(clipPath) + canvas.drawOutline(outline, paint) + canvas.translate(offsetX, offsetY) + canvas.drawOutline(outline, ShadowMaskPaint) + canvas.translate(-offsetX, -offsetY) + canvas.restore() + } + } + + drawContext.canvas.let { canvas -> + canvas.save() + canvas.clipPath(clipPath) + drawLayer(layer) + canvas.restore() + } + } + + override fun onAttach() { + shadowLayer = requireGraphicsContext().createGraphicsLayer().apply { + compositingStrategy = CompositingStrategy.Offscreen + } + } + + override fun onDetach() { + shadowLayer?.let { layer -> + requireGraphicsContext().releaseGraphicsLayer(layer) + shadowLayer = null + } + } +} + +private val ShadowMaskPaint: Paint = Paint().apply { + blendMode = BlendMode.Clear +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/Lens.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/Lens.kt new file mode 100644 index 000000000..269fad265 --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/Lens.kt @@ -0,0 +1,216 @@ +// Adapted from Kyant0/AndroidLiquidGlass — https://github.com/Kyant0/AndroidLiquidGlass (Apache 2.0). +// Mirrored from compose-miuix-ui example. + +package com.resukisu.resukisu.ui.component.liquid + +import androidx.compose.foundation.shape.CornerBasedShape +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.util.fastCoerceAtMost +import top.yukonga.miuix.kmp.blur.BackdropEffectScope +import top.yukonga.miuix.kmp.blur.isRuntimeShaderSupported +import top.yukonga.miuix.kmp.blur.runtimeShaderEffect + +fun BackdropEffectScope.lens( + refractionHeight: Float, + refractionAmount: Float, + depthEffect: Boolean = false, + chromaticAberration: Float = 0f, +) { + if (!isRuntimeShaderSupported()) return + if (refractionHeight <= 0f || refractionAmount <= 0f) return + + if (padding < refractionAmount) { + padding = refractionAmount + } + + val radii = roundedRectCornerRadii() ?: return + + val dispersionEnabled = chromaticAberration > 0f + val shaderString = + if (dispersionEnabled) { + ROUNDED_RECT_REFRACTION_WITH_DISPERSION_SHADER + } else { + ROUNDED_RECT_REFRACTION_SHADER + } + val key = if (dispersionEnabled) "LiquidGlassLensDispersion" else "LiquidGlassLens" + + val sf = downscaleFactor.coerceAtLeast(1).toFloat() + val scaledSizeW = size.width / sf + val scaledSizeH = size.height / sf + val scaledPadding = padding / sf + val scaledRefractionHeight = refractionHeight / sf + val scaledRefractionAmount = refractionAmount / sf + val scaledRadii = FloatArray(radii.size) { radii[it] / sf } + + runtimeShaderEffect( + key = key, + shaderString = shaderString, + uniformShaderName = "content", + ) { + setFloatUniform("size", scaledSizeW, scaledSizeH) + setFloatUniform("offset", -scaledPadding, -scaledPadding) + setFloatUniform("cornerRadii", scaledRadii) + setFloatUniform("refractionHeight", scaledRefractionHeight) + setFloatUniform("refractionAmount", -scaledRefractionAmount) + setFloatUniform("depthEffect", if (depthEffect) 1f else 0f) + if (dispersionEnabled) { + setFloatUniform("chromaticAberration", chromaticAberration) + } + } +} + +private fun BackdropEffectScope.roundedRectCornerRadii(): FloatArray? { + val cornerShape = shape as? CornerBasedShape ?: return null + val sizePx = size + val maxRadius = sizePx.minDimension / 2f + val isLtr = layoutDirection == LayoutDirection.Ltr + val topLeft = if (isLtr) cornerShape.topStart.toPx(sizePx, this) else cornerShape.topEnd.toPx(sizePx, this) + val topRight = if (isLtr) cornerShape.topEnd.toPx(sizePx, this) else cornerShape.topStart.toPx(sizePx, this) + val bottomRight = if (isLtr) cornerShape.bottomEnd.toPx(sizePx, this) else cornerShape.bottomStart.toPx(sizePx, this) + val bottomLeft = if (isLtr) cornerShape.bottomStart.toPx(sizePx, this) else cornerShape.bottomEnd.toPx(sizePx, this) + return floatArrayOf( + topLeft.fastCoerceAtMost(maxRadius), + topRight.fastCoerceAtMost(maxRadius), + bottomRight.fastCoerceAtMost(maxRadius), + bottomLeft.fastCoerceAtMost(maxRadius), + ) +} + +private const val ROUNDED_RECT_SDF = """ +float radiusAt(float2 coord, float4 radii) { + if (coord.x >= 0.0) { + if (coord.y <= 0.0) return radii.y; + else return radii.z; + } else { + if (coord.y <= 0.0) return radii.x; + else return radii.w; + } +} + +float sdRoundedRect(float2 coord, float2 halfSize, float radius) { + float2 cornerCoord = abs(coord) - (halfSize - float2(radius)); + float outside = length(max(cornerCoord, 0.0)) - radius; + float inside = min(max(cornerCoord.x, cornerCoord.y), 0.0); + return outside + inside; +} + +float2 gradSdRoundedRect(float2 coord, float2 halfSize, float radius) { + float2 cornerCoord = abs(coord) - (halfSize - float2(radius)); + if (cornerCoord.x >= 0.0 || cornerCoord.y >= 0.0) { + return sign(coord) * normalize(max(cornerCoord, 0.0)); + } else { + float gradX = step(cornerCoord.y, cornerCoord.x); + return sign(coord) * float2(gradX, 1.0 - gradX); + } +} +""" + +private const val ROUNDED_RECT_REFRACTION_SHADER = """ +uniform shader content; + +uniform float2 size; +uniform float2 offset; +uniform float4 cornerRadii; +uniform float refractionHeight; +uniform float refractionAmount; +uniform float depthEffect; + +$ROUNDED_RECT_SDF + +float circleMap(float x) { + return 1.0 - sqrt(1.0 - x * x); +} + +half4 main(float2 coord) { + float2 halfSize = size * 0.5; + float2 centeredCoord = (coord + offset) - halfSize; + float radius = radiusAt(coord, cornerRadii); + + float sd = sdRoundedRect(centeredCoord, halfSize, radius); + if (-sd >= refractionHeight) { + return content.eval(coord); + } + sd = min(sd, 0.0); + + float d = circleMap(1.0 - -sd / refractionHeight) * refractionAmount; + float gradRadius = min(radius * 1.5, min(halfSize.x, halfSize.y)); + float2 grad = normalize(gradSdRoundedRect(centeredCoord, halfSize, gradRadius) + depthEffect * normalize(centeredCoord)); + + float2 refractedCoord = coord + d * grad; + return content.eval(refractedCoord); +} +""" + +private const val ROUNDED_RECT_REFRACTION_WITH_DISPERSION_SHADER = """ +uniform shader content; + +uniform float2 size; +uniform float2 offset; +uniform float4 cornerRadii; +uniform float refractionHeight; +uniform float refractionAmount; +uniform float depthEffect; +uniform float chromaticAberration; + +$ROUNDED_RECT_SDF + +float circleMap(float x) { + return 1.0 - sqrt(1.0 - x * x); +} + +half4 main(float2 coord) { + float2 halfSize = size * 0.5; + float2 centeredCoord = (coord + offset) - halfSize; + float radius = radiusAt(coord, cornerRadii); + + float sd = sdRoundedRect(centeredCoord, halfSize, radius); + if (-sd >= refractionHeight) { + return content.eval(coord); + } + sd = min(sd, 0.0); + + float d = circleMap(1.0 - -sd / refractionHeight) * refractionAmount; + float gradRadius = min(radius * 1.5, min(halfSize.x, halfSize.y)); + float2 grad = normalize(gradSdRoundedRect(centeredCoord, halfSize, gradRadius) + depthEffect * normalize(centeredCoord)); + + float2 refractedCoord = coord + d * grad; + float dispersionIntensity = chromaticAberration * ((centeredCoord.x * centeredCoord.y) / (halfSize.x * halfSize.y)); + float2 dispersedCoord = d * grad * dispersionIntensity; + + half4 color = half4(0.0); + + half4 red = content.eval(refractedCoord + dispersedCoord); + color.r += red.r / 3.5; + color.a += red.a / 7.0; + + half4 orange = content.eval(refractedCoord + dispersedCoord * (2.0 / 3.0)); + color.r += orange.r / 3.5; + color.g += orange.g / 7.0; + color.a += orange.a / 7.0; + + half4 yellow = content.eval(refractedCoord + dispersedCoord * (1.0 / 3.0)); + color.r += yellow.r / 3.5; + color.g += yellow.g / 3.5; + color.a += yellow.a / 7.0; + + half4 green = content.eval(refractedCoord); + color.g += green.g / 3.5; + color.a += green.a / 7.0; + + half4 cyan = content.eval(refractedCoord - dispersedCoord * (1.0 / 3.0)); + color.g += cyan.g / 3.5; + color.b += cyan.b / 3.0; + color.a += cyan.a / 7.0; + + half4 blue = content.eval(refractedCoord - dispersedCoord * (2.0 / 3.0)); + color.b += blue.b / 3.0; + color.a += blue.a / 7.0; + + half4 purple = content.eval(refractedCoord - dispersedCoord); + color.r += purple.r / 7.0; + color.b += purple.b / 3.0; + color.a += purple.a / 7.0; + + return color; +} +""" diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/Vibrancy.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/Vibrancy.kt new file mode 100644 index 000000000..e32c17fd2 --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/Vibrancy.kt @@ -0,0 +1,15 @@ +// Adapted from Kyant0/AndroidLiquidGlass — https://github.com/Kyant0/AndroidLiquidGlass (Apache 2.0). +// Mirrored from compose-miuix-ui example. + +package com.resukisu.resukisu.ui.component.liquid + +import top.yukonga.miuix.kmp.blur.BackdropEffectScope +import top.yukonga.miuix.kmp.blur.colorControls + +fun BackdropEffectScope.vibrancy() { + colorControls( + brightness = 0f, + contrast = 1f, + saturation = 1.5f, + ) +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/miuix/animation/DampedDragAnimation.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/miuix/animation/DampedDragAnimation.kt new file mode 100644 index 000000000..d4f50e74f --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/miuix/animation/DampedDragAnimation.kt @@ -0,0 +1,157 @@ +package com.resukisu.resukisu.ui.component.miuix.animation + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.spring +import androidx.compose.foundation.MutatorMutex +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.util.VelocityTracker +import androidx.compose.ui.unit.IntSize +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Job +import kotlinx.coroutines.android.awaitFrame +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import com.resukisu.resukisu.ui.component.miuix.modifier.inspectDragGestures +import kotlin.math.abs +import kotlin.time.TimeSource + +class DampedDragAnimation( + private val animationScope: CoroutineScope, + val initialValue: Float, + val valueRange: ClosedRange, + val visibilityThreshold: Float, + val initialScale: Float, + val pressedScale: Float, + val canDrag: (Offset) -> Boolean = { true }, + val onDragStarted: DampedDragAnimation.(position: Offset) -> Unit, + val onDragStopped: DampedDragAnimation.() -> Unit, + val onDragCancelled: DampedDragAnimation.() -> Unit = onDragStopped, + val onDrag: DampedDragAnimation.(size: IntSize, dragAmount: Offset) -> Unit, +) { + + private val valueAnimationSpec = + spring(1f, 1000f, visibilityThreshold) + private val velocityAnimationSpec = + spring(0.5f, 300f, visibilityThreshold * 10f) + private val pressProgressAnimationSpec = + spring(1f, 1000f, 0.001f) + private val scaleXAnimationSpec = + spring(0.6f, 250f, 0.001f) + private val scaleYAnimationSpec = + spring(0.7f, 250f, 0.001f) + + private val valueAnimation = + Animatable(initialValue, visibilityThreshold) + private val velocityAnimation = + Animatable(0f, 5f) + private val pressProgressAnimation = + Animatable(0f, 0.001f) + private val scaleXAnimation = + Animatable(initialScale, 0.001f) + private val scaleYAnimation = + Animatable(initialScale, 0.001f) + + private val mutatorMutex = MutatorMutex() + + private var pressJob: Job? = null + private var releaseJob: Job? = null + + private val velocityTracker = VelocityTracker() + private val startMark = TimeSource.Monotonic.markNow() + + val value: Float get() = valueAnimation.value + val targetValue: Float get() = valueAnimation.targetValue + val pressProgress: Float get() = pressProgressAnimation.value + val scaleX: Float get() = scaleXAnimation.value + val scaleY: Float get() = scaleYAnimation.value + val velocity: Float get() = velocityAnimation.value + + val modifier: Modifier = Modifier.pointerInput(Unit) { + inspectDragGestures( + onDragStart = { down -> + onDragStarted(down.position) + press() + }, + onDragEnd = { + onDragStopped() + release() + }, + onDragCancel = { + onDragCancelled() + release() + } + ) { change, dragAmount -> + val position = change.position + val previousPosition = change.previousPosition + + val isInside = canDrag(position) + val wasInside = canDrag(previousPosition) + + if (isInside && wasInside) { + onDrag(size, dragAmount) + } + } + } + + fun press() { + releaseJob?.cancel() + pressJob?.cancel() + velocityTracker.resetTracking() + pressJob = animationScope.launch { + launch { pressProgressAnimation.animateTo(1f, pressProgressAnimationSpec) } + launch { scaleXAnimation.animateTo(pressedScale, scaleXAnimationSpec) } + launch { scaleYAnimation.animateTo(pressedScale, scaleYAnimationSpec) } + } + } + + fun release() { + releaseJob?.cancel() + releaseJob = animationScope.launch { + awaitFrame() + if (value != targetValue) { + val threshold = (valueRange.endInclusive - valueRange.start) * 0.025f + snapshotFlow { valueAnimation.value }.first { abs(it - valueAnimation.targetValue) < threshold } + } + launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) } + launch { scaleXAnimation.animateTo(initialScale, scaleXAnimationSpec) } + launch { scaleYAnimation.animateTo(initialScale, scaleYAnimationSpec) } + } + } + + fun updateValue(value: Float) { + val targetValue = value.coerceIn(valueRange) + animationScope.launch(start = CoroutineStart.UNDISPATCHED) { + valueAnimation.animateTo(targetValue, valueAnimationSpec) { updateVelocity() } + } + } + + fun animateToValue(value: Float) { + animationScope.launch { + mutatorMutex.mutate { + press() + val targetValue = value.coerceIn(valueRange) + launch { valueAnimation.animateTo(targetValue, valueAnimationSpec) } + if (velocity != 0f) { + launch { velocityAnimation.animateTo(0f, velocityAnimationSpec) } + } + release() + } + } + } + + private fun updateVelocity() { + velocityTracker.addPosition( + startMark.elapsedNow().inWholeMilliseconds, + Offset(value, 0f), + ) + val span = (valueRange.endInclusive - valueRange.start).coerceAtLeast(1e-6f) + val targetVelocity = velocityTracker.calculateVelocity().x / span + animationScope.launch(start = CoroutineStart.UNDISPATCHED) { + velocityAnimation.snapTo(targetVelocity) + } + } +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/miuix/animation/InteractiveHighlight.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/miuix/animation/InteractiveHighlight.kt new file mode 100644 index 000000000..a8786b4e2 --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/miuix/animation/InteractiveHighlight.kt @@ -0,0 +1,113 @@ +package com.resukisu.resukisu.ui.component.miuix.animation + +import android.annotation.SuppressLint +import android.graphics.RuntimeShader +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.VectorConverter +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.spring +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.util.fastCoerceIn +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import com.resukisu.resukisu.ui.component.miuix.modifier.inspectDragGestures +import org.intellij.lang.annotations.Language + +@SuppressLint("NewApi") +class InteractiveHighlight( + val animationScope: CoroutineScope, + val position: (size: Size, offset: Offset) -> Offset = { _, offset -> offset } +) { + + private val pressProgressAnimationSpec = + spring(0.5f, 300f, 0.001f) + private val positionAnimationSpec = + spring(0.5f, 300f, Offset.VisibilityThreshold) + + private val pressProgressAnimation = + Animatable(0f, 0.001f) + private val positionAnimation = + Animatable(Offset.Zero, Offset.VectorConverter, Offset.VisibilityThreshold) + + private var startPosition = Offset.Zero + val offset: Offset get() = positionAnimation.value - startPosition + + @Language("AGSL") + private val shader = + RuntimeShader( + """ + uniform float2 size; + layout(color) uniform half4 color; + uniform float radius; + uniform float2 position; + + half4 main(float2 coord) { + float dist = distance(coord, position); + float intensity = smoothstep(radius, radius * 0.5, dist); + return color * intensity; + }""" + ) + + val modifier: Modifier = + Modifier.drawWithContent { + val progress = pressProgressAnimation.value + if (progress > 0f) { + drawRect( + Color.White.copy(0.06f * progress), + blendMode = BlendMode.Plus + ) + shader.apply { + val position = position(size, positionAnimation.value) + setFloatUniform("size", size.width, size.height) + setColorUniform("color", Color.White.copy(0.12f * progress).toArgb()) + setFloatUniform("radius", size.minDimension * 1.2f) + setFloatUniform( + "position", + position.x.fastCoerceIn(0f, size.width), + position.y.fastCoerceIn(0f, size.height) + ) + } + drawRect( + ShaderBrush(shader), + blendMode = BlendMode.Plus + ) + } + + drawContent() + } + + val gestureModifier: Modifier = + Modifier.pointerInput(animationScope) { + inspectDragGestures( + onDragStart = { down -> + startPosition = down.position + animationScope.launch { + launch { pressProgressAnimation.animateTo(1f, pressProgressAnimationSpec) } + launch { positionAnimation.snapTo(startPosition) } + } + }, + onDragEnd = { + animationScope.launch { + launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) } + launch { positionAnimation.animateTo(startPosition, positionAnimationSpec) } + } + }, + onDragCancel = { + animationScope.launch { + launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) } + launch { positionAnimation.animateTo(startPosition, positionAnimationSpec) } + } + } + ) { change, _ -> + animationScope.launch { positionAnimation.snapTo(change.position) } + } + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/miuix/modifier/DragGestureInspector.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/miuix/modifier/DragGestureInspector.kt new file mode 100644 index 000000000..27c0ae7bf --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/miuix/modifier/DragGestureInspector.kt @@ -0,0 +1,84 @@ +package com.resukisu.resukisu.ui.component.miuix.modifier + +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.AwaitPointerEventScope +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerId +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed +import androidx.compose.ui.input.pointer.positionChange +import androidx.compose.ui.util.fastFirstOrNull + +suspend fun PointerInputScope.inspectDragGestures( + onDragStart: (down: PointerInputChange) -> Unit = {}, + onDragEnd: (change: PointerInputChange) -> Unit = {}, + onDragCancel: () -> Unit = {}, + onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit +) { + awaitEachGesture { + val initialDown = awaitFirstDown(false, PointerEventPass.Initial) + + val down = awaitFirstDown(false) + + onDragStart(down) + onDrag(initialDown, Offset.Zero) + val upEvent = + drag( + pointerId = initialDown.id, + onDrag = { onDrag(it, it.positionChange()) } + ) + if (upEvent == null) { + onDragCancel() + } else { + onDragEnd(upEvent) + } + } +} + +private suspend inline fun AwaitPointerEventScope.drag( + pointerId: PointerId, + onDrag: (PointerInputChange) -> Unit +): PointerInputChange? { + val isPointerUp = currentEvent.changes.fastFirstOrNull { it.id == pointerId }?.pressed != true + if (isPointerUp) { + return null + } + var pointer = pointerId + while (true) { + val change = awaitDragOrUp(pointer) ?: return null + if (change.isConsumed) { + return null + } + if (change.changedToUpIgnoreConsumed()) { + return change + } + onDrag(change) + pointer = change.id + } +} + +private suspend inline fun AwaitPointerEventScope.awaitDragOrUp( + pointerId: PointerId +): PointerInputChange? { + var pointer = pointerId + while (true) { + val event = awaitPointerEvent() + val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null + if (dragEvent.changedToUpIgnoreConsumed()) { + val otherDown = event.changes.fastFirstOrNull { it.pressed } + if (otherDown == null) { + return dragEvent + } else { + pointer = otherDown.id + } + } else { + val hasDragged = dragEvent.previousPosition != dragEvent.position + if (hasDragged) { + return dragEvent + } + } + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/ThemeSettings.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/ThemeSettings.kt index 9c69f767a..db2bb8a26 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/ThemeSettings.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/ThemeSettings.kt @@ -41,6 +41,7 @@ import androidx.compose.material.icons.twotone.ColorLens import androidx.compose.material.icons.twotone.Contrast import androidx.compose.material.icons.twotone.DarkMode import androidx.compose.material.icons.twotone.DesignServices +import androidx.compose.material.icons.twotone.Dock import androidx.compose.material.icons.twotone.Draw import androidx.compose.material.icons.twotone.FormatColorFill import androidx.compose.material.icons.twotone.FormatSize @@ -77,6 +78,7 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -103,6 +105,7 @@ import com.resukisu.resukisu.ui.screen.themeSettings.component.LanguageSelection import com.resukisu.resukisu.ui.screen.themeSettings.component.ThemeSettingsDialogs import com.resukisu.resukisu.ui.screen.themeSettings.crop.BackgroundCropActivity import com.resukisu.resukisu.ui.theme.BackgroundManager +import com.resukisu.resukisu.ui.theme.BottomBarStyle import com.resukisu.resukisu.ui.theme.CardConfig import com.resukisu.resukisu.ui.theme.ThemeConfig import com.resukisu.resukisu.ui.theme.blurEffect @@ -486,6 +489,9 @@ private fun AppearanceSettings( val cardConfig: CardConfig = koinInject() val backgroundManager: BackgroundManager = koinInject() val paletteStyles = state.dynamicColorSpec.availablePaletteStyles() + val configuration = LocalConfiguration.current + val isPortrait = configuration.screenWidthDp < configuration.screenHeightDp || + (configuration.screenHeightDp.toFloat() / configuration.screenWidthDp > 1.4f) SegmentedColumn(title = stringResource(R.string.appearance_settings)) { item { // 语言设置 @@ -601,6 +607,34 @@ private fun AppearanceSettings( } } + item(visible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + SettingsSwitchWidget( + icon = Icons.TwoTone.BlurOn, + title = stringResource(id = R.string.settings_config_enable_blur), + description = stringResource(id = R.string.settings_config_enable_blur_summary), + checked = themeConfig.isEnableBlur, + onCheckedChange = { isChecked -> + backgroundManager.saveEnableBlur(isChecked) + if (!isChecked) + backgroundManager.saveEnableBlurExp(false) + } + ) + } + + item(visible = isPortrait) { + SettingsSwitchWidget( + icon = Icons.TwoTone.Dock, + title = stringResource(R.string.enable_floating_bottom_bar), + description = stringResource(R.string.enable_floating_bottom_bar_summary), + checked = themeConfig.bottomBarStyle == BottomBarStyle.FLOATING, + onCheckedChange = { enabled -> + val style = if (enabled) BottomBarStyle.FLOATING else BottomBarStyle.MATERIAL3_EXPRESSIVE + backgroundManager.saveBottomBarStyle(style) + } + ) + } + + expandableItem( expanded = state.isCustomBackgroundEnabled, topContent = { @@ -887,40 +921,18 @@ private fun SegmentedColumnScope.backgroundAdjustmentControls( ) } - expandableItem( - animatedVisibility = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S, - expanded = themeConfig.isEnableBlur, - topPadding = 1.dp, - topContent = { - SettingsSwitchWidget( - icon = Icons.TwoTone.BlurOn, - title = stringResource(id = R.string.settings_config_enable_blur), - description = stringResource(id = R.string.settings_config_enable_blur_summary), - checked = themeConfig.isEnableBlur, - onCheckedChange = { isChecked -> - backgroundManager.saveEnableBlur(isChecked) - if (!isChecked) - backgroundManager.saveEnableBlurExp(false) - } - ) - }, - bottomContent = { - item( - topPadding = 1.dp, - ) { - SettingsSwitchWidget( - icon = Icons.TwoTone.Draw, - title = stringResource(id = R.string.settings_exp_draw_background_to_blur), - description = stringResource(id = R.string.settings_exp_draw_background_to_blur_description), - isError = true, - checked = themeConfig.isEnableBlurExp, - onCheckedChange = { isChecked -> - backgroundManager.saveEnableBlurExp(isChecked) - } - ) + item(visible = themeConfig.isEnableBlur, topPadding = 1.dp) { + SettingsSwitchWidget( + icon = Icons.TwoTone.Draw, + title = stringResource(id = R.string.settings_exp_draw_background_to_blur), + description = stringResource(id = R.string.settings_exp_draw_background_to_blur_description), + isError = true, + checked = themeConfig.isEnableBlurExp, + onCheckedChange = { isChecked -> + backgroundManager.saveEnableBlurExp(isChecked) } - } - ) + ) + } item( visible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && state.useDynamicColor, diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/theme/BottomBarStyle.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/theme/BottomBarStyle.kt new file mode 100644 index 000000000..22a72a07c --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/theme/BottomBarStyle.kt @@ -0,0 +1,11 @@ +package com.resukisu.resukisu.ui.theme + +enum class BottomBarStyle { + MATERIAL3_EXPRESSIVE, + FLOATING; + + companion object { + fun fromOrdinal(ordinal: Int): BottomBarStyle = + entries.getOrElse(ordinal) { MATERIAL3_EXPRESSIVE } + } +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/theme/Theme.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/theme/Theme.kt index ced4b18a9..068a5c35b 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/theme/Theme.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/theme/Theme.kt @@ -139,6 +139,7 @@ class ThemeConfig( var isEnableBlur by mutableStateOf(false) var isEnableBlurExp by mutableStateOf(false) var isUseBackgroundSeedColor by mutableStateOf(false) + var bottomBarStyle by mutableStateOf(BottomBarStyle.MATERIAL3_EXPRESSIVE) // 主题变化检测 private var lastDarkModeState: Boolean? = null @@ -221,6 +222,11 @@ class BackgroundManager( settings.putBoolean("enable_blur_exp", enable) } + fun saveBottomBarStyle(style: BottomBarStyle) { + config.bottomBarStyle = style + settings.putInt("bottom_bar_style", style.ordinal) + } + fun saveUseBackgroundSeedColor(enable: Boolean) { config.isUseBackgroundSeedColor = enable settings.putBoolean("use_background_seed_color", enable) @@ -283,7 +289,6 @@ class BackgroundManager( } config.backgroundDim = prefs.getFloat("background_dim", 0f).coerceIn(0f, 1f) - config.isEnableBlur = prefs.getBoolean("enable_blur", false) config.isEnableBlurExp = prefs.getBoolean("enable_blur_exp", false) config.isUseBackgroundSeedColor = prefs.getBoolean("use_background_seed_color", false) config.isHighContrastMode = prefs.getBoolean("high_contrast_mode", false) @@ -360,6 +365,7 @@ fun KernelSUTheme( themeRepository = themeRepository, backgroundManager = backgroundManager, cardConfig = cardConfig, + settings = settings, ) // 创建颜色方案 @@ -409,6 +415,7 @@ private fun ThemeInitializer( themeRepository: ThemeRepository, backgroundManager: BackgroundManager, cardConfig: CardConfig, + settings: AppSettingsRepository, ) { val themeChanged = themeConfig.detectThemeChange(systemIsDark) val scope = rememberCoroutineScope() @@ -441,6 +448,8 @@ private fun ThemeInitializer( themeConfig.dynamicPaletteStyle = themeRepository.loadDynamicPaletteStyle( themeConfig.dynamicColorSpec, ) + themeConfig.isEnableBlur = settings.getBoolean("enable_blur", false) + themeConfig.bottomBarStyle = BottomBarStyle.fromOrdinal(settings.getInt("bottom_bar_style", 0)) cardConfig.load() if (!themeConfig.backgroundImageLoaded && !themeConfig.preventBackgroundRefresh) { @@ -618,8 +627,13 @@ fun Modifier.blurEffect( } return LocalBlurState.current?.let { backdrop -> + // 0.8f like haze, for material design without custom background enable + val blurTintAlpha = if (cardConfig.isCustomBackgroundEnabled) + cardConfig.cardAlpha + else 0.8f + val blendColor = - MaterialTheme.colorScheme.surfaceContainer.copy(alpha = cardConfig.cardAlpha) + MaterialTheme.colorScheme.surfaceContainer.copy(alpha = blurTintAlpha) this.then( Modifier diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index a6eda5888..99e2f3e26 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -208,6 +208,8 @@ 旋转角度 启用模糊 对此 App 启用模糊处理 + 悬浮底栏 + 使用 Apple 风格的悬浮底栏 将自定义背景渲染到模糊 实验性功能,后果自负 从自定义背景图片中取色 diff --git a/manager/app/src/main/res/values-zh-rHK/strings.xml b/manager/app/src/main/res/values-zh-rHK/strings.xml index 4845e60bd..382d1d98e 100644 --- a/manager/app/src/main/res/values-zh-rHK/strings.xml +++ b/manager/app/src/main/res/values-zh-rHK/strings.xml @@ -392,6 +392,8 @@ 旋轉角度 啟用模糊 為此應用程式啟用模糊處理 + 懸浮底部欄 + 使用 Apple 風格懸浮底部欄 繪製自訂背景以實現模糊效果 實驗性功能,使用風險自負 從自訂背景選取顏色 diff --git a/manager/app/src/main/res/values-zh-rTW/strings.xml b/manager/app/src/main/res/values-zh-rTW/strings.xml index a3cb85dd2..1c6317f4c 100644 --- a/manager/app/src/main/res/values-zh-rTW/strings.xml +++ b/manager/app/src/main/res/values-zh-rTW/strings.xml @@ -197,6 +197,8 @@ 選擇一張圖片作為應用程式背景 啟用模糊 對這個 App 啟用模糊處理 + 浮動底欄 + 使用 Apple 風格的浮動底欄 模糊渲染自定義背景 實驗性功能,請自行承擔風險 從自定義背景影像中取色 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 3de262274..2ba899e34 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -545,4 +545,6 @@ Version: %1$s (%2$d)\nArchitecture: %3$s Check beta updates Auto-check beta builds from the main branch + Floating bottom bar + Use Apple style floating bottom bar. From 3576e6a5255fc880c00f6a3175d89268a6d942be Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Mon, 14 Sep 2026 16:01:50 +0200 Subject: [PATCH 25/34] manager: update translations from Weblate (#404) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translations updated in [Hosted Weblate](https://hosted.weblate.org) for [ReSukiSU/ReSukiSU](https://hosted.weblate.org/projects/resukisu/resukisu/). Translation status: ![Weblate translation status](https://hosted.weblate.org/widget/resukisu/resukisu/matrix-auto.svg) Co-authored-by: Adriel Conceição Co-authored-by: AlexLiuDev233 Co-authored-by: DTINH Co-authored-by: Deleted User Co-authored-by: Faisal AlDossary (2B-4G10) Co-authored-by: Lynx Virgous Co-authored-by: NicosXRus Co-authored-by: PifGadget Co-authored-by: kuklux Co-authored-by: naosil Co-authored-by: sarreos Co-authored-by: tilla2 Co-authored-by: Влад Плотников Co-authored-by: 愛莉希雅 <1138745761@qq.com> --- .../app/src/main/res/values-fr/strings.xml | 90 +++++++++++-------- .../app/src/main/res/values-hu/strings.xml | 9 ++ .../app/src/main/res/values-ru/strings.xml | 16 ++-- .../app/src/main/res/values-uk/strings.xml | 9 ++ .../app/src/main/res/values-vi/strings.xml | 1 + .../src/main/res/values-zh-rCN/strings.xml | 2 +- 6 files changed, 81 insertions(+), 46 deletions(-) diff --git a/manager/app/src/main/res/values-fr/strings.xml b/manager/app/src/main/res/values-fr/strings.xml index ce56ec83d..60f7f7164 100644 --- a/manager/app/src/main/res/values-fr/strings.xml +++ b/manager/app/src/main/res/values-fr/strings.xml @@ -6,7 +6,7 @@ Appuyer ici pour installer En cours d\'exécution Non pris en charge - Aucun pilote KernelSU détecté dans le noyau installé sur cet appareil, pas le bon noyau ? + Aucun pilote KernelSU détecté dans le noyau installé sur cet appareil, pas le bon noyau ? Noyau Version de SuSFS Version du gestionnaire @@ -31,9 +31,9 @@ Redémarrer en mode téléchargement Redémarrer en mode EDL À propos - Désinstaller le module %s ? + Désinstaller le module %s ? %s a été désinstallé - Échec de la désinstallation : %s + Échec de la désinstallation : %s Auteur Afficher les applications système Envoi des journaux @@ -41,7 +41,7 @@ Continuer (%1$d) Jailbreak automatique Redémarrer pour appliquer les modifications - Modules indisponibles en raison d\'un conflit avec Magisk ! + Modules indisponibles en raison d\'un conflit avec Magisk ! Découvrir KernelSU Découvrir comment installer KernelSU et utiliser les modules Nous soutenir @@ -81,7 +81,7 @@ Importer/exporter Importer à partir du presse-papiers Exporter vers le presse-papiers - Aucun modèle local à exporter ! + Aucun modèle local à exporter ! Importation réussie Synchroniser les modèles en ligne Échec de l\'enregistrement du modèle @@ -92,7 +92,7 @@ Installation directe (recommandé) Sélectionner un fichier d\'image à modifier Installer dans l\'emplacement inactif (après OTA) - Le démarrage de cet appareil sera **FORCÉ** sur l\'emplacement inactif actuel après un redémarrage ! \nN\'utiliser cette option qu\'une fois la mise à jour OTA terminée. \nContinuer ? + Le démarrage de cet appareil sera **FORCÉ** sur l\'emplacement inactif actuel après un redémarrage ! \nN\'utiliser cette option qu\'une fois la mise à jour OTA terminée. \nContinuer ? Suivant Image de la partition %1$s recommandée Sélectionner une KMI @@ -102,7 +102,7 @@ Restaurer l\'image d\'origine Désinstalle KernelSU temporairement et rétablit l\'état d\'origine au prochain redémarrage Désinstallation complète et permanente de KernelSU (root et tous les modules) - Restaure l\'image d\'usine (s\'il en existe une sauvegarde). Généralement utilisé avant une mise à jour OTA ; pour désinstaller KernelSU, utiliser plutôt l\'option \"Désinstaller définitivement\" + Restaure l\'image d\'usine (s\'il en existe une sauvegarde). Généralement utilisé avant une mise à jour OTA ; pour désinstaller KernelSU, utiliser plutôt l\'option \"Désinstaller définitivement\" Flashage en cours Flashage réussi Échec du flashage @@ -113,11 +113,11 @@ Confirmer Annuler Sauvegarde de la liste d\'autorisations root réussie - Échec de la sauvegarde de la liste d\'autorisations root : %1$s + Échec de la sauvegarde de la liste d\'autorisations root : %1$s Confirmer la restauration de la liste d\'autorisations root - Cette opération écrasera la liste des applications autorisées à s\'exécuter en tant que root actuelle. Continuer ? + Cette opération écrasera la liste des applications autorisées à s\'exécuter en tant que root actuelle. Continuer ? Restauration de la liste d\'autorisations root réussie - Échec de la restauration de la liste d\'autorisations root : %1$s + Échec de la restauration de la liste d\'autorisations root : %1$s Sauvegarder la liste d\'autorisations root Restaurer la liste d\'autorisations root Arrière-plan personnalisé @@ -126,7 +126,7 @@ Modèle de l\'appareil Octroi des privilèges superutilisateur à %s non autorisé Commande su classique - Permet aux applications ayant l\'autorisation superutilisateur dans le profil d\'application d\'obtenir un shell superutilisateur en exécutant /system/bin/su ; effectif uniquement pour les nouveaux processus. + Permet aux applications ayant l\'autorisation superutilisateur dans le profil d\'application d\'obtenir un shell superutilisateur en exécutant /system/bin/su ; effectif uniquement pour les nouveaux processus. Démontage du noyau Comportement de démontage des modules au niveau du noyau contrôlé par KernelSU dans le profil d\'application Mode simplifié @@ -150,7 +150,7 @@ Flashage terminé Sélection de l\'emplacement de flashage Sélectionner l\'emplacement cible pour le flashage de l\'image de démarrage - Emplacement sélectionné : %1$s + Emplacement sélectionné : %1$s Échec de la copie Erreur inconnue Échec du flashage @@ -176,7 +176,7 @@ Personnalisable Appliquer les paramètres de densité Modification de la densité d\'affichage - Passer la densité d\'affichage de l\'application de %1$d PPP à %2$d PPP ? + Passer la densité d\'affichage de l\'application de %1$d PPP à %2$d PPP ? Langue de l\'application Langue du système code d\'erreur @@ -218,20 +218,20 @@ Licences open source Affiche la liste des bibliothèques tierces en open source et leurs licences Consulter le site - Licence : %s + Licence : %s Aucun texte de licence disponible. - Désinstaller le module %s ? Cette action affectera tous les modules et certaines fonctionnalités fournies par le métamodule (comme le montage de volumes) ne seront plus disponibles. + Désinstaller le module %s ? Cette action affectera tous les modules et certaines fonctionnalités fournies par le métamodule (comme le montage de volumes) ne seront plus disponibles. Version Mode jailbreak Jailbreak Le jailbreak a peut-être échoué, consulter les journaux - Appareil en **mode jailbreak**. Flasher une partition sur un appareil avec un **bootloader verrouillé** désactivera AVB (Android Verified Boot) et pourrait **empêcher le démarrage** de l\'appareil.\n\nVérifier que le bootloader de cet appareil est déverrouillé avant de continuer ! + Appareil en **mode jailbreak**. Flasher une partition sur un appareil avec un **bootloader verrouillé** désactivera AVB (Android Verified Boot) et pourrait **empêcher le démarrage** de l\'appareil.\n\nVérifier que le bootloader de cet appareil est déverrouillé avant de continuer ! Utilise automatiquement Magica pour l\'élévation de privilèges lorsque SELinux en mode permissif est détecté au démarrage. Nécessite l\'autorisation de démarrage automatique pour cette application. Exécute le démon adbd avec les privilèges root Dissimulation des modifications SELinux Empêche les applications de détecter les modifications SELinux Redémarrer pour appliquer les modifications - Erreur : %d + Erreur : %d Espace de noms de montage Hérité Global @@ -252,7 +252,7 @@ Recherche automatiquement les mises à jour du gestionnaire Recherche des mises à jour de modules Recherche automatiquement les mises à jour des modules installés - Ceci est une version de débogage. Ne PAS utiliser en production ! + Ceci est une version de débogage. Ne PAS utiliser en production ! Sélectionner la partition Utiliser un fichier local d\'image LKM Seuls les fichier .ko sont pris en charge @@ -276,7 +276,7 @@ Affiche une ombre autour du texte Transparence de la carte Fonctionnalité non prise en charge par le noyau - Emplacement actuel du système par défaut : %1$s + Emplacement actuel du système par défaut : %1$s Densité d\'affichage définie à %1$d PPP Ajustement de la luminosité de l\'arrière-plan https://kernelsu.org/guide/what-is-kernelsu.html @@ -319,10 +319,10 @@ Affiche des informations complémentaires sur les modules telles que les URL des fichiers JSON de mise à jour Ajouter Configuration de Kstat - - add_sus_kstat_statically : Statistiques statiques des fichiers/répertoires - - add_sus_kstat : Ajoute le chemin avant le montage de liaison, en stockant les statistiques d\'origine - - update_sus_kstat : Met à jour l\'inode cible, conserve la taille et le nombre de blocs - - update_sus_kstat_full_clone : Met à jour l\'inode uniquement, conserve les autres valeurs d\'origine + - add_sus_kstat_statically : Statistiques statiques des fichiers/répertoires + - add_sus_kstat : Ajoute le chemin avant le montage de liaison, en stockant les statistiques d\'origine + - update_sus_kstat : Met à jour l\'inode cible, conserve la taille et le nombre de blocs + - update_sus_kstat_full_clone : Met à jour l\'inode uniquement, conserve les autres valeurs d\'origine Crée une sauvegarde de toutes les configurations de SuSFS. Le fichier de sauvegarde contiendra tous les paramètres, chemins de fichiers/répertoires, et configurations Restaurer Restaure les configurations de SuSFS depuis un fichier de sauvegarde. Tous les paramètres actuels seront écrasés @@ -330,7 +330,7 @@ Rechercher des applications Rechercher des modules Configuration du gestionnaire dynamique - Activé (Taille : %s) + Activé (Taille : %s) Désactivé Taille de la signature du gestionnaire dynamique Hachage de la signature du gestionnaire dynamique @@ -363,22 +363,22 @@ Appareils pris en charge Versions Informations d\'états - Superutilisateur : %1$d, Modules : %2$d + Superutilisateur : %1$d, Modules : %2$d Version du pilote de noyau Aucun contenu trouvé Nouvelle version bêta %1$d disponible, cliquer ici pour l\'installer Échec de recherche de version bêta. Faire glisser votre doigt vers le bas pour actualiser et réessayer. Mise à jour de version stable Mise à jour de version bêta - Version : %1$s (%2$d)\nArchitecture : %3$s + Version : %1$s (%2$d)\nArchitecture : %3$s Recherche de mises à jour de version bêta du gestionnaire Recherche automatiquement les versions bêta du gestionnaire depuis la branche principale du dépôt GitHub Hors connexion Réessayer Vérifier la connexion Internet et réessayer - Installer le module %s ? + Installer le module %s ? Sélectionner les éléments à installer - Taille : %1$s, Téléchargés : %2$s + Taille : %1$s, Téléchargés : %2$s Aucun élément sélectionné Installé Ouvrir la page d\'accueil du module dans le navigateur @@ -411,9 +411,9 @@ Annuler Installer Autorisation d\'affichage des notifications requise pour afficher la progression de téléchargement. - Échec du téléchargement : Autorisation d\'affichage des notifications requise + Échec du téléchargement : Autorisation d\'affichage des notifications requise Autorisation d\'écriture sur stockage externe requise pour écrire le fichier. - Échec du téléchargement : Autorisation d\'écriture sur stockage externe requise + Échec du téléchargement : Autorisation d\'écriture sur stockage externe requise Gestion de la signature du gestionnaire dynamique Configuration actuelle Configuration manuelle de la signature @@ -421,10 +421,10 @@ Effacer la configuration Désactive et supprime la configuration du gestionnaire dynamique %1$s\ngéré par %2$s - Confirmer l\'autorisation ? + Confirmer l\'autorisation ? L\'application sélectionnée deviendra le gestionnaire dynamique. Si un gestionnaire dynamique est déjà configuré, il sera immédiatement invalidé.\n\nCette action va lui octroyer le niveau d\'autorisation le plus élevé pour cet appareil. Si vous ne comprenez pas ce que cela signifie, annulez. Effacement de la configuration du gestionnaire dynamique - Effacer la configuration du gestionnaire dynamique ? + Effacer la configuration du gestionnaire dynamique ? Gestionnaires dynamiques Gestion des chemins de démontage Gère les chemins de démontage du noyau @@ -434,7 +434,7 @@ Drapeaux de démontage 0 = démontage normal, 2 = MNT_DETACH Confirmer la suppression - Supprimer le chemin %s ? + Supprimer le chemin %s ? Chemin de démontage ajouté Chemin de démontage supprimé Échec de l\'opération @@ -444,8 +444,8 @@ non monté car le métamodule est en cours de désinstallation non monté car le métamodule n\'est pas installé Masque le fichier réel mappé en mémoire de divers mappings dans /proc/self/ - Masque les chemins réels des fichiers associés aux mappages mémoire dans /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Remarque : cette fonctionnalité ne permet pas de masquer les mappages mémoire anonymes, ni les hooks d\'interception \"inline\" ou PLT générés par la bibliothèque injectée elle-même - Avertissement important : pour les applications dotées de mécanismes de détection d’injection bien implémentés, cette fonctionnalité peut ne pas contourner efficacement la détection + Masque les chemins réels des fichiers associés aux mappages mémoire dans /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Remarque : cette fonctionnalité ne permet pas de masquer les mappages mémoire anonymes, ni les hooks d\'interception \"inline\" ou PLT générés par la bibliothèque injectée elle-même + Avertissement important : pour les applications dotées de mécanismes de détection d’injection bien implémentés, cette fonctionnalité peut ne pas contourner efficacement la détection Commencer par identifier le PID et l\'UID de l\'application cible à l\'aide de la commande **ps -enf**, puis vérifier les chemins correspondants dans /proc/<pid>/maps et comparer les numéros de périphériques avec ceux figurant dans /proc/1/mountinfo pour garantir la cohérence. La fonctionnalité de masquage du mappage ne peut fonctionner correctement que si les numéros de périphériques correspondent Slot A Slot B @@ -477,7 +477,7 @@ Informations d\'emplacements de démarrage non trouvées Fichiers cmdline/bootconfig Chemin d\'accès aux faux fichiers cmdline et bootconfig - Actuel : %1$s + Actuel : %1$s Non défini Ajouter une entrée Aucune entrée @@ -490,7 +490,7 @@ %1$d importés, %2$d échecs Importer depuis un fichier Échec de lecture du fichier - Fichier incorrect : ne contient pas de texte au format UTF-8 + Fichier incorrect : ne contient pas de texte au format UTF-8 Chemin SUS normal Chemin de boucles SUS Boucle @@ -504,7 +504,7 @@ Chemin cible Schéma d\'UID Ouverture de la redirection du chemin cible vers un chemin défini par l\'utilisateur pour les processus correspondant au schéma d\'UID sélectionné. - Schémas d\'UID:\n0 : Processus non-applicatifs (UID < 10000)\n1 : Processus root à UID 0 en dehors du domaine SU\n2 : Tous les processus non-SU (utiliser avec précaution)\n3 : Processus des applications non montées avec UID ≥ 10000 (utiliser avec précaution)\n4 : Tous les processus non montés, y compris la plupart des processus créés par init (utiliser avec précaution) + Schémas d\'UID:\n0 : Processus non-applicatifs (UID < 10000)\n1 : Processus root à UID 0 en dehors du domaine SU\n2 : Tous les processus non-SU (utiliser avec précaution)\n3 : Processus des applications non montées avec UID ≥ 10000 (utiliser avec précaution)\n4 : Tous les processus non montés, y compris la plupart des processus créés par init (utiliser avec précaution) %1$s · %2$s Non applicatif Root sauf SU @@ -523,10 +523,22 @@ Échec de l\'exportation de la configuration Échec de l\'importation de la configuration Confirmation de l\'importation - Cette opération écrasera la configuration SuSFS actuelle. Continuer ? + Cette opération écrasera la configuration SuSFS actuelle. Continuer ? Importer Configuration par défaut Supprime la configuration SuSFS actuelle et restaure la configuration par défaut Remarques importantes :\n• Les chemins cible et redirigé doivent exister avant l’ajout d’une entrée\n• les permissions SELinux pour les deux chemins doivent être configurées.\n• La redirection affecte uniquement les processus correspondant au schéma d\'UID sélectionné. + Badges de la barre de navigation + Affiche le nombre d\'applications superutilisateur et de modules dans la barre de navigation + Icônes dans le panneau d\'accueil + Ajoute des icônes aux cartes du panneau d\'accueil + Par nom + Par date d\'installation + Par date de mise à jour + Par taille + Ordre inverse + Mise à jour du gestionnaire requise + Mise à jour du noyau requise. Appuyer ici pour installer. + Mise à jour du noyau requise diff --git a/manager/app/src/main/res/values-hu/strings.xml b/manager/app/src/main/res/values-hu/strings.xml index bd52c13ed..dc2635a36 100644 --- a/manager/app/src/main/res/values-hu/strings.xml +++ b/manager/app/src/main/res/values-hu/strings.xml @@ -532,4 +532,13 @@ Root menedzser frissítése szükséges Kernel frissítése szükséges. Kattints a telepítéshez. Kernel frissítése szükséges + Kezdőlap kártyáinak ikonosítása + Ikonok megjelenítése a kezdőlap kártyáin + Superuser folyamatok és modulok számának megjelenítése a navigációs sávon + Bélyegek a navigációs sávon + Méret szerint + Fordított sorrendben + Legutóbbi frissítés ideje szerint + Telepítés ideje szerint + Név szerint diff --git a/manager/app/src/main/res/values-ru/strings.xml b/manager/app/src/main/res/values-ru/strings.xml index dc6c01422..8ceac88ae 100644 --- a/manager/app/src/main/res/values-ru/strings.xml +++ b/manager/app/src/main/res/values-ru/strings.xml @@ -57,8 +57,8 @@ Перезагрузить в Download Перезагрузить в EDL О приложении - Проверить исходный код - Проверить исходный код на GitHub + Изучить исходный код + Изучить исходный код на GitHub Присоединиться к сообществу Присоединиться к нашему Telegram-сообществу Open Source лицензия @@ -147,7 +147,7 @@ Затрагиваемые приложения Не удалось получить список изменений: %s Не удалось выдать root! - Это debug-сборка из PR. НЕ используйте в продакшене! + Это debug-сборка из PR. Не рекомендуется для публичного релиза Действие Закрыть Прямая установка (Рекомендуется) @@ -219,7 +219,7 @@ Использовать акцентные цвета системы Выберите цвет темы Установка AnyKernel3 - Прошить файл ядра AnyKernel3 + Прошить архив AnyKernel3 Требуется root-права Не удалось перезагрузиться Персонализация @@ -422,7 +422,7 @@ Стиль палитры Спецификация цвета AOSP - Ридми + Прочти меня Статус Стандарт SUS путь @@ -522,7 +522,7 @@ Бета обновление Версия: %1$s (%2$d)\nАрхитектура: %3$s Проверка бета обновлений - Автоматически проверять бета-сборки из главной ветки + Автоматически проверять бета-сборки из основной ветки Использовать встроенный моноширинный шрифт Использовать шрифт JetBrains Mono для отображения логов во избежание проблем с системным моноширинным шрифтом менеджер SUSFS @@ -532,4 +532,8 @@ Требуется обновление менеджера Требуется обновление ядра. Нажмите, чтобы установить. Требуется обновление ядра + Добавить иконки на информационные карточки главного экрана + Показывать иконки на карточках главного экрана + Показывать количество суперпользователь/модуль в панели навигации + Значки панели навигации diff --git a/manager/app/src/main/res/values-uk/strings.xml b/manager/app/src/main/res/values-uk/strings.xml index 5fa4ed8d3..9a10f5132 100644 --- a/manager/app/src/main/res/values-uk/strings.xml +++ b/manager/app/src/main/res/values-uk/strings.xml @@ -532,4 +532,13 @@ Потрібне оновлення ядра Потрібне оновлення менеджера Потрібне оновлення ядра. Натисніть, щоб встановити. + Додати іконки до інформаційних карток на головному екрані + Показувати іконки на картках головного екрана + Показувати кількість прав суперкористувача / модулів на панелі навігації + Позначки на панелі навігації + Час встановлення + Час оновлення + Розмір + Зворотний порядок + Ім\'я diff --git a/manager/app/src/main/res/values-vi/strings.xml b/manager/app/src/main/res/values-vi/strings.xml index 5c187e5ed..255c0553f 100644 --- a/manager/app/src/main/res/values-vi/strings.xml +++ b/manager/app/src/main/res/values-vi/strings.xml @@ -397,4 +397,5 @@ Cờ Chọn 1 phương thức crop Cắt ảnh thất bại + diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index 99e2f3e26..e83aefbb9 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -380,7 +380,7 @@ 重要提示:对于具备完善注入检测机制的应用,此功能可能无法有效绕过检测 首先通过 ps -enf 查找目标应用的 PID 和 UID,然后检查 /proc/<pid>/maps 中的相关路径,并与 /proc/1/mountinfo 中的设备号进行比对以确保一致性。只有当设备号一致时,隐藏映射才能正常工作 需要更新管理器 - 需要更新内核,点击此处安装 + 需要更新内核,点击此处安装。 需要更新内核 Umount 路径管理 管理内核卸载路径 From a5bb612a090edbd77b2f67d30b5568cda39900ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?YC=E9=85=B1luyancib?= Date: Mon, 14 Sep 2026 22:21:50 +0800 Subject: [PATCH 26/34] ksubot: fix release --- scripts/ksubot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/ksubot.py b/scripts/ksubot.py index b2d591667..163887446 100644 --- a/scripts/ksubot.py +++ b/scripts/ksubot.py @@ -99,7 +99,7 @@ def get_caption(): commit_line=commit_line, run_url=RUN_URL, ) - if BRANCH != "main": + if BRANCH != "main" and GITHUB_REF_TYPE != "tag": msg += "\n⚠️⚠️DEV VERSION, PLEASE BACKUP BEFORE INSTALLATION⚠️⚠️" msg += "\n⚠️⚠️测试版,安装前请备份⚠️⚠️" return msg @@ -113,7 +113,7 @@ def get_caption_for_debug(): commit_line=commit_line, run_url=RUN_URL, ) - if BRANCH != "main": + if BRANCH != "main" and GITHUB_REF_TYPE != "tag": msg += "\n⚠️⚠️DEV VERSION, PLEASE BACKUP BEFORE INSTALLATION⚠️⚠️" msg += "\n⚠️⚠️测试版,安装前请备份⚠️⚠️" return msg From 833edb0e8e4bc11ac8e976edd7de42da6bdc5bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?YC=E9=85=B1luyancib?= Date: Mon, 14 Sep 2026 23:27:57 +0800 Subject: [PATCH 27/34] manager: Restrict floating bottom bar to Android 12+ The floating bottom bar feature is now only enabled on Android 12 and newer. This prevents unsupported rendering and configuration on older devices by guarding both the navigation bar implementation and the theme settings toggle. --- .../resukisu/resukisu/ui/activity/component/NavigationBar.kt | 3 ++- .../resukisu/resukisu/ui/screen/themeSettings/ThemeSettings.kt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/activity/component/NavigationBar.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/activity/component/NavigationBar.kt index 4f7be090c..7ee8200c4 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/activity/component/NavigationBar.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/activity/component/NavigationBar.kt @@ -1,6 +1,7 @@ package com.resukisu.resukisu.ui.activity.component import android.annotation.SuppressLint +import android.os.Build import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -76,7 +77,7 @@ fun NavigationBar( val handlePageChange = LocalHandlePageChange.current val pagerState = LocalPagerState.current - if (isBottomBar && themeConfig.bottomBarStyle == BottomBarStyle.FLOATING) { + if (isBottomBar && themeConfig.bottomBarStyle == BottomBarStyle.FLOATING && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { Box( modifier = Modifier .fillMaxWidth() diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/ThemeSettings.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/ThemeSettings.kt index db2bb8a26..5ca6b1637 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/ThemeSettings.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/ThemeSettings.kt @@ -621,7 +621,7 @@ private fun AppearanceSettings( ) } - item(visible = isPortrait) { + item(visible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && isPortrait) { SettingsSwitchWidget( icon = Icons.TwoTone.Dock, title = stringResource(R.string.enable_floating_bottom_bar), From e3fcb183e1b2f9804a4e9796a318705e08669a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?YC=E9=85=B1luyancib?= Date: Tue, 15 Sep 2026 12:28:50 +0800 Subject: [PATCH 28/34] manager: fix cannot following showNavigationBarBadge when floatingbottombar enabled --- .../resukisu/resukisu/ui/activity/component/NavigationBar.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/activity/component/NavigationBar.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/activity/component/NavigationBar.kt index 7ee8200c4..ee8dd3ab8 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/activity/component/NavigationBar.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/activity/component/NavigationBar.kt @@ -115,7 +115,7 @@ fun NavigationBar( tint = contentColor ) } - if (count > 0) { + if (count > 0 && showNavigationBarBadge) { BadgedBox(badge = { Badge { Text(count.toString()) } }) { icon() } } else { icon() From b22a46e6ee79931b1e3b39fc562d56176936ab77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?YC=E9=85=B1luyancib?= Date: Tue, 15 Sep 2026 12:47:19 +0800 Subject: [PATCH 29/34] ci: Fix setup Android SDK ref: https://github.com/android-actions/setup-android/issues/537 --- .github/workflows/build-manager.yml | 2 ++ .github/workflows/lints-check.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/build-manager.yml b/.github/workflows/build-manager.yml index 3949a7bd6..0a9c3555c 100644 --- a/.github/workflows/build-manager.yml +++ b/.github/workflows/build-manager.yml @@ -186,6 +186,8 @@ jobs: - name: Setup Android SDK uses: android-actions/setup-android@v4 + with: + packages: "platform-tools" - name: Build APK run: | diff --git a/.github/workflows/lints-check.yml b/.github/workflows/lints-check.yml index e8bbfa688..0fc701f58 100644 --- a/.github/workflows/lints-check.yml +++ b/.github/workflows/lints-check.yml @@ -126,6 +126,8 @@ jobs: - name: Setup Android SDK uses: android-actions/setup-android@v4 + with: + packages: "platform-tools" - name: Check Manager Lint run: | From 0b5cffd9e16b0e3bee6be967ef7a4c941896df8b Mon Sep 17 00:00:00 2001 From: YuKongA <70465933+YuKongA@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:24:10 +0800 Subject: [PATCH 30/34] fix(actions): Remove all setup-android [cherry-pick upstream commit https://github.com/tiann/KernelSU/commit/3a7ac9dc0dc8c0b573fd4eb4836fd0860ef4242e] luyanci: This commit will revert commit b22a46e6ee79931b1e3b39fc562d56176936ab77. --- .github/workflows/build-manager.yml | 5 ----- .github/workflows/codeql.yml | 3 --- .github/workflows/lints-check.yml | 5 ----- 3 files changed, 13 deletions(-) diff --git a/.github/workflows/build-manager.yml b/.github/workflows/build-manager.yml index 0a9c3555c..1ef40e771 100644 --- a/.github/workflows/build-manager.yml +++ b/.github/workflows/build-manager.yml @@ -184,11 +184,6 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v6 - - name: Setup Android SDK - uses: android-actions/setup-android@v4 - with: - packages: "platform-tools" - - name: Build APK run: | if [ "${{ github.event_name }}" == "pull_request" ]; then diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 37800c82c..a33a6a6ba 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -75,9 +75,6 @@ jobs: build-scan-publish: false cache-read-only: true - - name: Setup Android SDK - uses: android-actions/setup-android@v4 - - name: Grant execute permission for gradlew run: chmod +x ./gradlew diff --git a/.github/workflows/lints-check.yml b/.github/workflows/lints-check.yml index 0fc701f58..3e9926fbe 100644 --- a/.github/workflows/lints-check.yml +++ b/.github/workflows/lints-check.yml @@ -123,11 +123,6 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v6 - - - name: Setup Android SDK - uses: android-actions/setup-android@v4 - with: - packages: "platform-tools" - name: Check Manager Lint run: | From 6ace7210b43689f29f930e67dd3301ab66f1abe5 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Tue, 15 Sep 2026 16:17:18 +0200 Subject: [PATCH 31/34] manager: update translations from Weblate (#415) Translations updated in [Hosted Weblate](https://hosted.weblate.org) for [ReSukiSU/ReSukiSU](https://hosted.weblate.org/projects/resukisu/resukisu/). Translation status: ![Weblate translation status](https://hosted.weblate.org/widget/resukisu/resukisu/matrix-auto.svg) Co-authored-by: NicosXRus Co-authored-by: PifGadget Co-authored-by: kuklux Co-authored-by: tilla2 Co-authored-by: AlexLiuDev233 --- manager/app/src/main/res/values-fr/strings.xml | 2 ++ manager/app/src/main/res/values-hu/strings.xml | 2 ++ manager/app/src/main/res/values-ru/strings.xml | 15 +++++++++++---- manager/app/src/main/res/values-uk/strings.xml | 2 ++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/manager/app/src/main/res/values-fr/strings.xml b/manager/app/src/main/res/values-fr/strings.xml index 60f7f7164..c92b8ca1b 100644 --- a/manager/app/src/main/res/values-fr/strings.xml +++ b/manager/app/src/main/res/values-fr/strings.xml @@ -541,4 +541,6 @@ Mise à jour du gestionnaire requise Mise à jour du noyau requise. Appuyer ici pour installer. Mise à jour du noyau requise + Bar de navigation flottante + Barre de navigation flottante dans le style de celle d\'appel. diff --git a/manager/app/src/main/res/values-hu/strings.xml b/manager/app/src/main/res/values-hu/strings.xml index dc2635a36..d40608309 100644 --- a/manager/app/src/main/res/values-hu/strings.xml +++ b/manager/app/src/main/res/values-hu/strings.xml @@ -541,4 +541,6 @@ Legutóbbi frissítés ideje szerint Telepítés ideje szerint Név szerint + Lebegő alsó sáv + Apple iOS stílusú lebegő alsó sáv használata. diff --git a/manager/app/src/main/res/values-ru/strings.xml b/manager/app/src/main/res/values-ru/strings.xml index 8ceac88ae..c5bbf3561 100644 --- a/manager/app/src/main/res/values-ru/strings.xml +++ b/manager/app/src/main/res/values-ru/strings.xml @@ -12,8 +12,8 @@ Версия менеджера Состояние SELinux Отключен - Блокирующий - Предупреждающий + Принудительный + Разрешающий Статус Seccomp Не поддерживается Отключено @@ -81,7 +81,7 @@ Вы находитесь в **режиме Jailbreak**. Прошивка раздела на устройстве с **заблокированным загрузчиком** нарушит защиту AVB (Android Verified Boot) и может привести к тому, что устройство **не загрузится**.\n\nПеред тем как продолжить, убедитесь, что ваш загрузчик разблокирован! Продолжить (%1$d) Автоматический Jailbreak - Автоматически повышать права через Magica, если при загрузке обнаружен предупреждающий режим SELinux. Требуется разрешение на автозапуск приложения. + Автоматически повышать права через Magica, если при загрузке обнаружен разрешающий режим SELinux. Требуется разрешение на автозапуск приложения. Запустить сервис adbd с Root правами Скрыть модификации SELinux Предотвратить обнаружение приложениями изменений SELinux @@ -325,7 +325,7 @@ Реализация мета-модуля Конфигурация путей монтирования loop Пути цикла повторно отмечены как SUS_PATH в каждом пользовательском приложении, не являющемся root, или изолированном запуске службы. Это помогает решить проблемы, в которых добавленные пути могут иметь сброс статуса inode или повторно созданные inode в ядре. - Типы хуков + Тип хука Подтвердите установку Подтвердите установку (%d файлов) Установить @@ -536,4 +536,11 @@ Показывать иконки на карточках главного экрана Показывать количество суперпользователь/модуль в панели навигации Значки панели навигации + Имя + Время установки + Время обновления + Размер + Обратный порядок + Плавающая нижняя панель + Использовать Apple-стиль для плавающей нижней панели diff --git a/manager/app/src/main/res/values-uk/strings.xml b/manager/app/src/main/res/values-uk/strings.xml index 9a10f5132..49f7e91ee 100644 --- a/manager/app/src/main/res/values-uk/strings.xml +++ b/manager/app/src/main/res/values-uk/strings.xml @@ -541,4 +541,6 @@ Розмір Зворотний порядок Ім\'я + Плаваюча нижня панель + Використовувати плаваючу нижню панель у стилі Apple. From 620700096e100e2400070c2a0f3aece5cc26abab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:18:20 +0800 Subject: [PATCH 32/34] ksud: bump the crates group across 1 directory with 3 updates (#414) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the crates group with 3 updates in the /userspace/ksud directory: [bitflags](https://github.com/bitflags/bitflags), [bindgen](https://github.com/rust-lang/rust-bindgen) and [toml_edit](https://github.com/toml-rs/toml). Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: AlexLiuDev233 Co-authored-by: YC酱luyancib --- userspace/ksud/Cargo.lock | 48 +++++++++++++++++++-------------------- userspace/ksud/Cargo.toml | 2 +- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/userspace/ksud/Cargo.lock b/userspace/ksud/Cargo.lock index b81108969..d7b70ed73 100644 --- a/userspace/ksud/Cargo.lock +++ b/userspace/ksud/Cargo.lock @@ -165,11 +165,11 @@ checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" [[package]] name = "bindgen" -version = "0.73.1" +version = "0.73.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54be9a169b85a1bef39252af30bb6246a31b23c7e0f6a162520dd869f8dad33c" +checksum = "787ef8ef523575546b106a58213d6e6b06198a05c2f757258c68a74273670cfa" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cexpr", "clang-sys", "log", @@ -190,9 +190,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "block-buffer" @@ -247,9 +247,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" +checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" dependencies = [ "find-msvc-tools", "jobserver", @@ -309,9 +309,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" dependencies = [ "clap_builder", "clap_derive", @@ -319,9 +319,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" dependencies = [ "anstream", "anstyle", @@ -331,9 +331,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.4" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" dependencies = [ "heck", "proc-macro2", @@ -343,9 +343,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" [[package]] name = "colorchoice" @@ -412,9 +412,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" dependencies = [ "cfg-if", ] @@ -971,7 +971,7 @@ dependencies = [ "anyhow", "base16ct", "bindgen", - "bitflags 2.13.1", + "bitflags 2.13.2", "cc", "chrono", "clap", @@ -1546,7 +1546,7 @@ name = "rustix" version = "0.38.34" source = "git+https://github.com/ReSukiSU/rustix.git?rev=4a53fbc#4a53fbc7cb7a07cabe87125cc21dbc27db316259" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "errno 0.3.14", "libc", "linux-raw-sys 0.4.15", @@ -1559,7 +1559,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "errno 0.3.14", "libc", "linux-raw-sys 0.12.1", @@ -1828,9 +1828,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.13+spec-1.1.0" +version = "0.25.15+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" dependencies = [ "indexmap", "toml_datetime", @@ -2171,9 +2171,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" [[package]] name = "zmij" diff --git a/userspace/ksud/Cargo.toml b/userspace/ksud/Cargo.toml index 9ae76dfcd..43c1eedf9 100644 --- a/userspace/ksud/Cargo.toml +++ b/userspace/ksud/Cargo.toml @@ -70,5 +70,5 @@ lto = true codegen-units = 1 [build-dependencies] -bindgen = "0.73.1" +bindgen = "0.73.2" cc = "1" From 60954ce374e6e052b721d249289542c3dacad1b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:18:59 +0800 Subject: [PATCH 33/34] ksuinit: bump bitflags from 2.13.1 to 2.13.2 in /userspace/ksuinit in the crates group across 1 directory (#408) Bumps the crates group with 1 update in the /userspace/ksuinit directory: [bitflags](https://github.com/bitflags/bitflags). Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: AlexLiuDev233 --- userspace/ksuinit/Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/userspace/ksuinit/Cargo.lock b/userspace/ksuinit/Cargo.lock index 136cef58d..d84cacac9 100644 --- a/userspace/ksuinit/Cargo.lock +++ b/userspace/ksuinit/Cargo.lock @@ -10,9 +10,9 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "errno" From 6d674e50a022a85076dcfe4498af9a8bfead2cf9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:46:27 +0800 Subject: [PATCH 34/34] build(deps): bump the maven group in /manager with 13 updates (#410) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the maven group in /manager with 13 updates: | Package | From | To | | --- | --- | --- | | [com.materialkolor:material-kolor](https://github.com/jordond/materialkolor) | `5.0.0` | `5.0.1` | | androidx.compose:compose-bom | `2026.08.00` | `2026.09.00` | | androidx.compose.material3:material3 | `1.5.0-alpha27` | `1.5.0-alpha28` | | androidx.benchmark:benchmark-macro-junit4 | `1.5.0-rc02` | `1.5.0` | | androidx.baselineprofile | `1.5.0-rc02` | `1.5.0` | | [com.mikepenz:aboutlibraries-core](https://github.com/mikepenz/AboutLibraries) | `15.1.1` | `15.2.0` | | [com.mikepenz:aboutlibraries-compose-m3](https://github.com/mikepenz/AboutLibraries) | `15.1.1` | `15.2.0` | | com.mikepenz.aboutlibraries.plugin | `15.1.1` | `15.2.0` | | com.android.application | `9.3.2` | `9.4.0` | | com.android.test | `9.3.2` | `9.4.0` | | [org.jetbrains.kotlin.android](https://github.com/JetBrains/kotlin) | `2.4.10` | `2.4.20` | | [org.jetbrains.kotlin.plugin.compose](https://github.com/JetBrains/kotlin) | `2.4.10` | `2.4.20` | | [org.jetbrains.kotlin.plugin.serialization](https://github.com/JetBrains/kotlin) | `2.4.10` | `2.4.20` | Updates `com.materialkolor:material-kolor` from 5.0.0 to 5.0.1
Release notes

Sourced from com.materialkolor:material-kolor's releases.

5.0.1

What's Changed

Dependencies

Full Changelog: https://github.com/jordond/MaterialKolor/compare/5.0.0...5.0.1

Commits
  • cc4d2cb fix: stop overriding consumer material3 on Android (#529)
  • d52e22f chore(deps): update actions/checkout action to v7 (#528)
  • 5fa8702 clean up gradle files
  • 21e9129 chore(deps): update actions/setup-java action to v6 (#518)
  • 1018332 fix(deps): update builder dependencies (#520)
  • 3f97fa2 Migrate MaterialKolorBuilder (#500)
  • 7154b68 switch to temurin
  • 78279e8 chore(deps): update dependency com.diffplug.spotless to v8.10.1 (#519)
  • 632f1ee chore(deps): update dependency com.android.kotlin.multiplatform.library to v9...
  • 213e6f2 fix(deps): update compose.multiplatform to v1.12.0 (#517)
  • Additional commits viewable in compare view

Updates `androidx.compose:compose-bom` from 2026.08.00 to 2026.09.00 Updates `androidx.compose.material3:material3` from 1.5.0-alpha27 to 1.5.0-alpha28 Updates `androidx.benchmark:benchmark-macro-junit4` from 1.5.0-rc02 to 1.5.0 Updates `androidx.baselineprofile` from 1.5.0-rc02 to 1.5.0 Updates `com.mikepenz:aboutlibraries-core` from 15.1.1 to 15.2.0
Release notes

Sourced from com.mikepenz:aboutlibraries-core's releases.

15.2.0

🚀 Features

  • chore(deps): update dependencies (Compose 1.12.0)

💬 Other

  • chore(deps): update github/codeql-action action to v4.37.7
  • fix(deps): update dependency com.squareup.okhttp3:okhttp to v5.5.0
  • chore(deps): update dependencies (Compose 1.12.0)

Contributors:

Commits
  • 0e252a5 Merge pull request #1461 from mikepenz/develop
  • 916754a - [release] v15.2.0
  • f16f582 Merge pull request #1460 from mikepenz/chore/compose-1.12.0-upgrade
  • f0aaff9 docs: reference v15.2.0 for Compose 1.12.x upgrade
  • af09963 chore(deps): update dependencies
  • e861427 Merge pull request #1458 from mikepenz/renovate/okhttp
  • 5f4eeba fix(deps): update dependency com.squareup.okhttp3:okhttp to v5.5.0
  • b7a0dfd chore(deps): update github/codeql-action action to v4.37.7 (#1457)
  • See full diff in compare view

Updates `com.mikepenz:aboutlibraries-compose-m3` from 15.1.1 to 15.2.0
Release notes

Sourced from com.mikepenz:aboutlibraries-compose-m3's releases.

15.2.0

🚀 Features

  • chore(deps): update dependencies (Compose 1.12.0)

💬 Other

  • chore(deps): update github/codeql-action action to v4.37.7
  • fix(deps): update dependency com.squareup.okhttp3:okhttp to v5.5.0
  • chore(deps): update dependencies (Compose 1.12.0)

Contributors:

Commits
  • 0e252a5 Merge pull request #1461 from mikepenz/develop
  • 916754a - [release] v15.2.0
  • f16f582 Merge pull request #1460 from mikepenz/chore/compose-1.12.0-upgrade
  • f0aaff9 docs: reference v15.2.0 for Compose 1.12.x upgrade
  • af09963 chore(deps): update dependencies
  • e861427 Merge pull request #1458 from mikepenz/renovate/okhttp
  • 5f4eeba fix(deps): update dependency com.squareup.okhttp3:okhttp to v5.5.0
  • b7a0dfd chore(deps): update github/codeql-action action to v4.37.7 (#1457)
  • See full diff in compare view

Updates `com.mikepenz.aboutlibraries.plugin` from 15.1.1 to 15.2.0 Updates `com.mikepenz:aboutlibraries-compose-m3` from 15.1.1 to 15.2.0
Release notes

Sourced from com.mikepenz:aboutlibraries-compose-m3's releases.

15.2.0

🚀 Features

  • chore(deps): update dependencies (Compose 1.12.0)

💬 Other

  • chore(deps): update github/codeql-action action to v4.37.7
  • fix(deps): update dependency com.squareup.okhttp3:okhttp to v5.5.0
  • chore(deps): update dependencies (Compose 1.12.0)

Contributors:

Commits
  • 0e252a5 Merge pull request #1461 from mikepenz/develop
  • 916754a - [release] v15.2.0
  • f16f582 Merge pull request #1460 from mikepenz/chore/compose-1.12.0-upgrade
  • f0aaff9 docs: reference v15.2.0 for Compose 1.12.x upgrade
  • af09963 chore(deps): update dependencies
  • e861427 Merge pull request #1458 from mikepenz/renovate/okhttp
  • 5f4eeba fix(deps): update dependency com.squareup.okhttp3:okhttp to v5.5.0
  • b7a0dfd chore(deps): update github/codeql-action action to v4.37.7 (#1457)
  • See full diff in compare view

Updates `com.android.application` from 9.3.2 to 9.4.0 Updates `com.android.test` from 9.3.2 to 9.4.0 Updates `com.android.test` from 9.3.2 to 9.4.0 Updates `androidx.baselineprofile` from 1.5.0-rc02 to 1.5.0 Updates `org.jetbrains.kotlin.android` from 2.4.10 to 2.4.20
Release notes

Sourced from org.jetbrains.kotlin.android's releases.

Kotlin 2.4.20

Changelog

Analysis API

  • KT-86546 Check suspicious when over ConeKotlinType in ConeTypeCompatibilityChecker
  • KT-85418 Implement an API for accessing deserialized file annotations in Analysis API
  • KT-74448 K2. False positive MISSING_DEPENDENCY_SUPERCLASS in LinkedListTest.kt, kotlinx.coroutines
  • KT-85856 containingSymbol of constructor property differs for local and non-local classes
  • KT-65417 K2 IDE: KTOR false positive expect-actual matching error on enum class because of implicit clone() in non-JVM source sets

Analysis API. Code Compilation

  • KT-76457 K2 IDE / KMP Debugger: KISEWA “Cannot compile a common source without a JVM counterpart” on evaluating inline fun from common module inside jvm

Analysis API. FIR

  • KT-70552 No expects for actual
  • KT-69727 K2 IDE. Wrong error in the editor on calling clone function of actual enum instance in non-jvm platform
  • KT-69726 FP errors on declaring fun clone() in actual enum in not-jvm source-set
  • KT-86014 Types are broken after remove parameter through change signature
  • KT-86363 KotlinIllegalArgumentExceptionWithAttachments: No dangling modifier found on companion blocks
  • KT-86147 Drop kotlin.parallel.resolve.under.global.lock registry key
  • KT-85543 Avoid lazy resolve for the contracts phase if no constracts might be resolved

Analysis API. Infrastructure

  • KT-84914 Do not publish analysis-api-test-framework
  • KT-86986 kotlin-compiler-common-for-ide bundles unrelated Analysis API modules
  • KT-86186 Analysis API: Codebase tests run twice in some analysis modules — pick a single JUnit runner and migrate
  • KT-85360 Drop kotlin-compiler-testdata-for-ide artifact
  • KT-85585 Simplify the dependencies graph for the Analysis API modules
  • KT-85381 Remove tests for the FE10 implementation

Analysis API. Light Classes

New Features

  • KT-84645 Support resolving to companion block members & extensions from Java (light classes)
  • KT-80775 Support PsiClass#getRecordComponents in light classes

Fixes

  • KT-57537 SLC: propagate default parameter value from (@JvmOverloads) expect declarations to actual declarations
  • KT-85040 [Analysis API] Improve Java / Kotlin interop in KMP projects
  • KT-87301 SymbolLightAccessorMethod#isValid returns false for delegated properties
  • KT-87171 SLC: non-mapped Kotlin collection supertype is dropped from supertype list
  • KT-87250 JvmExposeBoxed: light classes shouldn't be autogenerated for private declarations
  • KT-70428 AA: good code is red when a Java class extends a Kotlin class implementing MutableList by delegation
  • KT-63568 Symbol Light Classes: KtAnnotationApplicationWithArgumentsInfo.normalizedArguments() may work incorrectly when psi is not set

... (truncated)

Changelog

Sourced from org.jetbrains.kotlin.android's changelog.

2.4.20

Analysis API

  • KT-86546 Check suspicious when over ConeKotlinType in ConeTypeCompatibilityChecker
  • KT-85418 Implement an API for accessing deserialized file annotations in Analysis API
  • KT-74448 K2. False positive MISSING_DEPENDENCY_SUPERCLASS in LinkedListTest.kt, kotlinx.coroutines
  • KT-85856 containingSymbol of constructor property differs for local and non-local classes
  • KT-65417 K2 IDE: KTOR false positive expect-actual matching error on enum class because of implicit clone() in non-JVM source sets

Analysis API. Code Compilation

  • KT-76457 K2 IDE / KMP Debugger: KISEWA “Cannot compile a common source without a JVM counterpart” on evaluating inline fun from common module inside jvm

Analysis API. FIR

  • KT-70552 No expects for actual
  • KT-69727 K2 IDE. Wrong error in the editor on calling clone function of actual enum instance in non-jvm platform
  • KT-69726 FP errors on declaring fun clone() in actual enum in not-jvm source-set
  • KT-86014 Types are broken after remove parameter through change signature
  • KT-86363 KotlinIllegalArgumentExceptionWithAttachments: No dangling modifier found on companion blocks
  • KT-86147 Drop kotlin.parallel.resolve.under.global.lock registry key
  • KT-85543 Avoid lazy resolve for the contracts phase if no constracts might be resolved

Analysis API. Infrastructure

  • KT-84914 Do not publish analysis-api-test-framework
  • KT-86986 kotlin-compiler-common-for-ide bundles unrelated Analysis API modules
  • KT-86186 Analysis API: Codebase tests run twice in some analysis modules — pick a single JUnit runner and migrate
  • KT-85360 Drop kotlin-compiler-testdata-for-ide artifact
  • KT-85585 Simplify the dependencies graph for the Analysis API modules
  • KT-85381 Remove tests for the FE10 implementation

Analysis API. Light Classes

New Features

  • KT-84645 Support resolving to companion block members & extensions from Java (light classes)
  • KT-80775 Support PsiClass#getRecordComponents in light classes

Fixes

  • KT-57537 SLC: propagate default parameter value from (@JvmOverloads) expect declarations to actual declarations
  • KT-85040 [Analysis API] Improve Java / Kotlin interop in KMP projects
  • KT-87301 SymbolLightAccessorMethod#isValid returns false for delegated properties
  • KT-87171 SLC: non-mapped Kotlin collection supertype is dropped from supertype list
  • KT-87250 JvmExposeBoxed: light classes shouldn't be autogenerated for private declarations
  • KT-70428 AA: good code is red when a Java class extends a Kotlin class implementing MutableList by delegation
  • KT-63568 Symbol Light Classes: KtAnnotationApplicationWithArgumentsInfo.normalizedArguments() may work incorrectly when psi is not set
  • KT-36740 MPP: False-positive incompatible types in .java when using expect-class returned by non-expect member from common when actual is actual typealias

... (truncated)

Commits
  • 890ac1d Add Changelog for 2.4.20-RC3
  • 8860aed 🍒 [FIR] Fix suspend conversion when expected type is nullable (#7769)
  • ab5bcd9 Edit ChangeLog for 2.4.20-RC2
  • 9464edc Add ChangeLog for 2.4.20-RC2
  • 0261e43 Cherry-pick "Fix asBodyAndResultVar call in `visitInlinedLambdaInComposable...
  • 0763513 [box-tests] Workaround for klib compatibility tests (#7626)
  • 045aec6 [Wasm] Append scripts from webpack.config.d in the end of the file (#7615)
  • cd7173b 🍒 [2.4.20] [K/JS] Keep associated obj annotation only if getInstance survives...
  • d36a2ff CastsOptimizationPass: break aliases cycles (#7578)
  • 8664983 [CLI] Restore custom IC search scope creation
  • Additional commits viewable in compare view

Updates `org.jetbrains.kotlin.plugin.compose` from 2.4.10 to 2.4.20
Release notes

Sourced from org.jetbrains.kotlin.plugin.compose's releases.

Kotlin 2.4.20

Changelog

Analysis API

  • KT-86546 Check suspicious when over ConeKotlinType in ConeTypeCompatibilityChecker
  • KT-85418 Implement an API for accessing deserialized file annotations in Analysis API
  • KT-74448 K2. False positive MISSING_DEPENDENCY_SUPERCLASS in LinkedListTest.kt, kotlinx.coroutines
  • KT-85856 containingSymbol of constructor property differs for local and non-local classes
  • KT-65417 K2 IDE: KTOR false positive expect-actual matching error on enum class because of implicit clone() in non-JVM source sets

Analysis API. Code Compilation

  • KT-76457 K2 IDE / KMP Debugger: KISEWA “Cannot compile a common source without a JVM counterpart” on evaluating inline fun from common module inside jvm

Analysis API. FIR

  • KT-70552 No expects for actual
  • KT-69727 K2 IDE. Wrong error in the editor on calling clone function of actual enum instance in non-jvm platform
  • KT-69726 FP errors on declaring fun clone() in actual enum in not-jvm source-set
  • KT-86014 Types are broken after remove parameter through change signature
  • KT-86363 KotlinIllegalArgumentExceptionWithAttachments: No dangling modifier found on companion blocks
  • KT-86147 Drop kotlin.parallel.resolve.under.global.lock registry key
  • KT-85543 Avoid lazy resolve for the contracts phase if no constracts might be resolved

Analysis API. Infrastructure

  • KT-84914 Do not publish analysis-api-test-framework
  • KT-86986 kotlin-compiler-common-for-ide bundles unrelated Analysis API modules
  • KT-86186 Analysis API: Codebase tests run twice in some analysis modules — pick a single JUnit runner and migrate
  • KT-85360 Drop kotlin-compiler-testdata-for-ide artifact
  • KT-85585 Simplify the dependencies graph for the Analysis API modules
  • KT-85381 Remove tests for the FE10 implementation

Analysis API. Light Classes

New Features

  • KT-84645 Support resolving to companion block members & extensions from Java (light classes)
  • KT-80775 Support PsiClass#getRecordComponents in light classes

Fixes

  • KT-57537 SLC: propagate default parameter value from (@JvmOverloads) expect declarations to actual declarations
  • KT-85040 [Analysis API] Improve Java / Kotlin interop in KMP projects
  • KT-87301 SymbolLightAccessorMethod#isValid returns false for delegated properties
  • KT-87171 SLC: non-mapped Kotlin collection supertype is dropped from supertype list
  • KT-87250 JvmExposeBoxed: light classes shouldn't be autogenerated for private declarations
  • KT-70428 AA: good code is red when a Java class extends a Kotlin class implementing MutableList by delegation
  • KT-63568 Symbol Light Classes: KtAnnotationApplicationWithArgumentsInfo.normalizedArguments() may work incorrectly when psi is not set

... (truncated)

Changelog

Sourced from org.jetbrains.kotlin.plugin.compose's changelog.

2.4.20

Analysis API

  • KT-86546 Check suspicious when over ConeKotlinType in ConeTypeCompatibilityChecker
  • KT-85418 Implement an API for accessing deserialized file annotations in Analysis API
  • KT-74448 K2. False positive MISSING_DEPENDENCY_SUPERCLASS in LinkedListTest.kt, kotlinx.coroutines
  • KT-85856 containingSymbol of constructor property differs for local and non-local classes
  • KT-65417 K2 IDE: KTOR false positive expect-actual matching error on enum class because of implicit clone() in non-JVM source sets

Analysis API. Code Compilation

  • KT-76457 K2 IDE / KMP Debugger: KISEWA “Cannot compile a common source without a JVM counterpart” on evaluating inline fun from common module inside jvm

Analysis API. FIR

  • KT-70552 No expects for actual
  • KT-69727 K2 IDE. Wrong error in the editor on calling clone function of actual enum instance in non-jvm platform
  • KT-69726 FP errors on declaring fun clone() in actual enum in not-jvm source-set
  • KT-86014 Types are broken after remove parameter through change signature
  • KT-86363 KotlinIllegalArgumentExceptionWithAttachments: No dangling modifier found on companion blocks
  • KT-86147 Drop kotlin.parallel.resolve.under.global.lock registry key
  • KT-85543 Avoid lazy resolve for the contracts phase if no constracts might be resolved

Analysis API. Infrastructure

  • KT-84914 Do not publish analysis-api-test-framework
  • KT-86986 kotlin-compiler-common-for-ide bundles unrelated Analysis API modules
  • KT-86186 Analysis API: Codebase tests run twice in some analysis modules — pick a single JUnit runner and migrate
  • KT-85360 Drop kotlin-compiler-testdata-for-ide artifact
  • KT-85585 Simplify the dependencies graph for the Analysis API modules
  • KT-85381 Remove tests for the FE10 implementation

Analysis API. Light Classes

New Features

  • KT-84645 Support resolving to companion block members & extensions from Java (light classes)
  • KT-80775 Support PsiClass#getRecordComponents in light classes

Fixes

  • KT-57537 SLC: propagate default parameter value from (@JvmOverloads) expect declarations to actual declarations
  • KT-85040 [Analysis API] Improve Java / Kotlin interop in KMP projects
  • KT-87301 SymbolLightAccessorMethod#isValid returns false for delegated properties
  • KT-87171 SLC: non-mapped Kotlin collection supertype is dropped from supertype list
  • KT-87250 JvmExposeBoxed: light classes shouldn't be autogenerated for private declarations
  • KT-70428 AA: good code is red when a Java class extends a Kotlin class implementing MutableList by delegation
  • KT-63568 Symbol Light Classes: KtAnnotationApplicationWithArgumentsInfo.normalizedArguments() may work incorrectly when psi is not set
  • KT-36740 MPP: False-positive incompatible types in .java when using expect-class returned by non-expect member from common when actual is actual typealias

... (truncated)

Commits
  • 890ac1d Add Changelog for 2.4.20-RC3
  • 8860aed 🍒 [FIR] Fix suspend conversion when expected type is nullable (#7769)
  • ab5bcd9 Edit ChangeLog for 2.4.20-RC2
  • 9464edc Add ChangeLog for 2.4.20-RC2
  • 0261e43 Cherry-pick "Fix asBodyAndResultVar call in `visitInlinedLambdaInComposable...
  • 0763513 [box-tests] Workaround for klib compatibility tests (#7626)
  • 045aec6 [Wasm] Append scripts from webpack.config.d in the end of the file (#7615)
  • cd7173b 🍒 [2.4.20] [K/JS] Keep associated obj annotation only if getInstance survives...
  • d36a2ff CastsOptimizationPass: break aliases cycles (#7578)
  • 8664983 [CLI] Restore custom IC search scope creation
  • Additional commits viewable in compare view

Updates `org.jetbrains.kotlin.plugin.serialization` from 2.4.10 to 2.4.20
Release notes

Sourced from org.jetbrains.kotlin.plugin.serialization's releases.

Kotlin 2.4.20

Changelog

Analysis API

  • KT-86546 Check suspicious when over ConeKotlinType in ConeTypeCompatibilityChecker
  • KT-85418 Implement an API for accessing deserialized file annotations in Analysis API
  • KT-74448 K2. False positive MISSING_DEPENDENCY_SUPERCLASS in LinkedListTest.kt, kotlinx.coroutines
  • KT-85856 containingSymbol of constructor property differs for local and non-local classes
  • KT-65417 K2 IDE: KTOR false positive expect-actual matching error on enum class because of implicit clone() in non-JVM source sets

Analysis API. Code Compilation

  • KT-76457 K2 IDE / KMP Debugger: KISEWA “Cannot compile a common source without a JVM counterpart” on evaluating inline fun from common module inside jvm

Analysis API. FIR

  • KT-70552 No expects for actual
  • KT-69727 K2 IDE. Wrong error in the editor on calling clone function of actual enum instance in non-jvm platform
  • KT-69726 FP errors on declaring fun clone() in actual enum in not-jvm source-set
  • KT-86014 Types are broken after remove parameter through change signature
  • KT-86363 KotlinIllegalArgumentExceptionWithAttachments: No dangling modifier found on companion blocks
  • KT-86147 Drop kotlin.parallel.resolve.under.global.lock registry key
  • KT-85543 Avoid lazy resolve for the contracts phase if no constracts might be resolved

Analysis API. Infrastructure

  • KT-84914 Do not publish analysis-api-test-framework
  • KT-86986 kotlin-compiler-common-for-ide bundles unrelated Analysis API modules
  • KT-86186 Analysis API: Codebase tests run twice in some analysis modules — pick a single JUnit runner and migrate
  • KT-85360 Drop kotlin-compiler-testdata-for-ide artifact
  • KT-85585 Simplify the dependencies graph for the Analysis API modules
  • KT-85381 Remove tests for the FE10 implementation

Analysis API. Light Classes

New Features

  • KT-84645 Support resolving to companion block members & extensions from Java (light classes)
  • KT-80775 Support PsiClass#getRecordComponents in light classes

Fixes

  • KT-57537 SLC: propagate default parameter value from (@JvmOverloads) expect declarations to actual declarations
  • KT-85040 [Analysis API] Improve Java / Kotlin interop in KMP projects
  • KT-87301 SymbolLightAccessorMethod#isValid returns false for delegated properties
  • KT-87171 SLC: non-mapped Kotlin collection supertype is dropped from supertype list
  • KT-87250 JvmExposeBoxed: light classes shouldn't be autogenerated for private declarations
  • KT-70428 AA: good code is red when a Java class extends a Kotlin class implementing MutableList by delegation
  • KT-63568 Symbol Light Classes: KtAnnotationApplicationWithArgumentsInfo.normalizedArguments() may work incorrectly when psi is not set

... (truncated)

Changelog

Sourced from org.jetbrains.kotlin.plugin.serialization's changelog.

2.4.20

Analysis API

  • KT-86546 Check suspicious when over ConeKotlinType in ConeTypeCompatibilityChecker
  • KT-85418 Implement an API for accessing deserialized file annotations in Analysis API
  • KT-74448 K2. False positive MISSING_DEPENDENCY_SUPERCLASS in LinkedListTest.kt, kotlinx.coroutines
  • KT-85856 containingSymbol of constructor property differs for local and non-local classes
  • KT-65417 K2 IDE: KTOR false positive expect-actual matching error on enum class because of implicit clone() in non-JVM source sets

Analysis API. Code Compilation

  • KT-76457 K2 IDE / KMP Debugger: KISEWA “Cannot compile a common source without a JVM counterpart” on evaluating inline fun from common module inside jvm

Analysis API. FIR

  • KT-70552 No expects for actual
  • KT-69727 K2 IDE. Wrong error in the editor on calling clone function of actual enum instance in non-jvm platform
  • KT-69726 FP errors on declaring fun clone() in actual enum in not-jvm source-set
  • KT-86014 Types are broken after remove parameter through change signature
  • KT-86363 KotlinIllegalArgumentExceptionWithAttachments: No dangling modifier found on companion blocks
  • KT-86147 Drop kotlin.parallel.resolve.under.global.lock registry key
  • KT-85543 Avoid lazy resolve for the contracts phase if no constracts might be resolved

Analysis API. Infrastructure

  • KT-84914 Do not publish analysis-api-test-framework
  • KT-86986 kotlin-compiler-common-for-ide bundles unrelated Analysis API modules
  • KT-86186 Analysis API: Codebase tests run twice in some analysis modules — pick a single JUnit runner and migrate
  • KT-85360 Drop kotlin-compiler-testdata-for-ide artifact
  • KT-85585 Simplify the dependencies graph for the Analysis API modules
  • KT-85381 Remove tests for the FE10 implementation

Analysis API. Light Classes

New Features

  • KT-84645 Support resolving to companion block members & extensions from Java (light classes)
  • KT-80775 Support PsiClass#getRecordComponents in light classes

Fixes

  • KT-57537 SLC: propagate default parameter value from (@JvmOverloads) expect declarations to actual declarations
  • KT-85040 [Analysis API] Improve Java / Kotlin interop in KMP projects
  • KT-87301 SymbolLightAccessorMethod#isValid returns false for delegated properties
  • KT-87171 SLC: non-mapped Kotlin collection supertype is dropped from supertype list
  • KT-87250 JvmExposeBoxed: light classes shouldn't be autogenerated for private declarations
  • KT-70428 AA: good code is red when a Java class extends a Kotlin class implementing MutableList by delegation
  • KT-63568 Symbol Light Classes: KtAnnotationApplicationWithArgumentsInfo.normalizedArguments() may work incorrectly when psi is not set
  • KT-36740 MPP: False-positive incompatible types in .java when using expect-class returned by non-expect member from common when actual is actual typealias

... (truncated)

Commits
  • 890ac1d Add Changelog for 2.4.20-RC3
  • 8860aed 🍒 [FIR] Fix suspend conversion when expected type is nullable (#7769)
  • ab5bcd9 Edit ChangeLog for 2.4.20-RC2
  • 9464edc Add ChangeLog for 2.4.20-RC2
  • 0261e43 Cherry-pick "Fix asBodyAndResultVar call in `visitInlinedLambdaInComposable...
  • 0763513 [box-tests] Workaround for klib compatibility tests (#7626)
  • 045aec6 [Wasm] Append scripts from webpack.config.d in the end of the file (#7615)
  • cd7173b 🍒 [2.4.20] [K/JS] Keep associated obj annotation only if getInstance survives...
  • d36a2ff CastsOptimizationPass: break aliases cycles (#7578)
  • 8664983 [CLI] Restore custom IC search scope creation
  • Additional commits viewable in compare view

Updates `org.jetbrains.kotlin.plugin.compose` from 2.4.10 to 2.4.20
Release notes

Sourced from org.jetbrains.kotlin.plugin.compose's releases.

Kotlin 2.4.20

Changelog

Analysis API

  • KT-86546 Check suspicious when over ConeKotlinType in ConeTypeCompatibilityChecker
  • KT-85418 Implement an API for accessing deserialized file annotations in Analysis API
  • KT-74448 K2. False positive MISSING_DEPENDENCY_SUPERCLASS in LinkedListTest.kt, kotlinx.coroutines
  • KT-85856 containingSymbol of constructor property differs for local and non-local classes
  • KT-65417 K2 IDE: KTOR false positive expect-actual matching error on enum class because of implicit clone() in non-JVM source sets

Analysis API. Code Compilation

  • KT-76457 K2 IDE / KMP Debugger: KISEWA “Cannot compile a common source without a JVM counterpart” on evaluating inline fun from common module inside jvm

Analysis API. FIR

  • KT-70552 No expects for actual
  • KT-69727 K2 IDE. Wrong error in the editor on calling clone function of actual enum instance in non-jvm platform
  • KT-69726 FP errors on declaring fun clone() in actual enum in not-jvm source-set
  • KT-86014 Types are broken after remove parameter through change signature
  • KT-86363 KotlinIllegalArgumentExceptionWithAttachments: No dangling modifier found on companion blocks
  • KT-86147 Drop kotlin.parallel.resolve.under.global.lock registry key
  • KT-85543 Avoid lazy resolve for the contracts phase if no constracts might be resolved

Analysis API. Infrastructure

  • KT-84914 Do not publish analysis-api-test-framework
  • KT-86986 kotlin-compiler-common-for-ide bundles unrelated Analysis API modules
  • KT-86186 Analysis API: Codebase tests run twice in some analysis modules — pick a single JUnit runner and migrate
  • KT-85360 Drop kotlin-compiler-testdata-for-ide artifact
  • KT-85585 Simplify the dependencies graph for the Analysis API modules
  • KT-85381 Remove tests for the FE10 implementation

Analysis API. Light Classes

New Features

  • KT-84645 Support resolving to companion block members & extensions from Java (light classes)
  • KT-80775 Support PsiClass#getRecordComponents in light classes

Fixes

  • KT-57537 SLC: propagate default parameter value from (@JvmOverloads) expect declarations to actual declarations
  • KT-85040 [Analysis API] Improve Java / Kotlin interop in KMP projects
  • KT-87301 SymbolLightAccessorMethod#isValid returns false for delegated properties
  • KT-87171 SLC: non-mapped Kotlin collection supertype is dropped from supertype list
  • KT-87250 JvmExposeBoxed: light classes shouldn't be autogenerated for private declarations
  • KT-70428 AA: good code is red when a Java class extends a Kotlin class implementing MutableList by delegation
  • KT-63568 Symbol Light Classes: KtAnnotationApplicationWithArgumentsInfo.normalizedArguments() may work incorrectly when psi is not set

... (truncated)

Changelog

Sourced from org.jetbrains.kotlin.plugin.compose's changelog.

2.4.20

Analysis API

  • KT-86546 Check suspicious when over ConeKotlinType in ConeTypeCompatibilityChecker
  • KT-85418 Implement an API for accessing deserialized file annotations in Analysis API
  • KT-74448 K2. False positive MISSING_DEPENDENCY_SUPERCLASS in LinkedListTest.kt, kotlinx.coroutines
  • KT-85856 containingSymbol of constructor property differs for local and non-local classes
  • KT-65417 K2 IDE: KTOR false positive expect-actual matching error on enum class because of implicit clone() in non-JVM source sets

Analysis API. Code Compilation

  • KT-76457 K2 IDE / KMP Debugger: KISEWA “Cannot compile a common source without a JVM counterpart” on evaluating inline fun from common module inside jvm

Analysis API. FIR

  • KT-70552 No expects for actual
  • KT-69727 K2 IDE. Wrong error in the editor on calling clone function of actual enum instance in non-jvm platform
  • KT-69726 FP errors on declaring fun clone() in actual enum in not-jvm source-set
  • KT-86014 Types are broken after remove parameter through change signature
  • KT-86363 KotlinIllegalArgumentExceptionWithAttachments: No dangling modifier found on companion blocks
  • KT-86147 Drop kotlin.parallel.resolve.under.global.lock registry key
  • KT-85543 Avoid lazy resolve for the contracts phase if no constracts might be resolved

Analysis API. Infrastructure

  • KT-84914 Do not publish analysis-api-test-framework
  • KT-86986 kotlin-compiler-common-for-ide bundles unrelated Analysis API modules
  • KT-86186 Analysis API: Codebase tests run twice in some analysis modules — pick a single JUnit runner and migrate
  • KT-85360 Drop kotlin-compiler-testdata-for-ide artifact
  • KT-85585 Simplify the dependencies graph for the Analysis API modules
  • KT-85381 Remove tests for the FE10 implementation

Analysis API. Light Classes

New Features

  • KT-84645 Support resolving to companion block members & extensions from Java (light classes)
  • KT-80775 Support PsiClass#getRecordComponents in light classes

Fixes

  • KT-57537 SLC: propagate default parameter v... _Description has been truncated_ Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: AlexLiuDev233 --- manager/gradle/libs.versions.toml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/manager/gradle/libs.versions.toml b/manager/gradle/libs.versions.toml index 29543a87e..50c8d3282 100644 --- a/manager/gradle/libs.versions.toml +++ b/manager/gradle/libs.versions.toml @@ -1,13 +1,13 @@ [versions] accompanist-drawablepainter = "0.37.3" -agp = "9.3.2" +agp = "9.4.0" gson = "2.14.0" -kotlin = "2.4.10" -materialKolor = "5.0.0" +kotlin = "2.4.20" +materialKolor = "5.0.1" monetCompat = "0.4.1" materialComponents = "1.14.0" capsule = "2.1.3" -compose-bom = "2026.08.00" +compose-bom = "2026.09.00" lifecycle = "2.10.0" activity-compose = "1.13.0" core-splashscreen = "1.2.0" @@ -21,15 +21,15 @@ hiddenapibypass = "6.1" parcelablelist = "2.0.1" libsu = "6.0.0" apksign = "1.4" -compose-material3 = "1.5.0-alpha27" +compose-material3 = "1.5.0-alpha28" compose-ui = "1.11.2" documentfile = "1.1.0" ndk = "29.0.14206865" foundation = "1.11.2" -aboutLibraries = "15.1.1" +aboutLibraries = "15.2.0" miuix = "0.9.4-rc01" datastore = "1.2.1" -benchmark = "1.5.0-rc02" +benchmark = "1.5.0" profileinstaller = "1.4.1" koin-bom = "4.2.2"