From 0d73908d1bb99cd5d1cbd2ad6215e1dd15de8f43 Mon Sep 17 00:00:00 2001 From: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com> Date: Sat, 22 Nov 2025 04:16:04 +0800 Subject: [PATCH] Step 7: Add susfs interface --- .../com/sukisu/ultra/ui/screen/Settings.kt | 31 +- .../com/sukisu/ultra/ui/susfs/SuSFSConfig.kt | 1859 ++++++++++++++++ .../ui/susfs/component/SuSFSConfigDialogs.kt | 1891 +++++++++++++++++ .../ui/susfs/component/SuSFSConfigTabs.kt | 1183 +++++++++++ .../ultra/ui/susfs/util/SuSFSManager.kt | 1462 +++++++++++++ .../ultra/ui/susfs/util/SuSFSModuleScripts.kt | 550 +++++ .../ultra/ui/viewmodel/SuperUserViewModel.kt | 9 + .../src/main/res/values-zh-rCN/strings.xml | 247 +++ manager/app/src/main/res/values/strings.xml | 246 +++ 9 files changed, 7477 insertions(+), 1 deletion(-) create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigDialogs.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigTabs.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSModuleScripts.kt diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt index 4841cc79..eb078430 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt @@ -24,10 +24,10 @@ import androidx.compose.material.icons.rounded.DeveloperMode import androidx.compose.material.icons.rounded.EnhancedEncryption import androidx.compose.material.icons.rounded.Fence import androidx.compose.material.icons.rounded.FolderDelete -import androidx.compose.material.icons.rounded.Palette import androidx.compose.material.icons.rounded.RemoveCircle import androidx.compose.material.icons.rounded.RemoveModerator import androidx.compose.material.icons.rounded.RestartAlt +import androidx.compose.material.icons.rounded.Settings import androidx.compose.material.icons.rounded.Update import androidx.compose.material.icons.rounded.UploadFile import androidx.compose.runtime.Composable @@ -54,6 +54,7 @@ import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplat import com.ramcosta.composedestinations.generated.destinations.LogViewerDestination import com.ramcosta.composedestinations.generated.destinations.KpmScreenDestination import com.ramcosta.composedestinations.generated.destinations.PersonalizationDestination +import com.ramcosta.composedestinations.generated.destinations.SuSFSConfigScreenDestination import com.ramcosta.composedestinations.generated.destinations.ToolsDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import dev.chrisbanes.haze.HazeState @@ -308,6 +309,34 @@ fun SettingPager( } } + KsuIsValid { + Card( + modifier = Modifier + .padding(top = 12.dp) + .fillMaxWidth(), + colors = wallpaperCardColors(), + ) { + val susfsTitle = stringResource(id = R.string.susfs_config_title) + SuperArrow( + title = susfsTitle, + summary = stringResource(id = R.string.susfs_config_summary), + leftAction = { + Icon( + Icons.Rounded.Settings, + modifier = Modifier.padding(end = 16.dp), + contentDescription = susfsTitle, + tint = colorScheme.onBackground + ) + }, + onClick = { + navigator.navigate(SuSFSConfigScreenDestination) { + launchSingleTop = true + } + } + ) + } + } + KsuIsValid { Card( modifier = Modifier diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt new file mode 100644 index 00000000..ac6a26a2 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt @@ -0,0 +1,1859 @@ +package com.sukisu.ultra.ui.susfs + +import android.annotation.SuppressLint +import android.content.Context +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +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.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.systemBars +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.HazeTint +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.susfs.component.* +import com.sukisu.ultra.ui.theme.wallpaperCardColors +import com.sukisu.ultra.ui.theme.wallpaperContainerColor +import com.sukisu.ultra.ui.susfs.util.SuSFSManager +import com.sukisu.ultra.ui.util.isAbDevice +import kotlinx.coroutines.launch +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.coroutines.delay +import top.yukonga.miuix.kmp.basic.* +import top.yukonga.miuix.kmp.extra.SuperDialog +import top.yukonga.miuix.kmp.extra.SuperDropdown +import top.yukonga.miuix.kmp.extra.SuperSwitch +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.useful.Back +import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.getWindowSize +import top.yukonga.miuix.kmp.utils.overScrollVertical +import top.yukonga.miuix.kmp.utils.scrollEndHaptic +import java.io.File +import java.text.SimpleDateFormat +import java.util.* + +/** + * 标签页枚举类 + */ +enum class SuSFSTab(val displayNameRes: Int) { + BASIC_SETTINGS(R.string.susfs_tab_basic_settings), + SUS_PATHS(R.string.susfs_tab_sus_paths), + SUS_LOOP_PATHS(R.string.susfs_tab_sus_loop_paths), + SUS_MAPS(R.string.susfs_tab_sus_maps), + SUS_MOUNTS(R.string.susfs_tab_sus_mounts), + TRY_UMOUNT(R.string.susfs_tab_try_umount), + KSTAT_CONFIG(R.string.susfs_tab_kstat_config), + PATH_SETTINGS(R.string.susfs_tab_path_settings), + ENABLED_FEATURES(R.string.susfs_tab_enabled_features); + + companion object { + fun getAllTabs(): List { + return entries.toList() + } + } +} + +/** + * SuSFS配置界面 + */ +@SuppressLint("SdCardPath", "AutoboxingStateCreation") +@Destination +@Composable +fun SuSFSConfigScreen( + navigator: DestinationsNavigator +) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val scrollBehavior = MiuixScrollBehavior() + val hazeState = remember { HazeState() } + val hazeStyle = HazeStyle( + backgroundColor = colorScheme.surface, + tint = HazeTint(colorScheme.surface.copy(0.8f)) + ) + + var selectedTab by remember { mutableStateOf(SuSFSTab.BASIC_SETTINGS) } + var unameValue by remember { mutableStateOf("") } + var buildTimeValue by remember { mutableStateOf("") } + var isLoading by remember { mutableStateOf(false) } + var showConfirmReset by remember { mutableStateOf(false) } + var autoStartEnabled by remember { mutableStateOf(false) } + var executeInPostFsData by remember { mutableStateOf(false) } + var enableHideBl by remember { mutableStateOf(true) } + var enableCleanupResidue by remember { mutableStateOf(false) } + var enableAvcLogSpoofing by remember { mutableStateOf(false) } + + // 槽位信息相关状态 + var slotInfoList by remember { mutableStateOf(emptyList()) } + var currentActiveSlot by remember { mutableStateOf("") } + var isLoadingSlotInfo by remember { mutableStateOf(false) } + var showSlotInfoDialog by remember { mutableStateOf(false) } + + // 路径管理相关状态 + var susPaths by remember { mutableStateOf(emptySet()) } + var susLoopPaths by remember { mutableStateOf(emptySet()) } + var susMaps by remember { mutableStateOf(emptySet()) } + var susMounts by remember { mutableStateOf(emptySet()) } + var tryUmounts by remember { mutableStateOf(emptySet()) } + var androidDataPath by remember { mutableStateOf("") } + var sdcardPath by remember { mutableStateOf("") } + + // SUS挂载隐藏控制状态 + var hideSusMountsForAllProcs by remember { mutableStateOf(true) } + + var umountForZygoteIsoService by remember { mutableStateOf(false) } + + // Kstat配置相关状态 + var kstatConfigs by remember { mutableStateOf(emptySet()) } + var addKstatPaths by remember { mutableStateOf(emptySet()) } + + // 启用功能状态相关 + var enabledFeatures by remember { mutableStateOf(emptyList()) } + var isLoadingFeatures by remember { mutableStateOf(false) } + + // 应用列表相关状态 + var installedApps by remember { mutableStateOf(emptyList()) } + + // 对话框状态 + var showAddPathDialog by remember { mutableStateOf(false) } + var showAddLoopPathDialog by remember { mutableStateOf(false) } + var showAddSusMapDialog by remember { mutableStateOf(false) } + var showAddAppPathDialog by remember { mutableStateOf(false) } + var showAddMountDialog by remember { mutableStateOf(false) } + var showAddUmountDialog by remember { mutableStateOf(false) } + var showAddKstatStaticallyDialog by remember { mutableStateOf(false) } + var showAddKstatDialog by remember { mutableStateOf(false) } + + // 编辑状态 + var editingPath by remember { mutableStateOf(null) } + var editingLoopPath by remember { mutableStateOf(null) } + var editingSusMap by remember { mutableStateOf(null) } + var editingMount by remember { mutableStateOf(null) } + var editingUmount by remember { mutableStateOf(null) } + var editingKstatConfig by remember { mutableStateOf(null) } + var editingKstatPath by remember { mutableStateOf(null) } + + // 重置确认对话框状态 + var showResetPathsDialog by remember { mutableStateOf(false) } + var showResetLoopPathsDialog by remember { mutableStateOf(false) } + var showResetSusMapsDialog by remember { mutableStateOf(false) } + var showResetMountsDialog by remember { mutableStateOf(false) } + var showResetUmountsDialog by remember { mutableStateOf(false) } + var showResetKstatDialog by remember { mutableStateOf(false) } + + // 备份还原相关状态 + var showBackupDialog by remember { mutableStateOf(false) } + var showRestoreDialog by remember { mutableStateOf(false) } + var showRestoreConfirmDialog by remember { mutableStateOf(false) } + var selectedBackupFile by remember { mutableStateOf(null) } + var backupInfo by remember { mutableStateOf(null) } + + var isNavigating by remember { mutableStateOf(false) } + + val allTabs = SuSFSTab.getAllTabs() + + // 实时判断是否可以启用开机自启动 + val canEnableAutoStart by remember { + derivedStateOf { + SuSFSManager.hasConfigurationForAutoStart(context) + } + } + + + // 文件选择器 + val backupFileLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.CreateDocument("application/json") + ) { uri -> + uri?.let { fileUri -> + val fileName = SuSFSManager.getDefaultBackupFileName() + val tempFile = File(context.cacheDir, fileName) + coroutineScope.launch { + isLoading = true + val success = SuSFSManager.createBackup(context, tempFile.absolutePath) + if (success) { + try { + context.contentResolver.openOutputStream(fileUri)?.use { outputStream -> + tempFile.inputStream().use { inputStream -> + inputStream.copyTo(outputStream) + } + } + } catch (e: Exception) { + e.printStackTrace() + } + tempFile.delete() + } + isLoading = false + showBackupDialog = false + } + } + } + + val restoreFileLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument() + ) { uri -> + uri?.let { fileUri -> + coroutineScope.launch { + try { + val tempFile = File(context.cacheDir, "temp_restore.susfs_backup") + context.contentResolver.openInputStream(fileUri)?.use { inputStream -> + tempFile.outputStream().use { outputStream -> + inputStream.copyTo(outputStream) + } + } + + // 验证备份文件 + val backup = SuSFSManager.validateBackupFile(tempFile.absolutePath) + if (backup != null) { + selectedBackupFile = tempFile.absolutePath + backupInfo = backup + showRestoreConfirmDialog = true + } + tempFile.deleteOnExit() + } catch (e: Exception) { + e.printStackTrace() + } + showRestoreDialog = false + } + } + } + + // 加载启用功能状态 + fun loadEnabledFeatures() { + coroutineScope.launch { + isLoadingFeatures = true + enabledFeatures = SuSFSManager.getEnabledFeatures(context) + isLoadingFeatures = false + } + } + + // 加载应用列表 + fun loadInstalledApps() { + coroutineScope.launch { + installedApps = SuSFSManager.getInstalledApps() + } + } + + // 加载槽位信息 + fun loadSlotInfo() { + coroutineScope.launch { + isLoadingSlotInfo = true + slotInfoList = SuSFSManager.getCurrentSlotInfo() + currentActiveSlot = SuSFSManager.getCurrentActiveSlot() + isLoadingSlotInfo = false + } + } + + // 加载当前配置 + LaunchedEffect(Unit) { + coroutineScope.launch { + + unameValue = SuSFSManager.getUnameValue(context) + buildTimeValue = SuSFSManager.getBuildTimeValue(context) + autoStartEnabled = SuSFSManager.isAutoStartEnabled(context) + executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context) + susPaths = SuSFSManager.getSusPaths(context) + susLoopPaths = SuSFSManager.getSusLoopPaths(context) + susMaps = SuSFSManager.getSusMaps(context) + susMounts = SuSFSManager.getSusMounts(context) + tryUmounts = SuSFSManager.getTryUmounts(context) + androidDataPath = SuSFSManager.getAndroidDataPath(context) + sdcardPath = SuSFSManager.getSdcardPath(context) + kstatConfigs = SuSFSManager.getKstatConfigs(context) + addKstatPaths = SuSFSManager.getAddKstatPaths(context) + hideSusMountsForAllProcs = SuSFSManager.getHideSusMountsForAllProcs(context) + enableHideBl = SuSFSManager.getEnableHideBl(context) + enableCleanupResidue = SuSFSManager.getEnableCleanupResidue(context) + umountForZygoteIsoService = SuSFSManager.getUmountForZygoteIsoService(context) + enableAvcLogSpoofing = SuSFSManager.getEnableAvcLogSpoofing(context) + + loadSlotInfo() + } + } + + // 当切换到启用功能状态标签页时加载数据 + LaunchedEffect(selectedTab) { + if (selectedTab == SuSFSTab.ENABLED_FEATURES) { + loadEnabledFeatures() + } + } + + // 当配置变化时,自动调整开机自启动状态 + LaunchedEffect(canEnableAutoStart) { + if (!canEnableAutoStart && autoStartEnabled) { + autoStartEnabled = false + SuSFSManager.configureAutoStart(context, false) + } + } + + // 备份对话框 + val showBackupDialogState = remember { mutableStateOf(showBackupDialog) } + LaunchedEffect(showBackupDialog) { + showBackupDialogState.value = showBackupDialog + } + if (showBackupDialog) { + SuperDialog( + show = showBackupDialogState, + title = stringResource(R.string.susfs_backup_title), + onDismissRequest = { showBackupDialog = false }, + content = { + Text(stringResource(R.string.susfs_backup_description)) + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + TextButton( + text = stringResource(R.string.cancel), + onClick = { showBackupDialog = false }, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp) + ) + TextButton( + text = stringResource(R.string.susfs_backup_create), + onClick = { + val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()) + val timestamp = dateFormat.format(Date()) + backupFileLauncher.launch("SuSFS_Config_$timestamp.susfs_backup") + }, + enabled = !isLoading, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + colors = ButtonDefaults.textButtonColorsPrimary() + ) + } + } + ) + } + + // 还原对话框 + val showRestoreDialogState = remember { mutableStateOf(showRestoreDialog) } + LaunchedEffect(showRestoreDialog) { + showRestoreDialogState.value = showRestoreDialog + } + if (showRestoreDialog) { + SuperDialog( + show = showRestoreDialogState, + title = stringResource(R.string.susfs_restore_title), + onDismissRequest = { showRestoreDialog = false }, + content = { + Text(stringResource(R.string.susfs_restore_description)) + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + TextButton( + text = stringResource(R.string.cancel), + onClick = { showRestoreDialog = false }, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp) + ) + TextButton( + text = stringResource(R.string.susfs_restore_select_file), + onClick = { + restoreFileLauncher.launch(arrayOf("application/json", "*/*")) + }, + enabled = !isLoading, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + colors = ButtonDefaults.textButtonColorsPrimary() + ) + } + } + ) + } + + // 还原确认对话框 + val showRestoreConfirmDialogState = remember { mutableStateOf(showRestoreConfirmDialog && backupInfo != null) } + LaunchedEffect(showRestoreConfirmDialog, backupInfo) { + showRestoreConfirmDialogState.value = showRestoreConfirmDialog && backupInfo != null + } + if (showRestoreConfirmDialog && backupInfo != null) { + SuperDialog( + show = showRestoreConfirmDialogState, + title = stringResource(R.string.susfs_restore_confirm_title), + onDismissRequest = { + showRestoreConfirmDialog = false + selectedBackupFile = null + backupInfo = null + }, + content = { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text(stringResource(R.string.susfs_restore_confirm_description)) + + Card( + modifier = Modifier.fillMaxWidth(), + colors = wallpaperCardColors() + ) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + Text( + text = stringResource(R.string.susfs_backup_info_date, + dateFormat.format(Date(backupInfo!!.timestamp))), + fontSize = MiuixTheme.textStyles.body2.fontSize + ) + Text( + text = stringResource(R.string.susfs_backup_info_device, backupInfo!!.deviceInfo), + fontSize = MiuixTheme.textStyles.body2.fontSize + ) + Text( + text = stringResource(R.string.susfs_backup_info_version, backupInfo!!.version), + fontSize = MiuixTheme.textStyles.body2.fontSize + ) + } + } + } + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + TextButton( + text = stringResource(R.string.cancel), + onClick = { + showRestoreConfirmDialog = false + selectedBackupFile = null + backupInfo = null + }, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp) + ) + TextButton( + text = stringResource(R.string.susfs_restore_confirm), + onClick = { + selectedBackupFile?.let { filePath -> + coroutineScope.launch { + isLoading = true + try { + val success = SuSFSManager.restoreFromBackup(context, filePath) + if (success) { + // 在后台线程读取配置,然后在主线程更新状态 + val configs = withContext(Dispatchers.IO) { + mapOf( + "unameValue" to SuSFSManager.getUnameValue(context), + "buildTimeValue" to SuSFSManager.getBuildTimeValue(context), + "autoStartEnabled" to SuSFSManager.isAutoStartEnabled(context), + "executeInPostFsData" to SuSFSManager.getExecuteInPostFsData(context), + "susPaths" to SuSFSManager.getSusPaths(context), + "susLoopPaths" to SuSFSManager.getSusLoopPaths(context), + "susMaps" to SuSFSManager.getSusMaps(context), + "susMounts" to SuSFSManager.getSusMounts(context), + "tryUmounts" to SuSFSManager.getTryUmounts(context), + "androidDataPath" to SuSFSManager.getAndroidDataPath(context), + "sdcardPath" to SuSFSManager.getSdcardPath(context), + "kstatConfigs" to SuSFSManager.getKstatConfigs(context), + "addKstatPaths" to SuSFSManager.getAddKstatPaths(context), + "hideSusMountsForAllProcs" to SuSFSManager.getHideSusMountsForAllProcs(context), + "enableHideBl" to SuSFSManager.getEnableHideBl(context), + "enableCleanupResidue" to SuSFSManager.getEnableCleanupResidue(context), + "umountForZygoteIsoService" to SuSFSManager.getUmountForZygoteIsoService(context), + "enableAvcLogSpoofing" to SuSFSManager.getEnableAvcLogSpoofing(context) + ) + } + + // 在主线程更新状态 + @Suppress("UNCHECKED_CAST") + unameValue = configs["unameValue"] as String + @Suppress("UNCHECKED_CAST") + buildTimeValue = configs["buildTimeValue"] as String + @Suppress("UNCHECKED_CAST") + autoStartEnabled = configs["autoStartEnabled"] as Boolean + @Suppress("UNCHECKED_CAST") + executeInPostFsData = configs["executeInPostFsData"] as Boolean + @Suppress("UNCHECKED_CAST") + susPaths = configs["susPaths"] as Set + @Suppress("UNCHECKED_CAST") + susLoopPaths = configs["susLoopPaths"] as Set + @Suppress("UNCHECKED_CAST") + susMaps = configs["susMaps"] as Set + @Suppress("UNCHECKED_CAST") + susMounts = configs["susMounts"] as Set + @Suppress("UNCHECKED_CAST") + tryUmounts = configs["tryUmounts"] as Set + @Suppress("UNCHECKED_CAST") + androidDataPath = configs["androidDataPath"] as String + @Suppress("UNCHECKED_CAST") + sdcardPath = configs["sdcardPath"] as String + @Suppress("UNCHECKED_CAST") + kstatConfigs = configs["kstatConfigs"] as Set + @Suppress("UNCHECKED_CAST") + addKstatPaths = configs["addKstatPaths"] as Set + @Suppress("UNCHECKED_CAST") + hideSusMountsForAllProcs = configs["hideSusMountsForAllProcs"] as Boolean + @Suppress("UNCHECKED_CAST") + enableHideBl = configs["enableHideBl"] as Boolean + @Suppress("UNCHECKED_CAST") + enableCleanupResidue = configs["enableCleanupResidue"] as Boolean + @Suppress("UNCHECKED_CAST") + umountForZygoteIsoService = configs["umountForZygoteIsoService"] as Boolean + @Suppress("UNCHECKED_CAST") + enableAvcLogSpoofing = configs["enableAvcLogSpoofing"] as Boolean + + // 延迟关闭对话框,给 UI 时间更新 + delay(300) + } + } catch (e: Exception) { + e.printStackTrace() + } finally { + // 先关闭对话框,确保在主线程上执行 + withContext(Dispatchers.Main) { + isLoading = false + showRestoreConfirmDialog = false + } + // 延迟清空状态,确保对话框完全关闭后再清空 + delay(100) + withContext(Dispatchers.Main) { + selectedBackupFile = null + backupInfo = null + } + } + } + } + }, + enabled = !isLoading, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + colors = ButtonDefaults.textButtonColorsPrimary() + ) + } + } + ) + } + + // 槽位信息对话框 + SlotInfoDialog( + showDialog = showSlotInfoDialog, + onDismiss = { showSlotInfoDialog = false }, + slotInfoList = slotInfoList, + currentActiveSlot = currentActiveSlot, + isLoadingSlotInfo = isLoadingSlotInfo, + onRefresh = { loadSlotInfo() }, + onUseUname = { uname: String -> + unameValue = uname + showSlotInfoDialog = false + }, + onUseBuildTime = { buildTime: String -> + buildTimeValue = buildTime + showSlotInfoDialog = false + } + ) + + // 各种对话框 + AddPathDialog( + showDialog = showAddPathDialog, + onDismiss = { + showAddPathDialog = false + editingPath = null + }, + onConfirm = { path -> + coroutineScope.launch { + isLoading = true + val success = if (editingPath != null) { + SuSFSManager.editSusPath(context, editingPath!!, path) + } else { + SuSFSManager.addSusPath(context, path) + } + if (success) { + susPaths = SuSFSManager.getSusPaths(context) + } + isLoading = false + showAddPathDialog = false + editingPath = null + } + }, + isLoading = isLoading, + titleRes = if (editingPath != null) R.string.susfs_edit_sus_path else R.string.susfs_add_sus_path, + labelRes = R.string.susfs_path_label, + initialValue = editingPath ?: "" + ) + + AddPathDialog( + showDialog = showAddLoopPathDialog, + onDismiss = { + showAddLoopPathDialog = false + editingLoopPath = null + }, + onConfirm = { path -> + coroutineScope.launch { + isLoading = true + val success = if (editingLoopPath != null) { + SuSFSManager.editSusLoopPath(context, editingLoopPath!!, path) + } else { + SuSFSManager.addSusLoopPath(context, path) + } + if (success) { + susLoopPaths = SuSFSManager.getSusLoopPaths(context) + } + isLoading = false + showAddLoopPathDialog = false + editingLoopPath = null + } + }, + isLoading = isLoading, + titleRes = if (editingLoopPath != null) R.string.susfs_edit_sus_loop_path else R.string.susfs_add_sus_loop_path, + labelRes = R.string.susfs_loop_path_label, + initialValue = editingLoopPath ?: "" + ) + + AddPathDialog( + showDialog = showAddSusMapDialog, + onDismiss = { + showAddSusMapDialog = false + editingSusMap = null + }, + onConfirm = { path -> + coroutineScope.launch { + isLoading = true + val success = if (editingSusMap != null) { + SuSFSManager.editSusMap(context, editingSusMap!!, path) + } else { + SuSFSManager.addSusMap(context, path) + } + if (success) { + susMaps = SuSFSManager.getSusMaps(context) + } + isLoading = false + showAddSusMapDialog = false + editingSusMap = null + } + }, + isLoading = isLoading, + titleRes = if (editingSusMap != null) R.string.susfs_edit_sus_map else R.string.susfs_add_sus_map, + labelRes = R.string.susfs_sus_map_label, + initialValue = editingSusMap ?: "" + ) + + AddAppPathDialog( + showDialog = showAddAppPathDialog, + onDismiss = { showAddAppPathDialog = false }, + onConfirm = { packageNames -> + coroutineScope.launch { + isLoading = true + var successCount = 0 + packageNames.forEach { packageName -> + if (SuSFSManager.addAppPaths(context, packageName)) { + successCount++ + } + } + if (successCount > 0) { + susPaths = SuSFSManager.getSusPaths(context) + } + isLoading = false + showAddAppPathDialog = false + } + }, + isLoading = isLoading, + apps = installedApps, + onLoadApps = { loadInstalledApps() }, + existingSusPaths = susPaths + ) + + AddPathDialog( + showDialog = showAddMountDialog, + onDismiss = { + showAddMountDialog = false + editingMount = null + }, + onConfirm = { mount -> + coroutineScope.launch { + isLoading = true + val success = if (editingMount != null) { + SuSFSManager.editSusMount(context, editingMount!!, mount) + } else { + SuSFSManager.addSusMount(context, mount) + } + if (success) { + susMounts = SuSFSManager.getSusMounts(context) + } + isLoading = false + showAddMountDialog = false + editingMount = null + } + }, + isLoading = isLoading, + titleRes = if (editingMount != null) R.string.susfs_edit_sus_mount else R.string.susfs_add_sus_mount, + labelRes = R.string.susfs_mount_path_label, + initialValue = editingMount ?: "" + ) + + AddTryUmountDialog( + showDialog = showAddUmountDialog, + onDismiss = { + showAddUmountDialog = false + editingUmount = null + }, + onConfirm = { path, mode -> + coroutineScope.launch { + isLoading = true + val success = if (editingUmount != null) { + SuSFSManager.editTryUmount(context, editingUmount!!, path, mode) + } else { + SuSFSManager.addTryUmount(context, path, mode) + } + if (success) { + tryUmounts = SuSFSManager.getTryUmounts(context) + } + isLoading = false + showAddUmountDialog = false + editingUmount = null + } + }, + isLoading = isLoading, + initialPath = editingUmount?.split("|")?.get(0) ?: "", + initialMode = editingUmount?.split("|")?.get(1)?.toIntOrNull() ?: 0 + ) + + AddKstatStaticallyDialog( + showDialog = showAddKstatStaticallyDialog, + onDismiss = { + showAddKstatStaticallyDialog = false + editingKstatConfig = null + }, + onConfirm = { path, ino, dev, nlink, size, atime, atimeNsec, mtime, mtimeNsec, ctime, ctimeNsec, blocks, blksize -> + coroutineScope.launch { + isLoading = true + val success = if (editingKstatConfig != null) { + SuSFSManager.editKstatConfig( + context, + editingKstatConfig!!, + path, + ino, + dev, + nlink, + size, + atime, + atimeNsec, + mtime, + mtimeNsec, + ctime, + ctimeNsec, + blocks, + blksize + ) + } else { + SuSFSManager.addKstatStatically( + context, path, ino, dev, nlink, size, atime, atimeNsec, + mtime, mtimeNsec, ctime, ctimeNsec, blocks, blksize + ) + } + if (success) { + kstatConfigs = SuSFSManager.getKstatConfigs(context) + } + isLoading = false + showAddKstatStaticallyDialog = false + editingKstatConfig = null + } + }, + isLoading = isLoading, + initialConfig = editingKstatConfig ?: "" + ) + + AddPathDialog( + showDialog = showAddKstatDialog, + onDismiss = { + showAddKstatDialog = false + editingKstatPath = null + }, + onConfirm = { path -> + coroutineScope.launch { + isLoading = true + val success = if (editingKstatPath != null) { + SuSFSManager.editAddKstat(context, editingKstatPath!!, path) + } else { + SuSFSManager.addKstat(context, path) + } + if (success) { + addKstatPaths = SuSFSManager.getAddKstatPaths(context) + } + isLoading = false + showAddKstatDialog = false + editingKstatPath = null + } + }, + isLoading = isLoading, + titleRes = if (editingKstatPath != null) R.string.edit_kstat_path_title else R.string.add_kstat_path_title, + labelRes = R.string.file_or_directory_path_label, + initialValue = editingKstatPath ?: "" + ) + + // 确认对话框 + ConfirmDialog( + showDialog = showConfirmReset, + onDismiss = { showConfirmReset = false }, + onConfirm = { + showConfirmReset = false + coroutineScope.launch { + isLoading = true + if (SuSFSManager.resetToDefault(context)) { + unameValue = "default" + buildTimeValue = "default" + autoStartEnabled = false + } + isLoading = false + } + }, + titleRes = R.string.susfs_reset_confirm_title, + messageRes = R.string.susfs_reset_confirm_title, + isLoading = isLoading + ) + + // 重置对话框 + ConfirmDialog( + showDialog = showResetPathsDialog, + onDismiss = { showResetPathsDialog = false }, + onConfirm = { + coroutineScope.launch { + isLoading = true + SuSFSManager.saveSusPaths(context, emptySet()) + susPaths = emptySet() + if (SuSFSManager.isAutoStartEnabled(context)) { + SuSFSManager.configureAutoStart(context, true) + } + isLoading = false + showResetPathsDialog = false + } + }, + titleRes = R.string.susfs_reset_paths_title, + messageRes = R.string.susfs_reset_paths_message, + isLoading = isLoading + ) + + ConfirmDialog( + showDialog = showResetLoopPathsDialog, + onDismiss = { showResetLoopPathsDialog = false }, + onConfirm = { + coroutineScope.launch { + isLoading = true + SuSFSManager.saveSusLoopPaths(context, emptySet()) + susLoopPaths = emptySet() + if (SuSFSManager.isAutoStartEnabled(context)) { + SuSFSManager.configureAutoStart(context, true) + } + isLoading = false + showResetLoopPathsDialog = false + } + }, + titleRes = R.string.susfs_reset_loop_paths_title, + messageRes = R.string.susfs_reset_loop_paths_message, + isLoading = isLoading + ) + + ConfirmDialog( + showDialog = showResetSusMapsDialog, + onDismiss = { showResetSusMapsDialog = false }, + onConfirm = { + coroutineScope.launch { + isLoading = true + SuSFSManager.saveSusMaps(context, emptySet()) + susMaps = emptySet() + if (SuSFSManager.isAutoStartEnabled(context)) { + SuSFSManager.configureAutoStart(context, true) + } + isLoading = false + showResetSusMapsDialog = false + } + }, + titleRes = R.string.susfs_reset_sus_maps_title, + messageRes = R.string.susfs_reset_sus_maps_message, + isLoading = isLoading + ) + + ConfirmDialog( + showDialog = showResetMountsDialog, + onDismiss = { showResetMountsDialog = false }, + onConfirm = { + coroutineScope.launch { + isLoading = true + SuSFSManager.saveSusMounts(context, emptySet()) + susMounts = emptySet() + if (SuSFSManager.isAutoStartEnabled(context)) { + SuSFSManager.configureAutoStart(context, true) + } + isLoading = false + showResetMountsDialog = false + } + }, + titleRes = R.string.susfs_reset_mounts_title, + messageRes = R.string.susfs_reset_mounts_message, + isLoading = isLoading + ) + + ConfirmDialog( + showDialog = showResetUmountsDialog, + onDismiss = { showResetUmountsDialog = false }, + onConfirm = { + coroutineScope.launch { + isLoading = true + SuSFSManager.saveTryUmounts(context, emptySet()) + tryUmounts = emptySet() + if (SuSFSManager.isAutoStartEnabled(context)) { + SuSFSManager.configureAutoStart(context, true) + } + isLoading = false + showResetUmountsDialog = false + } + }, + titleRes = R.string.susfs_reset_umounts_title, + messageRes = R.string.susfs_reset_umounts_message, + isLoading = isLoading + ) + + ConfirmDialog( + showDialog = showResetKstatDialog, + onDismiss = { showResetKstatDialog = false }, + onConfirm = { + coroutineScope.launch { + isLoading = true + SuSFSManager.saveKstatConfigs(context, emptySet()) + SuSFSManager.saveAddKstatPaths(context, emptySet()) + kstatConfigs = emptySet() + addKstatPaths = emptySet() + if (SuSFSManager.isAutoStartEnabled(context)) { + SuSFSManager.configureAutoStart(context, true) + } + isLoading = false + showResetKstatDialog = false + } + }, + titleRes = R.string.reset_kstat_config_title, + messageRes = R.string.reset_kstat_config_message, + isLoading = isLoading + ) + + // 主界面布局 + Scaffold( + containerColor = wallpaperContainerColor(), + topBar = { + TopAppBar( + modifier = Modifier.hazeEffect(hazeState) { + style = hazeStyle + blurRadius = 30.dp + noiseFactor = 0f + }, + color = Color.Transparent, + title = stringResource(R.string.susfs_config_title), + navigationIcon = { + IconButton(onClick = { + if (!isNavigating) { + isNavigating = true + navigator.popBackStack() + } + }) { + Icon( + MiuixIcons.Useful.Back, + contentDescription = stringResource(R.string.log_viewer_back), + tint = colorScheme.onBackground + ) + } + }, + scrollBehavior = scrollBehavior + ) + }, + popupHost = { }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) + ) { innerPadding -> + LazyColumn( + modifier = Modifier + .height(getWindowSize().height.dp) + .scrollEndHaptic() + .overScrollVertical() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .hazeSource(state = hazeState) + .padding(horizontal = 12.dp), + contentPadding = innerPadding, + overscrollEffect = null, + ) { + item { + // 标签页 + LazyRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp), + contentPadding = PaddingValues(vertical = 8.dp) + ) { + items(allTabs.size) { index -> + val tab = allTabs[index] + val isSelected = selectedTab == tab + Card( + modifier = Modifier + .clickable { selectedTab = tab }, + colors = wallpaperCardColors( + background = if (isSelected) { + colorScheme.primaryContainer + } else { + colorScheme.surfaceVariant.copy(alpha = 0.3f) + } + ), + cornerRadius = 8.dp + ) { + Text( + text = stringResource(tab.displayNameRes), + modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp), + style = MiuixTheme.textStyles.body1, + fontWeight = if (isSelected) FontWeight.Medium else FontWeight.Normal, + color = if (isSelected) { + colorScheme.onPrimaryContainer + } else { + colorScheme.onSurfaceVariantSummary + } + ) + } + } + } + + } + + item { + Spacer(modifier = Modifier.height(12.dp)) + + // 标签页内容 + when (selectedTab) { + SuSFSTab.BASIC_SETTINGS -> { + BasicSettingsContent( + unameValue = unameValue, + onUnameValueChange = { value -> unameValue = value }, + buildTimeValue = buildTimeValue, + onBuildTimeValueChange = { value -> buildTimeValue = value }, + executeInPostFsData = executeInPostFsData, + onExecuteInPostFsDataChange = { value -> executeInPostFsData = value }, + autoStartEnabled = autoStartEnabled, + canEnableAutoStart = canEnableAutoStart, + isLoading = isLoading, + onAutoStartToggle = { enabled: Boolean -> + if (canEnableAutoStart) { + coroutineScope.launch { + isLoading = true + if (SuSFSManager.configureAutoStart(context, enabled)) { + autoStartEnabled = enabled + } + isLoading = false + } + } + }, + onShowSlotInfo = { showSlotInfoDialog = true }, + context = context, + onShowBackupDialog = { showBackupDialog = true }, + onShowRestoreDialog = { showRestoreDialog = true }, + enableHideBl = enableHideBl, + onEnableHideBlChange = { enabled: Boolean -> + enableHideBl = enabled + SuSFSManager.saveEnableHideBl(context, enabled) + if (SuSFSManager.isAutoStartEnabled(context)) { + coroutineScope.launch { + SuSFSManager.configureAutoStart(context, true) + } + } + }, + enableCleanupResidue = enableCleanupResidue, + onEnableCleanupResidueChange = { enabled: Boolean -> + enableCleanupResidue = enabled + SuSFSManager.saveEnableCleanupResidue(context, enabled) + if (SuSFSManager.isAutoStartEnabled(context)) { + coroutineScope.launch { + SuSFSManager.configureAutoStart(context, true) + } + } + }, + enableAvcLogSpoofing = enableAvcLogSpoofing, + onEnableAvcLogSpoofingChange = { enabled: Boolean -> + coroutineScope.launch { + isLoading = true + val success = SuSFSManager.setEnableAvcLogSpoofing(context, enabled) + if (success) { + enableAvcLogSpoofing = enabled + } + isLoading = false + } + }, + onReset = { showConfirmReset = true } + ) + } + SuSFSTab.SUS_PATHS -> { + SusPathsContent( + susPaths = susPaths, + isLoading = isLoading, + onAddPath = { showAddPathDialog = true }, + onAddAppPath = { showAddAppPathDialog = true }, + onRemovePath = { path -> + coroutineScope.launch { + isLoading = true + if (SuSFSManager.removeSusPath(context, path)) { + susPaths = SuSFSManager.getSusPaths(context) + } + isLoading = false + } + }, + onEditPath = { path -> + editingPath = path + showAddPathDialog = true + }, + forceRefreshApps = selectedTab == SuSFSTab.SUS_PATHS, + onReset = { showResetPathsDialog = true } + ) + } + SuSFSTab.SUS_LOOP_PATHS -> { + SusLoopPathsContent( + susLoopPaths = susLoopPaths, + isLoading = isLoading, + onAddLoopPath = { showAddLoopPathDialog = true }, + onRemoveLoopPath = { path -> + coroutineScope.launch { + isLoading = true + if (SuSFSManager.removeSusLoopPath(context, path)) { + susLoopPaths = SuSFSManager.getSusLoopPaths(context) + } + isLoading = false + } + }, + onEditLoopPath = { path -> + editingLoopPath = path + showAddLoopPathDialog = true + }, + onReset = { showResetLoopPathsDialog = true } + ) + } + SuSFSTab.SUS_MAPS -> { + SusMapsContent( + susMaps = susMaps, + isLoading = isLoading, + onAddSusMap = { showAddSusMapDialog = true }, + onRemoveSusMap = { map -> + coroutineScope.launch { + isLoading = true + if (SuSFSManager.removeSusMap(context, map)) { + susMaps = SuSFSManager.getSusMaps(context) + } + isLoading = false + } + }, + onEditSusMap = { map -> + editingSusMap = map + showAddSusMapDialog = true + }, + onReset = { showResetSusMapsDialog = true } + ) + } + SuSFSTab.SUS_MOUNTS -> { + SusMountsContent( + susMounts = susMounts, + hideSusMountsForAllProcs = hideSusMountsForAllProcs, + isLoading = isLoading, + onAddMount = { showAddMountDialog = true }, + onRemoveMount = { mount -> + coroutineScope.launch { + isLoading = true + if (SuSFSManager.removeSusMount(context, mount)) { + susMounts = SuSFSManager.getSusMounts(context) + } + isLoading = false + } + }, + onEditMount = { mount -> + editingMount = mount + showAddMountDialog = true + }, + onToggleHideSusMountsForAllProcs = { hideForAll -> + coroutineScope.launch { + isLoading = true + if (SuSFSManager.setHideSusMountsForAllProcs( + context, + hideForAll + ) + ) { + hideSusMountsForAllProcs = hideForAll + } + isLoading = false + } + }, + onReset = { showResetMountsDialog = true } + ) + } + + SuSFSTab.TRY_UMOUNT -> { + TryUmountContent( + tryUmounts = tryUmounts, + umountForZygoteIsoService = umountForZygoteIsoService, + isLoading = isLoading, + onAddUmount = { showAddUmountDialog = true }, + onRemoveUmount = { umountEntry -> + coroutineScope.launch { + isLoading = true + if (SuSFSManager.removeTryUmount(context, umountEntry)) { + tryUmounts = SuSFSManager.getTryUmounts(context) + } + isLoading = false + } + }, + onEditUmount = { umountEntry -> + editingUmount = umountEntry + showAddUmountDialog = true + }, + onToggleUmountForZygoteIsoService = { enabled -> + coroutineScope.launch { + isLoading = true + val success = + SuSFSManager.setUmountForZygoteIsoService(context, enabled) + if (success) { + umountForZygoteIsoService = enabled + } + isLoading = false + } + }, + onReset = { showResetUmountsDialog = true } + ) + } + + SuSFSTab.KSTAT_CONFIG -> { + KstatConfigContent( + kstatConfigs = kstatConfigs, + addKstatPaths = addKstatPaths, + isLoading = isLoading, + onAddKstatStatically = { showAddKstatStaticallyDialog = true }, + onAddKstat = { showAddKstatDialog = true }, + onRemoveKstatConfig = { config -> + coroutineScope.launch { + isLoading = true + if (SuSFSManager.removeKstatConfig(context, config)) { + kstatConfigs = SuSFSManager.getKstatConfigs(context) + } + isLoading = false + } + }, + onEditKstatConfig = { config -> + editingKstatConfig = config + showAddKstatStaticallyDialog = true + }, + onRemoveAddKstat = { path -> + coroutineScope.launch { + isLoading = true + if (SuSFSManager.removeAddKstat(context, path)) { + addKstatPaths = SuSFSManager.getAddKstatPaths(context) + } + isLoading = false + } + }, + onEditAddKstat = { path -> + editingKstatPath = path + showAddKstatDialog = true + }, + onUpdateKstat = { path -> + coroutineScope.launch { + isLoading = true + SuSFSManager.updateKstat(context, path) + isLoading = false + } + }, + onUpdateKstatFullClone = { path -> + coroutineScope.launch { + isLoading = true + SuSFSManager.updateKstatFullClone(context, path) + isLoading = false + } + } + ) + } + SuSFSTab.PATH_SETTINGS -> { + PathSettingsContent( + androidDataPath = androidDataPath, + onAndroidDataPathChange = { androidDataPath = it }, + sdcardPath = sdcardPath, + onSdcardPathChange = { sdcardPath = it }, + isLoading = isLoading, + onSetAndroidDataPath = { + coroutineScope.launch { + isLoading = true + SuSFSManager.setAndroidDataPath(context, androidDataPath.trim()) + isLoading = false + } + }, + onSetSdcardPath = { + coroutineScope.launch { + isLoading = true + SuSFSManager.setSdcardPath(context, sdcardPath.trim()) + isLoading = false + } + }, + onReset = { + androidDataPath = "/sdcard/Android/data" + sdcardPath = "/sdcard" + coroutineScope.launch { + isLoading = true + SuSFSManager.setAndroidDataPath(context, androidDataPath) + SuSFSManager.setSdcardPath(context, sdcardPath) + isLoading = false + } + } + ) + } + SuSFSTab.ENABLED_FEATURES -> { + EnabledFeaturesContent( + enabledFeatures = enabledFeatures, + onRefresh = { loadEnabledFeatures() } + ) + } + } + } + } + } +} + +/** + * 基本设置内容组件 + */ +@Composable +fun BasicSettingsContent( + unameValue: String, + onUnameValueChange: (String) -> Unit, + buildTimeValue: String, + onBuildTimeValueChange: (String) -> Unit, + executeInPostFsData: Boolean, + onExecuteInPostFsDataChange: (Boolean) -> Unit, + autoStartEnabled: Boolean, + canEnableAutoStart: Boolean, + isLoading: Boolean, + onAutoStartToggle: (Boolean) -> Unit, + onShowSlotInfo: () -> Unit, + context: Context, + onShowBackupDialog: () -> Unit, + onShowRestoreDialog: () -> Unit, + enableHideBl: Boolean, + onEnableHideBlChange: (Boolean) -> Unit, + enableCleanupResidue: Boolean, + onEnableCleanupResidueChange: (Boolean) -> Unit, + enableAvcLogSpoofing: Boolean, + onEnableAvcLogSpoofingChange: (Boolean) -> Unit, + onReset: (() -> Unit)? = null +) { + val isAbDevice = produceState(initialValue = false) { + value = isAbDevice() + }.value + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // 说明卡片 + Card( + modifier = Modifier.fillMaxWidth(), + colors = wallpaperCardColors( + background = colorScheme.surfaceVariant.copy(alpha = 0.3f) + ) + ) { + Column( + modifier = Modifier.padding(14.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = stringResource(R.string.susfs_config_description), + style = MiuixTheme.textStyles.title3, + fontWeight = FontWeight.Medium, + color = colorScheme.primary + ) + Text( + text = stringResource(R.string.susfs_config_description_text), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary, + lineHeight = 18.sp + ) + } + } + + // Uname输入框 + TextField( + value = unameValue, + onValueChange = onUnameValueChange, + label = stringResource(R.string.susfs_uname_label), + useLabelAsPlaceholder = true, + modifier = Modifier.fillMaxWidth(), + enabled = !isLoading + ) + + // 构建时间伪装输入框 + TextField( + value = buildTimeValue, + onValueChange = onBuildTimeValueChange, + label = stringResource(R.string.susfs_build_time_label), + useLabelAsPlaceholder = true, + modifier = Modifier.fillMaxWidth(), + enabled = !isLoading + ) + + // 执行位置选择 + val locationItems = listOf( + stringResource(R.string.susfs_execution_location_service), + stringResource(R.string.susfs_execution_location_post_fs_data) + ) + SuperDropdown( + title = stringResource(R.string.susfs_execution_location_label), + summary = if (executeInPostFsData) { + stringResource(R.string.susfs_execution_location_post_fs_data) + } else { + stringResource(R.string.susfs_execution_location_service) + }, + items = locationItems, + selectedIndex = if (executeInPostFsData) 1 else 0, + onSelectedIndexChange = { index -> + onExecuteInPostFsDataChange(index == 1) + }, + enabled = !isLoading + ) + + // 当前值显示 + Card( + modifier = Modifier.fillMaxWidth(), + colors = wallpaperCardColors( + background = colorScheme.surfaceVariant.copy(alpha = 0.2f) + ) + ) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = stringResource(R.string.susfs_current_value, SuSFSManager.getUnameValue(context)), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary + ) + Text( + text = stringResource(R.string.susfs_current_build_time, SuSFSManager.getBuildTimeValue(context)), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary + ) + Text( + text = stringResource(R.string.susfs_current_execution_location, if (SuSFSManager.getExecuteInPostFsData(context)) "Post-FS-Data" else "Service"), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary + ) + } + } + + // 开机自启动开关 + Card( + modifier = Modifier.fillMaxWidth(), + colors = wallpaperCardColors() + ) { + SuperSwitch( + title = stringResource(R.string.susfs_autostart_title), + summary = if (canEnableAutoStart) { + stringResource(R.string.susfs_autostart_description) + } else { + stringResource(R.string.susfs_autostart_requirement) + }, + leftAction = { + Icon( + Icons.Default.AutoMode, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(R.string.susfs_autostart_title), + tint = if (canEnableAutoStart) colorScheme.onBackground else colorScheme.onSurfaceVariantSummary + ) + }, + checked = autoStartEnabled, + onCheckedChange = onAutoStartToggle, + enabled = !isLoading && canEnableAutoStart + ) + } + + // 隐藏BL脚本开关 + Card( + modifier = Modifier.fillMaxWidth(), + colors = wallpaperCardColors() + ) { + SuperSwitch( + title = stringResource(R.string.hide_bl_script), + summary = stringResource(R.string.hide_bl_script_description), + leftAction = { + Icon( + Icons.Default.Security, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(R.string.hide_bl_script), + tint = colorScheme.onBackground + ) + }, + checked = enableHideBl, + onCheckedChange = onEnableHideBlChange, + enabled = !isLoading + ) + } + + // 清理残留脚本开关 + Card( + modifier = Modifier.fillMaxWidth(), + colors = wallpaperCardColors() + ) { + SuperSwitch( + title = stringResource(R.string.cleanup_residue), + summary = stringResource(R.string.cleanup_residue_description), + leftAction = { + Icon( + Icons.Default.CleaningServices, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(R.string.cleanup_residue), + tint = colorScheme.onBackground + ) + }, + checked = enableCleanupResidue, + onCheckedChange = onEnableCleanupResidueChange, + enabled = !isLoading + ) + } + + // AVC日志欺骗开关 + Card( + modifier = Modifier.fillMaxWidth(), + colors = wallpaperCardColors() + ) { + SuperSwitch( + title = stringResource(R.string.avc_log_spoofing), + summary = stringResource(R.string.avc_log_spoofing_description), + leftAction = { + Icon( + Icons.Default.VisibilityOff, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(R.string.avc_log_spoofing), + tint = colorScheme.onBackground + ) + }, + checked = enableAvcLogSpoofing, + onCheckedChange = onEnableAvcLogSpoofingChange, + enabled = !isLoading + ) + } + + // 槽位信息按钮 + if (isAbDevice) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = wallpaperCardColors() + ) { + Column( + modifier = Modifier.padding(14.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Default.Info, + contentDescription = null, + tint = colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(10.dp)) + Text( + text = stringResource(R.string.susfs_slot_info_title), + style = MiuixTheme.textStyles.title3, + fontWeight = FontWeight.Medium, + color = colorScheme.onBackground + ) + } + Text( + text = stringResource(R.string.susfs_slot_info_description), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary, + lineHeight = 18.sp + ) + TextButton( + text = stringResource(R.string.susfs_slot_info_title), + onClick = onShowSlotInfo, + enabled = !isLoading, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 48.dp) + .padding(vertical = 8.dp) + ) + } + } + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + // 备份按钮 + TextButton( + text = stringResource(R.string.susfs_backup_title), + onClick = onShowBackupDialog, + enabled = !isLoading, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp) + ) + // 还原按钮 + TextButton( + text = stringResource(R.string.susfs_restore_title), + onClick = onShowRestoreDialog, + enabled = !isLoading, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp) + ) + } + + // 重置按钮 + if (onReset != null) { + Spacer(modifier = Modifier.height(8.dp)) + Card( + onClick = onReset, + modifier = Modifier.fillMaxWidth(), + colors = wallpaperCardColors( + background = colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + cornerRadius = 8.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.RestoreFromTrash, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = colorScheme.primary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.susfs_reset_confirm_title), + style = MiuixTheme.textStyles.body1, + fontWeight = FontWeight.Medium, + color = colorScheme.primary + ) + } + } + } + } +} + +/** + * 槽位信息对话框 + */ +@Composable +fun SlotInfoDialog( + showDialog: Boolean, + onDismiss: () -> Unit, + slotInfoList: List, + currentActiveSlot: String, + isLoadingSlotInfo: Boolean, + onRefresh: () -> Unit, + onUseUname: (String) -> Unit, + onUseBuildTime: (String) -> Unit +) { + val isAbDevice = produceState(initialValue = false) { + value = isAbDevice() + }.value + + val showDialogState = remember { mutableStateOf(showDialog && isAbDevice) } + + LaunchedEffect(showDialog, isAbDevice) { + showDialogState.value = showDialog && isAbDevice + } + + if (showDialogState.value) { + SuperDialog( + show = showDialogState, + title = stringResource(R.string.susfs_slot_info_title), + onDismissRequest = onDismiss, + content = { + Column( + modifier = Modifier.padding(horizontal = 24.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = stringResource(R.string.susfs_current_active_slot, currentActiveSlot), + style = MiuixTheme.textStyles.body1, + fontWeight = FontWeight.Medium, + color = colorScheme.primary + ) + + if (slotInfoList.isNotEmpty()) { + slotInfoList.forEach { slotInfo -> + Card( + modifier = Modifier.fillMaxWidth(), + colors = wallpaperCardColors( + background = if (slotInfo.slotName == currentActiveSlot) { + colorScheme.primary.copy(alpha = 0.1f) + } else { + colorScheme.surface.copy(alpha = 0.5f) + } + ), + cornerRadius = 8.dp + ) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Storage, + contentDescription = null, + tint = if (slotInfo.slotName == currentActiveSlot) { + colorScheme.primary + } else { + colorScheme.onSurface + }, + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = slotInfo.slotName, + style = MiuixTheme.textStyles.title2, + fontWeight = FontWeight.Bold, + color = if (slotInfo.slotName == currentActiveSlot) { + colorScheme.primary + } else { + colorScheme.onSurface + } + ) + if (slotInfo.slotName == currentActiveSlot) { + Spacer(modifier = Modifier.width(6.dp)) + Box( + modifier = Modifier + .background( + color = colorScheme.primary, + shape = RoundedCornerShape(4.dp) + ) + .padding(horizontal = 6.dp, vertical = 2.dp) + ) { + Text( + text = stringResource(R.string.susfs_slot_current_badge), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onPrimary + ) + } + } + } + Text( + text = stringResource(R.string.susfs_slot_uname, slotInfo.uname), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurface + ) + Text( + text = stringResource(R.string.susfs_slot_build_time, slotInfo.buildTime), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurface + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Button( + onClick = { onUseUname(slotInfo.uname) }, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + cornerRadius = 8.dp + ) { + Text( + text = stringResource(R.string.susfs_slot_use_uname), + style = MiuixTheme.textStyles.body2, + maxLines = 2 + ) + } + Button( + onClick = { onUseBuildTime(slotInfo.buildTime) }, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + cornerRadius = 8.dp + ) { + Text( + text = stringResource(R.string.susfs_slot_use_build_time), + style = MiuixTheme.textStyles.body2, + maxLines = 2 + ) + } + } + } + } + } + } else { + Text( + text = stringResource(R.string.susfs_slot_info_unavailable), + style = MiuixTheme.textStyles.body2, + color = colorScheme.error + ) + } + + Spacer(modifier = Modifier.height(12.dp)) + + Button( + onClick = onRefresh, + enabled = !isLoadingSlotInfo, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + cornerRadius = 8.dp + ) { + Text( + text = stringResource(R.string.refresh), + style = MiuixTheme.textStyles.body2, + maxLines = 2 + ) + } + + TextButton( + text = stringResource(android.R.string.cancel), + onClick = onDismiss, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 48.dp) + .padding(vertical = 8.dp) + ) + } + } + ) + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigDialogs.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigDialogs.kt new file mode 100644 index 00000000..71d5a63f --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigDialogs.kt @@ -0,0 +1,1891 @@ +package com.sukisu.ultra.ui.susfs.component + +import android.annotation.SuppressLint +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import android.util.Log +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.background +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.runtime.* +import androidx.compose.runtime.snapshotFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.core.graphics.drawable.toBitmap +import com.sukisu.ultra.R +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import com.sukisu.ultra.ui.susfs.util.SuSFSManager +import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel +import kotlinx.coroutines.launch +import top.yukonga.miuix.kmp.basic.Button +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TextButton +import top.yukonga.miuix.kmp.basic.TextField +import top.yukonga.miuix.kmp.extra.SuperDialog +import top.yukonga.miuix.kmp.extra.SuperDropdown +import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.basic.CardDefaults +import top.yukonga.miuix.kmp.basic.Switch + +/** + * 添加路径对话框 + */ +@Composable +fun AddPathDialog( + showDialog: Boolean, + onDismiss: () -> Unit, + onConfirm: (String) -> Unit, + isLoading: Boolean, + titleRes: Int, + labelRes: Int, + initialValue: String = "" +) { + var newPath by remember { mutableStateOf("") } + + // 当对话框显示时,设置初始值 + LaunchedEffect(showDialog, initialValue) { + if (showDialog) { + newPath = initialValue + } + } + + val showDialogState = remember { mutableStateOf(showDialog) } + + LaunchedEffect(showDialog) { + showDialogState.value = showDialog + } + + if (showDialogState.value) { + SuperDialog( + show = showDialogState, + title = stringResource(titleRes), + onDismissRequest = { + onDismiss() + newPath = "" + }, + content = { + Column( + modifier = Modifier + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + TextField( + value = newPath, + onValueChange = { value -> newPath = value }, + label = stringResource(labelRes), + useLabelAsPlaceholder = true, + modifier = Modifier.fillMaxWidth(), + enabled = !isLoading + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + TextButton( + text = stringResource(android.R.string.cancel), + onClick = { + onDismiss() + newPath = "" + }, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp) + ) + Button( + onClick = { + if (newPath.isNotBlank()) { + onConfirm(newPath.trim()) + newPath = "" + } + }, + enabled = newPath.isNotBlank() && !isLoading, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + cornerRadius = 8.dp + ) { + Text( + text = stringResource(if (initialValue.isNotEmpty()) R.string.susfs_save else R.string.add), + style = MiuixTheme.textStyles.body2, + maxLines = 2 + ) + } + } + } + } + ) + } +} + +/** + * 快捷添加应用路径对话框 + */ +@Composable +fun AddAppPathDialog( + showDialog: Boolean, + onDismiss: () -> Unit, + onConfirm: (List) -> Unit, + isLoading: Boolean, + apps: List = emptyList(), + onLoadApps: () -> Unit, + existingSusPaths: Set = emptySet() +) { + var searchText by remember { mutableStateOf("") } + var selectedApps by remember { mutableStateOf(setOf()) } + + // 获取已添加的包名 + val addedPackageNames = remember(existingSusPaths) { + existingSusPaths.mapNotNull { path -> + val regex = Regex(".*/Android/data/([^/]+)/?.*") + regex.find(path)?.groupValues?.get(1) + }.toSet() + } + + // 过滤掉已添加的应用 + val availableApps = remember(apps, addedPackageNames) { + apps.filter { app -> + !addedPackageNames.contains(app.packageName) + } + } + + val filteredApps = remember(availableApps, searchText) { + if (searchText.isBlank()) { + availableApps + } else { + availableApps.filter { app -> + app.appName.contains(searchText, ignoreCase = true) || + app.packageName.contains(searchText, ignoreCase = true) + } + } + } + + LaunchedEffect(showDialog) { + if (showDialog && apps.isEmpty()) { + onLoadApps() + } + // 当对话框显示时清空选择 + if (showDialog) { + selectedApps = setOf() + } + } + + val showDialogState = remember { mutableStateOf(showDialog) } + + LaunchedEffect(showDialog) { + showDialogState.value = showDialog + } + + if (showDialogState.value) { + SuperDialog( + show = showDialogState, + title = stringResource(R.string.susfs_add_app_path), + onDismissRequest = { + onDismiss() + selectedApps = setOf() + searchText = "" + }, + content = { + Column( + modifier = Modifier + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + TextField( + value = searchText, + onValueChange = { searchText = it }, + label = stringResource(R.string.search_apps), + leadingIcon = { + Icon( + imageVector = Icons.Default.Search, + contentDescription = null + ) + }, + modifier = Modifier.fillMaxWidth() + ) + + // 显示统计信息 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + if (selectedApps.isNotEmpty()) { + Text( + text = stringResource(R.string.selected_apps_count, selectedApps.size), + style = MiuixTheme.textStyles.body2, + color = colorScheme.primary, + fontWeight = FontWeight.Medium + ) + } + if (addedPackageNames.isNotEmpty()) { + Text( + text = stringResource(R.string.already_added_apps_count, addedPackageNames.size), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary + ) + } + } + + if (filteredApps.isEmpty()) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.defaultColors( + color = colorScheme.surfaceVariant.copy(alpha = 0.2f) + ) + ) { + Text( + text = if (availableApps.isEmpty()) { + stringResource(R.string.all_apps_already_added) + } else { + stringResource(R.string.no_apps_found) + }, + modifier = Modifier.padding(16.dp), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary, + textAlign = TextAlign.Center + ) + } + } else { + LazyColumn( + modifier = Modifier.height(300.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(filteredApps) { app -> + val isSelected = selectedApps.contains(app) + + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.defaultColors( + color = if (isSelected) { + colorScheme.primaryContainer.copy(alpha = 0.6f) + } else { + colorScheme.surface + } + ), + onClick = { + selectedApps = if (isSelected) { + selectedApps - app + } else { + selectedApps + app + } + }, + cornerRadius = 8.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 应用图标 + AppIcon( + packageName = app.packageName, + packageInfo = app.packageInfo, + modifier = Modifier.size(40.dp) + ) + + Column( + modifier = Modifier + .weight(1f) + .padding(start = 12.dp) + ) { + Text( + text = app.appName, + style = MiuixTheme.textStyles.body1, + fontWeight = FontWeight.Medium, + color = if (isSelected) { + colorScheme.onPrimaryContainer + } else { + colorScheme.onSurface + } + ) + Text( + text = app.packageName, + style = MiuixTheme.textStyles.body2, + color = if (isSelected) { + colorScheme.onPrimaryContainer.copy(alpha = 0.7f) + } else { + colorScheme.onSurfaceVariantSummary + } + ) + } + + // 选择指示器 + if (isSelected) { + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = null, + tint = colorScheme.primary, + modifier = Modifier.size(24.dp) + ) + } else { + Icon( + imageVector = Icons.Default.RadioButtonUnchecked, + contentDescription = null, + tint = colorScheme.onSurfaceVariantSummary, + modifier = Modifier.size(24.dp) + ) + } + } + } + } + } + } + + // 按钮 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + TextButton( + text = stringResource(R.string.cancel), + onClick = { + onDismiss() + selectedApps = setOf() + searchText = "" + }, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp) + ) + Button( + onClick = { + if (selectedApps.isNotEmpty()) { + onConfirm(selectedApps.map { it.packageName }) + } + selectedApps = setOf() + searchText = "" + }, + enabled = selectedApps.isNotEmpty() && !isLoading, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + cornerRadius = 8.dp + ) { + Text( + text = stringResource(R.string.add), + style = MiuixTheme.textStyles.body2, + maxLines = 2 + ) + } + } + } + } + ) + } +} + + +/** + * 应用图标组件 + */ +@Composable +fun AppIcon( + packageName: String, + packageInfo: PackageInfo? = null, + @SuppressLint("ModifierParameter") modifier: Modifier = Modifier +) { + val context = LocalContext.current + var iconBitmap by remember(packageName, packageInfo) { mutableStateOf(null) } + var isLoadingIcon by remember(packageName, packageInfo) { mutableStateOf(true) } + + LaunchedEffect(packageName, packageInfo) { + isLoadingIcon = true + iconBitmap = null + + withContext(Dispatchers.IO) { + try { + val drawable = when { + packageInfo != null -> { + packageInfo.applicationInfo?.loadIcon(context.packageManager) + } + else -> { + val cachedInfo = AppInfoCache.getAppInfo(packageName) + if (cachedInfo?.drawable != null) { + cachedInfo.drawable + } else if (cachedInfo?.packageInfo != null) { + cachedInfo.packageInfo.applicationInfo?.loadIcon(context.packageManager) + } else { + // 尝试从 PackageManager 获取 + try { + val packageManager = context.packageManager + val applicationInfo = packageManager.getApplicationInfo(packageName, 0) + val icon = packageManager.getApplicationIcon(applicationInfo) + // 更新缓存 + val newCachedInfo = AppInfoCache.CachedAppInfo( + appName = packageName, + packageInfo = null, + drawable = icon + ) + AppInfoCache.putAppInfo(packageName, newCachedInfo) + icon + } catch (e: Exception) { + Log.d("AppIcon", "获取应用图标失败: $packageName", e) + null + } + } + } + } + + iconBitmap = drawable?.toBitmap()?.asImageBitmap() + } catch (e: Exception) { + Log.d("AppIcon", "获取应用图标失败: $packageName", e) + } finally { + isLoadingIcon = false + } + } + } + + Box( + modifier = modifier + .clip(RoundedCornerShape(8.dp)) + .background( + if (iconBitmap == null) colorScheme.surfaceVariant.copy(alpha = 0.3f) + else androidx.compose.ui.graphics.Color.Transparent + ), + contentAlignment = Alignment.Center + ) { + if (iconBitmap != null) { + Image( + bitmap = iconBitmap!!, + contentDescription = null, + modifier = Modifier.fillMaxSize() + ) + } else if (!isLoadingIcon) { + // 显示占位图标 + Icon( + imageVector = Icons.Default.Android, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = colorScheme.onSurfaceVariantSummary.copy(alpha = 0.6f) + ) + } + } +} + + +/** + * 添加尝试卸载对话框 + */ +@Composable +fun AddTryUmountDialog( + showDialog: Boolean, + onDismiss: () -> Unit, + onConfirm: (String, Int) -> Unit, + isLoading: Boolean, + initialPath: String = "", + initialMode: Int = 0 +) { + var newUmountPath by remember { mutableStateOf("") } + var newUmountMode by remember { mutableIntStateOf(0) } + + // 当对话框显示时,设置初始值 + LaunchedEffect(showDialog, initialPath, initialMode) { + if (showDialog) { + newUmountPath = initialPath + newUmountMode = initialMode + } + } + + val showDialogState = remember { mutableStateOf(showDialog) } + + LaunchedEffect(showDialog) { + showDialogState.value = showDialog + } + + val umountModeItems = listOf( + stringResource(R.string.susfs_umount_mode_normal), + stringResource(R.string.susfs_umount_mode_detach) + ) + + if (showDialogState.value) { + SuperDialog( + show = showDialogState, + title = stringResource(if (initialPath.isNotEmpty()) R.string.susfs_edit_try_umount else R.string.susfs_add_try_umount), + onDismissRequest = { + onDismiss() + newUmountPath = "" + newUmountMode = 0 + }, + content = { + Column( + modifier = Modifier + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + TextField( + value = newUmountPath, + onValueChange = { newUmountPath = it }, + label = stringResource(R.string.susfs_path_label), + useLabelAsPlaceholder = true, + modifier = Modifier.fillMaxWidth(), + enabled = !isLoading + ) + + SuperDropdown( + title = stringResource(R.string.susfs_umount_mode_label), + summary = umountModeItems[newUmountMode], + items = umountModeItems, + selectedIndex = newUmountMode, + onSelectedIndexChange = { newUmountMode = it }, + enabled = !isLoading + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + TextButton( + text = stringResource(R.string.cancel), + onClick = { + onDismiss() + newUmountPath = "" + newUmountMode = 0 + }, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp) + ) + Button( + onClick = { + if (newUmountPath.isNotBlank()) { + onConfirm(newUmountPath.trim(), newUmountMode) + newUmountPath = "" + newUmountMode = 0 + } + }, + enabled = newUmountPath.isNotBlank() && !isLoading, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + cornerRadius = 8.dp + ) { + Text( + text = stringResource(if (initialPath.isNotEmpty()) R.string.susfs_save else R.string.add), + style = MiuixTheme.textStyles.body2, + maxLines = 2 + ) + } + } + } + } + ) + } +} + +/** + * 添加Kstat静态配置对话框 + */ +@Composable +fun AddKstatStaticallyDialog( + showDialog: Boolean, + onDismiss: () -> Unit, + onConfirm: (String, String, String, String, String, String, String, String, String, String, String, String, String) -> Unit, + isLoading: Boolean, + initialConfig: String = "" +) { + var newKstatPath by remember { mutableStateOf("") } + var newKstatIno by remember { mutableStateOf("") } + var newKstatDev by remember { mutableStateOf("") } + var newKstatNlink by remember { mutableStateOf("") } + var newKstatSize by remember { mutableStateOf("") } + var newKstatAtime by remember { mutableStateOf("") } + var newKstatAtimeNsec by remember { mutableStateOf("") } + var newKstatMtime by remember { mutableStateOf("") } + var newKstatMtimeNsec by remember { mutableStateOf("") } + var newKstatCtime by remember { mutableStateOf("") } + var newKstatCtimeNsec by remember { mutableStateOf("") } + var newKstatBlocks by remember { mutableStateOf("") } + var newKstatBlksize by remember { mutableStateOf("") } + + // 当对话框显示时,解析初始配置 + LaunchedEffect(showDialog, initialConfig) { + if (showDialog && initialConfig.isNotEmpty()) { + val parts = initialConfig.split("|") + if (parts.size >= 13) { + newKstatPath = parts[0] + newKstatIno = if (parts[1] == "default") "" else parts[1] + newKstatDev = if (parts[2] == "default") "" else parts[2] + newKstatNlink = if (parts[3] == "default") "" else parts[3] + newKstatSize = if (parts[4] == "default") "" else parts[4] + newKstatAtime = if (parts[5] == "default") "" else parts[5] + newKstatAtimeNsec = if (parts[6] == "default") "" else parts[6] + newKstatMtime = if (parts[7] == "default") "" else parts[7] + newKstatMtimeNsec = if (parts[8] == "default") "" else parts[8] + newKstatCtime = if (parts[9] == "default") "" else parts[9] + newKstatCtimeNsec = if (parts[10] == "default") "" else parts[10] + newKstatBlocks = if (parts[11] == "default") "" else parts[11] + newKstatBlksize = if (parts[12] == "default") "" else parts[12] + } + } else if (showDialog && initialConfig.isEmpty()) { + // 清空所有字段 + newKstatPath = "" + newKstatIno = "" + newKstatDev = "" + newKstatNlink = "" + newKstatSize = "" + newKstatAtime = "" + newKstatAtimeNsec = "" + newKstatMtime = "" + newKstatMtimeNsec = "" + newKstatCtime = "" + newKstatCtimeNsec = "" + newKstatBlocks = "" + newKstatBlksize = "" + } + } + + val showDialogState = remember { mutableStateOf(showDialog) } + + LaunchedEffect(showDialog) { + showDialogState.value = showDialog + } + + if (showDialogState.value) { + SuperDialog( + show = showDialogState, + title = stringResource(if (initialConfig.isNotEmpty()) R.string.edit_kstat_statically_title else R.string.add_kstat_statically_title), + onDismissRequest = { + onDismiss() + // 清空所有字段 + newKstatPath = "" + newKstatIno = "" + newKstatDev = "" + newKstatNlink = "" + newKstatSize = "" + newKstatAtime = "" + newKstatAtimeNsec = "" + newKstatMtime = "" + newKstatMtimeNsec = "" + newKstatCtime = "" + newKstatCtimeNsec = "" + newKstatBlocks = "" + newKstatBlksize = "" + }, + content = { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + TextField( + value = newKstatPath, + onValueChange = { newKstatPath = it }, + label = stringResource(R.string.file_or_directory_path_label), + useLabelAsPlaceholder = true, + modifier = Modifier.fillMaxWidth(), + enabled = !isLoading + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + TextField( + value = newKstatIno, + onValueChange = { newKstatIno = it }, + label = "ino", + useLabelAsPlaceholder = true, + modifier = Modifier.weight(1f), + enabled = !isLoading + ) + TextField( + value = newKstatDev, + onValueChange = { newKstatDev = it }, + label = "dev", + useLabelAsPlaceholder = true, + modifier = Modifier.weight(1f), + enabled = !isLoading + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + TextField( + value = newKstatNlink, + onValueChange = { newKstatNlink = it }, + label = "nlink", + useLabelAsPlaceholder = true, + modifier = Modifier.weight(1f), + enabled = !isLoading + ) + TextField( + value = newKstatSize, + onValueChange = { newKstatSize = it }, + label = "size", + useLabelAsPlaceholder = true, + modifier = Modifier.weight(1f), + enabled = !isLoading + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + TextField( + value = newKstatAtime, + onValueChange = { newKstatAtime = it }, + label = "atime", + useLabelAsPlaceholder = true, + modifier = Modifier.weight(1f), + enabled = !isLoading + ) + TextField( + value = newKstatAtimeNsec, + onValueChange = { newKstatAtimeNsec = it }, + label = "atime_nsec", + useLabelAsPlaceholder = true, + modifier = Modifier.weight(1f), + enabled = !isLoading + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + TextField( + value = newKstatMtime, + onValueChange = { newKstatMtime = it }, + label = "mtime", + useLabelAsPlaceholder = true, + modifier = Modifier.weight(1f), + enabled = !isLoading + ) + TextField( + value = newKstatMtimeNsec, + onValueChange = { newKstatMtimeNsec = it }, + label = "mtime_nsec", + useLabelAsPlaceholder = true, + modifier = Modifier.weight(1f), + enabled = !isLoading + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + TextField( + value = newKstatCtime, + onValueChange = { newKstatCtime = it }, + label = "ctime", + useLabelAsPlaceholder = true, + modifier = Modifier.weight(1f), + enabled = !isLoading + ) + TextField( + value = newKstatCtimeNsec, + onValueChange = { newKstatCtimeNsec = it }, + label = "ctime_nsec", + useLabelAsPlaceholder = true, + modifier = Modifier.weight(1f), + enabled = !isLoading + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + TextField( + value = newKstatBlocks, + onValueChange = { newKstatBlocks = it }, + label = "blocks", + useLabelAsPlaceholder = true, + modifier = Modifier.weight(1f), + enabled = !isLoading + ) + TextField( + value = newKstatBlksize, + onValueChange = { newKstatBlksize = it }, + label = "blksize", + useLabelAsPlaceholder = true, + modifier = Modifier.weight(1f), + enabled = !isLoading + ) + } + + Text( + text = stringResource(R.string.hint_use_default_value), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + TextButton( + text = stringResource(R.string.cancel), + onClick = { + onDismiss() + // 清空所有字段 + newKstatPath = "" + newKstatIno = "" + newKstatDev = "" + newKstatNlink = "" + newKstatSize = "" + newKstatAtime = "" + newKstatAtimeNsec = "" + newKstatMtime = "" + newKstatMtimeNsec = "" + newKstatCtime = "" + newKstatCtimeNsec = "" + newKstatBlocks = "" + newKstatBlksize = "" + }, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp) + ) + Button( + onClick = { + if (newKstatPath.isNotBlank()) { + onConfirm( + newKstatPath.trim(), + newKstatIno.trim().ifBlank { "default" }, + newKstatDev.trim().ifBlank { "default" }, + newKstatNlink.trim().ifBlank { "default" }, + newKstatSize.trim().ifBlank { "default" }, + newKstatAtime.trim().ifBlank { "default" }, + newKstatAtimeNsec.trim().ifBlank { "default" }, + newKstatMtime.trim().ifBlank { "default" }, + newKstatMtimeNsec.trim().ifBlank { "default" }, + newKstatCtime.trim().ifBlank { "default" }, + newKstatCtimeNsec.trim().ifBlank { "default" }, + newKstatBlocks.trim().ifBlank { "default" }, + newKstatBlksize.trim().ifBlank { "default" } + ) + // 清空所有字段 + newKstatPath = "" + newKstatIno = "" + newKstatDev = "" + newKstatNlink = "" + newKstatSize = "" + newKstatAtime = "" + newKstatAtimeNsec = "" + newKstatMtime = "" + newKstatMtimeNsec = "" + newKstatCtime = "" + newKstatCtimeNsec = "" + newKstatBlocks = "" + newKstatBlksize = "" + } + }, + enabled = newKstatPath.isNotBlank() && !isLoading, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + cornerRadius = 8.dp + ) { + Text( + text = stringResource(if (initialConfig.isNotEmpty()) R.string.susfs_save else R.string.add), + style = MiuixTheme.textStyles.body2, + maxLines = 2 + ) + } + } + } + } + ) + } +} + +/** + * 确认对话框 + */ +@Composable +fun ConfirmDialog( + showDialog: Boolean, + onDismiss: () -> Unit, + onConfirm: () -> Unit, + titleRes: Int, + messageRes: Int, + isLoading: Boolean = false +) { + val showDialogState = remember { mutableStateOf(showDialog) } + + LaunchedEffect(showDialog) { + showDialogState.value = showDialog + } + + if (showDialogState.value) { + SuperDialog( + show = showDialogState, + title = stringResource(titleRes), + onDismissRequest = onDismiss, + content = { + Column( + modifier = Modifier + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = stringResource(messageRes), + style = MiuixTheme.textStyles.body2 + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + TextButton( + text = stringResource(R.string.cancel), + onClick = onDismiss, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp) + ) + Button( + onClick = onConfirm, + enabled = !isLoading, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + cornerRadius = 8.dp + ) { + Text( + text = stringResource(R.string.confirm), + style = MiuixTheme.textStyles.body2, + maxLines = 2 + ) + } + } + } + } + ) + } +} + +// 应用信息缓存 +object AppInfoCache { + private val appInfoMap = mutableMapOf() + + data class CachedAppInfo( + val appName: String, + val packageInfo: PackageInfo?, + val drawable: Drawable?, + val timestamp: Long = System.currentTimeMillis() + ) + + fun getAppInfo(packageName: String): CachedAppInfo? { + return appInfoMap[packageName] + } + + fun putAppInfo(packageName: String, appInfo: CachedAppInfo) { + appInfoMap[packageName] = appInfo + } + + fun clearCache() { + appInfoMap.clear() + } + + fun getAppInfoFromSuperUser(packageName: String): CachedAppInfo? { + val superUserApp = SuperUserViewModel.getAppsSafely().find { it.packageName == packageName } + return superUserApp?.let { app -> + CachedAppInfo( + appName = app.label, + packageInfo = app.packageInfo, + drawable = null + ) + } + } +} + +/** + * 空状态显示组件 + */ +@Composable +fun EmptyStateCard( + message: String, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.defaultColors( + color = colorScheme.surfaceVariant.copy(alpha = 0.15f) + ), + cornerRadius = 8.dp + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = message, + style = MiuixTheme.textStyles.body1, + color = colorScheme.onSurfaceVariantSummary, + textAlign = TextAlign.Center + ) + } + } +} + +/** + * 路径项目卡片组件 + */ +@Composable +fun PathItemCard( + path: String, + icon: ImageVector, + onDelete: () -> Unit, + onEdit: (() -> Unit)? = null, + isLoading: Boolean = false, + additionalInfo: String? = null +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 2.dp), + cornerRadius = 8.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(14.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f) + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = path, + style = MiuixTheme.textStyles.body1, + fontWeight = FontWeight.Medium, + color = colorScheme.onSurface + ) + if (additionalInfo != null) { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = additionalInfo, + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary + ) + } + } + } + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + if (onEdit != null) { + IconButton( + onClick = onEdit, + enabled = !isLoading, + modifier = Modifier.size(36.dp) + ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = stringResource(R.string.edit), + tint = colorScheme.primary, + modifier = Modifier.size(18.dp) + ) + } + } + IconButton( + onClick = onDelete, + enabled = !isLoading, + modifier = Modifier.size(36.dp) + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource(R.string.delete), + tint = colorScheme.error, + modifier = Modifier.size(18.dp) + ) + } + } + } + } +} + +/** + * Kstat配置项目卡片组件 + */ +@Composable +fun KstatConfigItemCard( + config: String, + onDelete: () -> Unit, + onEdit: (() -> Unit)? = null, + isLoading: Boolean = false +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 2.dp), + cornerRadius = 8.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(14.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f) + ) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = null, + tint = colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Column( + modifier = Modifier.weight(1f) + ) { + val parts = config.split("|") + if (parts.isNotEmpty()) { + Text( + text = parts[0], // 路径 + style = MiuixTheme.textStyles.body1, + fontWeight = FontWeight.Medium, + color = colorScheme.onSurface + ) + if (parts.size > 1) { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = parts.drop(1).joinToString(" "), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary + ) + } + } else { + Text( + text = config, + style = MiuixTheme.textStyles.body1, + fontWeight = FontWeight.Medium, + color = colorScheme.onSurface + ) + } + } + } + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + if (onEdit != null) { + IconButton( + onClick = onEdit, + enabled = !isLoading, + modifier = Modifier.size(36.dp) + ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = stringResource(R.string.edit), + tint = colorScheme.primary, + modifier = Modifier.size(18.dp) + ) + } + } + IconButton( + onClick = onDelete, + enabled = !isLoading, + modifier = Modifier.size(36.dp) + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource(R.string.delete), + tint = colorScheme.error, + modifier = Modifier.size(18.dp) + ) + } + } + } + } +} + +/** + * Add Kstat路径项目卡片组件 + */ +@Composable +fun AddKstatPathItemCard( + path: String, + onDelete: () -> Unit, + onEdit: (() -> Unit)? = null, + onUpdate: () -> Unit, + onUpdateFullClone: () -> Unit, + isLoading: Boolean = false +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 2.dp), + cornerRadius = 8.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(14.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f) + ) { + Icon( + imageVector = Icons.Default.Folder, + contentDescription = null, + tint = colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = path, + style = MiuixTheme.textStyles.body1, + fontWeight = FontWeight.Medium, + color = colorScheme.onSurface, + modifier = Modifier.weight(1f) + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (onEdit != null) { + IconButton( + onClick = onEdit, + enabled = !isLoading, + modifier = Modifier.size(36.dp) + ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = stringResource(R.string.edit), + tint = colorScheme.primary, + modifier = Modifier.size(18.dp) + ) + } + } + IconButton( + onClick = onUpdate, + enabled = !isLoading, + modifier = Modifier.size(36.dp) + ) { + Icon( + imageVector = Icons.Default.Update, + contentDescription = stringResource(R.string.update), + tint = colorScheme.primary, + modifier = Modifier.size(18.dp) + ) + } + IconButton( + onClick = onUpdateFullClone, + enabled = !isLoading, + modifier = Modifier.size(36.dp) + ) { + Icon( + imageVector = Icons.Default.PlayArrow, + contentDescription = stringResource(R.string.susfs_update_full_clone), + tint = colorScheme.secondary, + modifier = Modifier.size(18.dp) + ) + } + IconButton( + onClick = onDelete, + enabled = !isLoading, + modifier = Modifier.size(36.dp) + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource(R.string.delete), + tint = colorScheme.error, + modifier = Modifier.size(18.dp) + ) + } + } + } + } +} + +/** + * 启用功能状态卡片组件 + */ +@Composable +fun FeatureStatusCard( + feature: SuSFSManager.EnabledFeature, + onRefresh: (() -> Unit)? = null, + @SuppressLint("ModifierParameter") modifier: Modifier = Modifier +) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + // 日志配置对话框状态 + var showLogConfigDialog by remember { mutableStateOf(false) } + var logEnabled by remember { mutableStateOf(SuSFSManager.getEnableLogState(context)) } + + // 日志配置对话框 + val showLogConfigDialogState = remember { mutableStateOf(showLogConfigDialog) } + + LaunchedEffect(showLogConfigDialog) { + showLogConfigDialogState.value = showLogConfigDialog + } + + if (showLogConfigDialogState.value) { + SuperDialog( + show = showLogConfigDialogState, + title = stringResource(R.string.susfs_log_config_title), + onDismissRequest = { + // 恢复原始状态 + logEnabled = SuSFSManager.getEnableLogState(context) + showLogConfigDialog = false + }, + content = { + Column( + modifier = Modifier + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = stringResource(R.string.susfs_log_config_description), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.susfs_enable_log_label), + style = MiuixTheme.textStyles.body1, + fontWeight = FontWeight.Medium + ) + Switch( + checked = logEnabled, + onCheckedChange = { logEnabled = it } + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + TextButton( + text = stringResource(R.string.cancel), + onClick = { + // 恢复原始状态 + logEnabled = SuSFSManager.getEnableLogState(context) + showLogConfigDialog = false + }, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp) + ) + Button( + onClick = { + coroutineScope.launch { + if (SuSFSManager.setEnableLog(context, logEnabled)) { + onRefresh?.invoke() + } + showLogConfigDialog = false + } + }, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + cornerRadius = 8.dp + ) { + Text( + text = stringResource(R.string.susfs_apply), + style = MiuixTheme.textStyles.body2, + maxLines = 2 + ) + } + } + } + } + ) + } + + Card( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 1.dp) + .then( + if (feature.canConfigure) { + Modifier.clickable { + // 更新当前状态 + logEnabled = SuSFSManager.getEnableLogState(context) + showLogConfigDialog = true + } + } else { + Modifier + } + ), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = feature.name, + style = MiuixTheme.textStyles.body1, + fontWeight = FontWeight.Medium + ) + if (feature.canConfigure) { + Text( + text = stringResource(R.string.susfs_feature_configurable), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary + ) + } + } + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 状态标签 + Box( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background( + when { + feature.isEnabled -> colorScheme.primaryContainer + else -> colorScheme.onSurfaceVariantSummary + } + ) + .padding(horizontal = 10.dp, vertical = 4.dp) + ) { + Text( + text = feature.statusText, + style = MiuixTheme.textStyles.body2, + fontWeight = FontWeight.Medium, + color = when { + feature.isEnabled -> colorScheme.onPrimaryContainer + else -> colorScheme.onSurface + } + ) + } + } + } + } +} + +/** + * SUS挂载隐藏控制卡片组件 + */ +@Composable +fun SusMountHidingControlCard( + hideSusMountsForAllProcs: Boolean, + isLoading: Boolean, + onToggleHiding: (Boolean) -> Unit +) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.defaultColors( + color = colorScheme.surface + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // 标题行 + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = if (hideSusMountsForAllProcs) Icons.Default.VisibilityOff else Icons.Default.Visibility, + contentDescription = null, + tint = colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.susfs_hide_mounts_control_title), + style = MiuixTheme.textStyles.title2, + fontWeight = FontWeight.Bold, + color = colorScheme.onSurface + ) + } + + // 描述文本 + Text( + text = stringResource(R.string.susfs_hide_mounts_control_description), + style = MiuixTheme.textStyles.body1, + color = colorScheme.onSurfaceVariantSummary, + lineHeight = 16.sp + ) + + // 控制开关行 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = stringResource(R.string.susfs_hide_mounts_for_all_procs_label), + style = MiuixTheme.textStyles.body1, + fontWeight = FontWeight.Medium, + color = colorScheme.onSurface + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = if (hideSusMountsForAllProcs) { + stringResource(R.string.susfs_hide_mounts_for_all_procs_enabled_description) + } else { + stringResource(R.string.susfs_hide_mounts_for_all_procs_disabled_description) + }, + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary, + lineHeight = 14.sp + ) + } + Switch( + checked = hideSusMountsForAllProcs, + onCheckedChange = onToggleHiding, + enabled = !isLoading + ) + } + + // 当前设置显示 + Text( + text = stringResource( + R.string.susfs_hide_mounts_current_setting, + if (hideSusMountsForAllProcs) { + stringResource(R.string.susfs_hide_mounts_setting_all) + } else { + stringResource(R.string.susfs_hide_mounts_setting_non_ksu) + } + ), + style = MiuixTheme.textStyles.body1, + color = colorScheme.primary, + fontWeight = FontWeight.Medium + ) + + // 建议文本 + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.defaultColors( + color = colorScheme.surfaceVariant.copy(alpha = 0.2f) + ), + cornerRadius = 8.dp + ) { + Text( + text = stringResource(R.string.susfs_hide_mounts_recommendation), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary, + lineHeight = 18.sp, + modifier = Modifier.padding(12.dp) + ) + } + } + } +} + +/** + * 应用路径分组卡片 + */ +@Composable +fun AppPathGroupCard( + packageName: String, + paths: List, + onDeleteGroup: () -> Unit, + onEditGroup: (() -> Unit)? = null, + isLoading: Boolean +) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + var superUserApps by remember { mutableStateOf(SuperUserViewModel.getAppsSafely()) } + + LaunchedEffect(Unit) { + snapshotFlow { SuperUserViewModel.apps } + .distinctUntilChanged() + .collect { _ -> + superUserApps = SuperUserViewModel.getAppsSafely() + } + } + + var cachedAppInfo by remember(packageName, superUserApps.size) { + mutableStateOf(AppInfoCache.getAppInfo(packageName)) + } + var isLoadingAppInfo by remember(packageName, superUserApps.size) { mutableStateOf(false) } + + LaunchedEffect(packageName, superUserApps.size) { + if (cachedAppInfo == null || superUserApps.isNotEmpty()) { + isLoadingAppInfo = true + coroutineScope.launch { + try { + val superUserAppInfo = AppInfoCache.getAppInfoFromSuperUser(packageName) + + if (superUserAppInfo != null) { + val packageManager = context.packageManager + val drawable = try { + superUserAppInfo.packageInfo?.applicationInfo?.let { + packageManager.getApplicationIcon(it) + } + } catch (_: Exception) { + null + } + + val newCachedInfo = AppInfoCache.CachedAppInfo( + appName = superUserAppInfo.appName, + packageInfo = superUserAppInfo.packageInfo, + drawable = drawable + ) + + AppInfoCache.putAppInfo(packageName, newCachedInfo) + cachedAppInfo = newCachedInfo + } else { + val packageManager = context.packageManager + val appInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA) + + val appName = try { + appInfo.applicationInfo?.let { + packageManager.getApplicationLabel(it).toString() + } ?: packageName + } catch (_: Exception) { + packageName + } + + val drawable = try { + appInfo.applicationInfo?.let { + packageManager.getApplicationIcon(it) + } + } catch (_: Exception) { + null + } + + val newCachedInfo = AppInfoCache.CachedAppInfo( + appName = appName, + packageInfo = appInfo, + drawable = drawable + ) + + AppInfoCache.putAppInfo(packageName, newCachedInfo) + cachedAppInfo = newCachedInfo + } + } catch (_: Exception) { + val newCachedInfo = AppInfoCache.CachedAppInfo( + appName = packageName, + packageInfo = null, + drawable = null + ) + AppInfoCache.putAppInfo(packageName, newCachedInfo) + cachedAppInfo = newCachedInfo + } finally { + isLoadingAppInfo = false + } + } + } + } + + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.defaultColors( + color = colorScheme.surface + ) + ) { + Column( + modifier = Modifier.padding(12.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + // 应用图标 + AppIcon( + packageName = packageName, + packageInfo = cachedAppInfo?.packageInfo, + modifier = Modifier.size(32.dp) + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Column(modifier = Modifier.weight(1f)) { + val displayName = cachedAppInfo?.appName?.ifEmpty { packageName } ?: packageName + Text( + text = displayName, + style = MiuixTheme.textStyles.title2, + fontWeight = FontWeight.Medium, + color = colorScheme.onSurface + ) + if (!isLoadingAppInfo && cachedAppInfo?.appName?.isNotEmpty() == true && + cachedAppInfo?.appName != packageName) { + Text( + text = packageName, + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary + ) + } + } + + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (onEditGroup != null) { + IconButton( + onClick = onEditGroup, + enabled = !isLoading + ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = stringResource(R.string.edit), + tint = colorScheme.primary + ) + } + } + IconButton( + onClick = onDeleteGroup, + enabled = !isLoading + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource(R.string.delete), + tint = colorScheme.error + ) + } + } + } + + // 显示所有路径 + Spacer(modifier = Modifier.height(8.dp)) + + paths.forEach { path -> + Text( + text = path, + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary, + modifier = Modifier + .fillMaxWidth() + .background( + colorScheme.surfaceVariant.copy(alpha = 0.3f), + RoundedCornerShape(6.dp) + ) + .padding(8.dp) + ) + + if (path != paths.last()) { + Spacer(modifier = Modifier.height(4.dp)) + } + } + } + } +} + +/** + * 分组标题组件 + */ +@Composable +fun SectionHeader( + title: String, + subtitle: String?, + icon: ImageVector, + count: Int +) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.defaultColors( + color = colorScheme.surfaceVariant.copy(alpha = 0.25f) + ), + cornerRadius = 8.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = colorScheme.primary, + modifier = Modifier.size(22.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MiuixTheme.textStyles.title3, + fontWeight = FontWeight.Medium, + color = colorScheme.onSurface + ) + subtitle?.let { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = it, + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary + ) + } + } + Box( + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background(colorScheme.primaryContainer) + .padding(horizontal = 10.dp, vertical = 5.dp) + ) { + Text( + text = count.toString(), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onPrimaryContainer, + fontWeight = FontWeight.Medium + ) + } + } + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigTabs.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigTabs.kt new file mode 100644 index 00000000..e6e2b17b --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigTabs.kt @@ -0,0 +1,1183 @@ +package com.sukisu.ultra.ui.susfs.component + +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.susfs.util.SuSFSManager +import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel +import top.yukonga.miuix.kmp.basic.Button +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TextField +import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.basic.CardDefaults +import top.yukonga.miuix.kmp.basic.Switch +/** + * SUS路径内容组件 + */ +@Composable +fun SusPathsContent( + susPaths: Set, + isLoading: Boolean, + onAddPath: () -> Unit, + onAddAppPath: () -> Unit, + onRemovePath: (String) -> Unit, + onEditPath: ((String) -> Unit)? = null, + forceRefreshApps: Boolean = false, + onReset: (() -> Unit)? = null +) { + var superUserApps by remember { mutableStateOf(SuperUserViewModel.getAppsSafely()) } + + LaunchedEffect(Unit) { + snapshotFlow { SuperUserViewModel.apps } + .distinctUntilChanged() + .collect { _ -> + superUserApps = SuperUserViewModel.getAppsSafely() + if (superUserApps.isNotEmpty()) { + try { + AppInfoCache.clearCache() + } catch (_: Exception) { + } + } + } + } + + LaunchedEffect(forceRefreshApps) { + if (forceRefreshApps) { + try { + AppInfoCache.clearCache() + } catch (_: Exception) { + // Ignore cache clear errors + } + } + } + + val (appPathGroups, otherPaths) = remember(susPaths, superUserApps) { + val appPathRegex = Regex(".*/Android/data/([^/]+)/?.*") + val uidPathRegex = Regex("/sys/fs/cgroup/uid_([0-9]+)") + val appPathMap = mutableMapOf>() + val uidToPackageMap = mutableMapOf() + val others = mutableListOf() + + // 构建UID到包名的映射 + try { + superUserApps.forEach { app: SuperUserViewModel.AppInfo -> + try { + val uid = app.packageInfo.applicationInfo?.uid + if (uid != null) { + uidToPackageMap[uid.toString()] = app.packageName + } + } catch (_: Exception) { + // Ignore individual app errors + } + } + } catch (_: Exception) { + // Ignore mapping errors + } + + susPaths.forEach { path -> + val appDataMatch = appPathRegex.find(path) + val uidMatch = uidPathRegex.find(path) + + when { + appDataMatch != null -> { + val packageName = appDataMatch.groupValues[1] + appPathMap.getOrPut(packageName) { mutableListOf() }.add(path) + } + uidMatch != null -> { + val uid = uidMatch.groupValues[1] + val packageName = uidToPackageMap[uid] + if (packageName != null) { + appPathMap.getOrPut(packageName) { mutableListOf() }.add(path) + } else { + others.add(path) + } + } + else -> { + others.add(path) + } + } + } + + val sortedAppGroups = appPathMap.toList() + .sortedBy { it.first } + .map { (packageName, paths) -> packageName to paths.sorted() } + + Pair(sortedAppGroups, others.sorted()) + } + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // 应用路径分组 + if (appPathGroups.isNotEmpty()) { + SectionHeader( + title = stringResource(R.string.app_paths_section), + subtitle = null, + icon = Icons.Default.Apps, + count = appPathGroups.size + ) + + appPathGroups.forEach { (packageName, paths) -> + AppPathGroupCard( + packageName = packageName, + paths = paths, + onDeleteGroup = { + paths.forEach { path -> onRemovePath(path) } + }, + onEditGroup = if (onEditPath != null) { + { + onEditPath(paths.first()) + } + } else null, + isLoading = isLoading + ) + } + } + + // 其他路径 + if (otherPaths.isNotEmpty()) { + SectionHeader( + title = stringResource(R.string.other_paths_section), + subtitle = null, + icon = Icons.Default.Folder, + count = otherPaths.size + ) + + otherPaths.forEach { path -> + PathItemCard( + path = path, + icon = Icons.Default.Folder, + onDelete = { onRemovePath(path) }, + onEdit = if (onEditPath != null) { { onEditPath(path) } } else null, + isLoading = isLoading + ) + } + } + + if (susPaths.isEmpty()) { + EmptyStateCard( + message = stringResource(R.string.susfs_no_paths_configured) + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = onAddPath, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + cornerRadius = 8.dp + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = stringResource(R.string.add_custom_path), + style = MiuixTheme.textStyles.body1, + maxLines = 2 + ) + } + + Button( + onClick = onAddAppPath, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + cornerRadius = 8.dp + ) { + Icon( + imageVector = Icons.Default.Apps, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = stringResource(R.string.add_app_path), + style = MiuixTheme.textStyles.body1, + maxLines = 2 + ) + } + } + + // 重置按钮 + if (onReset != null && susPaths.isNotEmpty()) { + Spacer(modifier = Modifier.height(16.dp)) + Card( + onClick = onReset, + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.defaultColors( + color = colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + cornerRadius = 8.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.RestoreFromTrash, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = colorScheme.primary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.susfs_reset_paths_title), + style = MiuixTheme.textStyles.body1, + fontWeight = FontWeight.Medium, + color = colorScheme.primary + ) + } + } + } + } +} + +/** + * SUS循环路径内容组件 + */ +@Composable +fun SusLoopPathsContent( + susLoopPaths: Set, + isLoading: Boolean, + onAddLoopPath: () -> Unit, + onRemoveLoopPath: (String) -> Unit, + onEditLoopPath: ((String) -> Unit)? = null, + onReset: (() -> Unit)? = null +) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // 说明卡片 + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.defaultColors( + color = colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + cornerRadius = 8.dp + ) { + Column( + modifier = Modifier.padding(14.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = stringResource(R.string.sus_loop_paths_description_title), + style = MiuixTheme.textStyles.title3, + fontWeight = FontWeight.Medium, + color = colorScheme.primary + ) + Text( + text = stringResource(R.string.sus_loop_paths_description_text), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary, + lineHeight = 18.sp + ) + Text( + text = stringResource(R.string.susfs_loop_path_restriction_warning), + style = MiuixTheme.textStyles.body2, + color = colorScheme.secondary, + fontWeight = FontWeight.Medium + ) + } + } + + if (susLoopPaths.isEmpty()) { + EmptyStateCard( + message = stringResource(R.string.susfs_no_loop_paths_configured) + ) + } else { + SectionHeader( + title = stringResource(R.string.loop_paths_section), + subtitle = null, + icon = Icons.Default.Loop, + count = susLoopPaths.size + ) + + susLoopPaths.toList().forEach { path -> + PathItemCard( + path = path, + icon = Icons.Default.Loop, + onDelete = { onRemoveLoopPath(path) }, + onEdit = if (onEditLoopPath != null) { { onEditLoopPath(path) } } else null, + isLoading = isLoading + ) + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = onAddLoopPath, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + cornerRadius = 8.dp + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = stringResource(R.string.add_loop_path), + style = MiuixTheme.textStyles.body1, + maxLines = 2 + ) + } + } + + // 重置按钮 + if (onReset != null && susLoopPaths.isNotEmpty()) { + Spacer(modifier = Modifier.height(16.dp)) + Card( + onClick = onReset, + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.defaultColors( + color = colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + cornerRadius = 8.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.RestoreFromTrash, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = colorScheme.primary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.susfs_reset_loop_paths_title), + style = MiuixTheme.textStyles.body1, + fontWeight = FontWeight.Medium, + color = colorScheme.primary + ) + } + } + } + } +} + +/** + * SUS Maps内容组件 + */ +@Composable +fun SusMapsContent( + susMaps: Set, + isLoading: Boolean, + onAddSusMap: () -> Unit, + onRemoveSusMap: (String) -> Unit, + onEditSusMap: ((String) -> Unit)? = null, + onReset: (() -> Unit)? = null +) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // 说明卡片 + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.defaultColors( + color = colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + cornerRadius = 8.dp + ) { + Column( + modifier = Modifier.padding(14.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = stringResource(R.string.sus_maps_description_title), + style = MiuixTheme.textStyles.title3, + fontWeight = FontWeight.Medium, + color = colorScheme.primary + ) + Text( + text = stringResource(R.string.sus_maps_description_text), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary, + lineHeight = 18.sp + ) + Text( + text = stringResource(R.string.sus_maps_warning), + style = MiuixTheme.textStyles.body2, + color = colorScheme.secondary, + fontWeight = FontWeight.Medium + ) + Text( + text = stringResource(R.string.sus_maps_debug_info), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary.copy(alpha = 0.8f), + lineHeight = 16.sp + ) + } + } + + if (susMaps.isEmpty()) { + EmptyStateCard( + message = stringResource(R.string.susfs_no_sus_maps_configured) + ) + } else { + SectionHeader( + title = stringResource(R.string.sus_maps_section), + subtitle = null, + icon = Icons.Default.Security, + count = susMaps.size + ) + + susMaps.toList().forEach { map -> + PathItemCard( + path = map, + icon = Icons.Default.Security, + onDelete = { onRemoveSusMap(map) }, + onEdit = if (onEditSusMap != null) { { onEditSusMap(map) } } else null, + isLoading = isLoading + ) + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = onAddSusMap, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + cornerRadius = 8.dp + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = stringResource(R.string.add), + style = MiuixTheme.textStyles.body1, + maxLines = 2 + ) + } + } + + // 重置按钮 + if (onReset != null && susMaps.isNotEmpty()) { + Spacer(modifier = Modifier.height(16.dp)) + Card( + onClick = onReset, + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.defaultColors( + color = colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + cornerRadius = 8.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.RestoreFromTrash, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = colorScheme.primary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.susfs_reset_sus_maps_title), + style = MiuixTheme.textStyles.body1, + fontWeight = FontWeight.Medium, + color = colorScheme.primary + ) + } + } + } + } +} + +/** + * SUS挂载内容组件 + */ +@Composable +fun SusMountsContent( + susMounts: Set, + hideSusMountsForAllProcs: Boolean, + isLoading: Boolean, + onAddMount: () -> Unit, + onRemoveMount: (String) -> Unit, + onEditMount: ((String) -> Unit)? = null, + onToggleHideSusMountsForAllProcs: (Boolean) -> Unit, + onReset: (() -> Unit)? = null +) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + SusMountHidingControlCard( + hideSusMountsForAllProcs = hideSusMountsForAllProcs, + isLoading = isLoading, + onToggleHiding = onToggleHideSusMountsForAllProcs + ) + + if (susMounts.isEmpty()) { + EmptyStateCard( + message = stringResource(R.string.susfs_no_mounts_configured) + ) + } else { + susMounts.toList().forEach { mount -> + PathItemCard( + path = mount, + icon = Icons.Default.Storage, + onDelete = { onRemoveMount(mount) }, + onEdit = if (onEditMount != null) { { onEditMount(mount) } } else null, + isLoading = isLoading + ) + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = onAddMount, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + cornerRadius = 8.dp + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = stringResource(R.string.add), + style = MiuixTheme.textStyles.body1, + maxLines = 2 + ) + } + } + + // 重置按钮 + if (onReset != null && susMounts.isNotEmpty()) { + Spacer(modifier = Modifier.height(16.dp)) + Card( + onClick = onReset, + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.defaultColors( + color = colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + cornerRadius = 8.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.RestoreFromTrash, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = colorScheme.primary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.susfs_reset_mounts_title), + style = MiuixTheme.textStyles.body1, + fontWeight = FontWeight.Medium, + color = colorScheme.primary + ) + } + } + } + } +} + +/** + * 尝试卸载内容组件 + */ +@Composable +fun TryUmountContent( + tryUmounts: Set, + umountForZygoteIsoService: Boolean, + isLoading: Boolean, + onAddUmount: () -> Unit, + onRemoveUmount: (String) -> Unit, + onEditUmount: ((String) -> Unit)? = null, + onToggleUmountForZygoteIsoService: (Boolean) -> Unit, + onReset: (() -> Unit)? = null +) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.defaultColors( + color = colorScheme.surface + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Security, + contentDescription = null, + tint = colorScheme.primary, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.umount_zygote_iso_service), + style = MiuixTheme.textStyles.title2, + fontWeight = FontWeight.Medium, + color = colorScheme.onSurface + ) + } + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = stringResource(R.string.umount_zygote_iso_service_description), + style = MiuixTheme.textStyles.body1, + color = colorScheme.onSurfaceVariantSummary, + lineHeight = 14.sp + ) + } + Switch( + checked = umountForZygoteIsoService, + onCheckedChange = onToggleUmountForZygoteIsoService, + enabled = !isLoading + ) + } + } + + if (tryUmounts.isEmpty()) { + EmptyStateCard( + message = stringResource(R.string.susfs_no_umounts_configured) + ) + } else { + tryUmounts.toList().forEach { umountEntry -> + val parts = umountEntry.split("|") + val path = if (parts.isNotEmpty()) parts[0] else umountEntry + val mode = if (parts.size > 1) parts[1] else "0" + val modeText = if (mode == "0") + stringResource(R.string.susfs_umount_mode_normal_short) + else + stringResource(R.string.susfs_umount_mode_detach_short) + + PathItemCard( + path = path, + icon = Icons.Default.Storage, + additionalInfo = stringResource(R.string.susfs_umount_mode_display, modeText, mode), + onDelete = { onRemoveUmount(umountEntry) }, + onEdit = if (onEditUmount != null) { { onEditUmount(umountEntry) } } else null, + isLoading = isLoading + ) + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = onAddUmount, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + cornerRadius = 8.dp + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = stringResource(R.string.add), + style = MiuixTheme.textStyles.body1, + maxLines = 2 + ) + } + } + + // 重置按钮 + if (onReset != null && tryUmounts.isNotEmpty()) { + Spacer(modifier = Modifier.height(16.dp)) + Card( + onClick = onReset, + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.defaultColors( + color = colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + cornerRadius = 8.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.RestoreFromTrash, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = colorScheme.primary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.susfs_reset_umounts_title), + style = MiuixTheme.textStyles.body1, + fontWeight = FontWeight.Medium, + color = colorScheme.primary + ) + } + } + } + } +} + +/** + * Kstat配置内容组件 + */ +@Composable +fun KstatConfigContent( + kstatConfigs: Set, + addKstatPaths: Set, + isLoading: Boolean, + onAddKstatStatically: () -> Unit, + onAddKstat: () -> Unit, + onRemoveKstatConfig: (String) -> Unit, + onEditKstatConfig: ((String) -> Unit)? = null, + onRemoveAddKstat: (String) -> Unit, + onEditAddKstat: ((String) -> Unit)? = null, + onUpdateKstat: (String) -> Unit, + onUpdateKstatFullClone: (String) -> Unit +) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.defaultColors( + color = colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + cornerRadius = 8.dp + ) { + Column( + modifier = Modifier.padding(14.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = stringResource(R.string.kstat_config_description_title), + style = MiuixTheme.textStyles.title3, + fontWeight = FontWeight.Medium, + color = colorScheme.primary + ) + Text( + text = stringResource(R.string.kstat_config_description_add_statically), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary, + lineHeight = 18.sp + ) + Text( + text = stringResource(R.string.kstat_config_description_add), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary, + lineHeight = 18.sp + ) + Text( + text = stringResource(R.string.kstat_config_description_update), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary, + lineHeight = 18.sp + ) + Text( + text = stringResource(R.string.kstat_config_description_update_full_clone), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary, + lineHeight = 18.sp + ) + } + } + + if (kstatConfigs.isNotEmpty()) { + SectionHeader( + title = stringResource(R.string.static_kstat_config), + subtitle = null, + icon = Icons.Default.Settings, + count = kstatConfigs.size + ) + kstatConfigs.toList().forEach { config -> + KstatConfigItemCard( + config = config, + onDelete = { onRemoveKstatConfig(config) }, + onEdit = if (onEditKstatConfig != null) { { onEditKstatConfig(config) } } else null, + isLoading = isLoading + ) + } + } + + if (addKstatPaths.isNotEmpty()) { + SectionHeader( + title = stringResource(R.string.kstat_path_management), + subtitle = null, + icon = Icons.Default.Folder, + count = addKstatPaths.size + ) + addKstatPaths.toList().forEach { path -> + AddKstatPathItemCard( + path = path, + onDelete = { onRemoveAddKstat(path) }, + onEdit = if (onEditAddKstat != null) { { onEditAddKstat(path) } } else null, + onUpdate = { onUpdateKstat(path) }, + onUpdateFullClone = { onUpdateKstatFullClone(path) }, + isLoading = isLoading + ) + } + } + + if (kstatConfigs.isEmpty() && addKstatPaths.isEmpty()) { + EmptyStateCard( + message = stringResource(R.string.no_kstat_config_message) + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = onAddKstat, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + cornerRadius = 8.dp + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = stringResource(R.string.add), + style = MiuixTheme.textStyles.body1, + maxLines = 2 + ) + } + + Button( + onClick = onAddKstatStatically, + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + cornerRadius = 8.dp + ) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = stringResource(R.string.add), + style = MiuixTheme.textStyles.body1, + maxLines = 2 + ) + } + } + } +} + +/** + * 路径设置内容组件 + */ +@SuppressLint("SdCardPath") +@Composable +fun PathSettingsContent( + androidDataPath: String, + onAndroidDataPathChange: (String) -> Unit, + sdcardPath: String, + onSdcardPathChange: (String) -> Unit, + isLoading: Boolean, + onSetAndroidDataPath: () -> Unit, + onSetSdcardPath: () -> Unit, + onReset: (() -> Unit)? = null +) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Card( + modifier = Modifier.fillMaxWidth(), + ) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + TextField( + value = androidDataPath, + onValueChange = onAndroidDataPathChange, + label = stringResource(R.string.susfs_android_data_path_label), + useLabelAsPlaceholder = true, + modifier = Modifier.fillMaxWidth(), + enabled = !isLoading + ) + + Button( + onClick = onSetAndroidDataPath, + enabled = !isLoading && androidDataPath.isNotBlank(), + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + ) { + Text( + stringResource(R.string.susfs_set_android_data_path), + maxLines = 2 + ) + } + } + } + + Card( + modifier = Modifier.fillMaxWidth(), + ) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + TextField( + value = sdcardPath, + onValueChange = onSdcardPathChange, + label = stringResource(R.string.susfs_sdcard_path_label), + useLabelAsPlaceholder = true, + modifier = Modifier.fillMaxWidth(), + enabled = !isLoading + ) + + Button( + onClick = onSetSdcardPath, + enabled = !isLoading && sdcardPath.isNotBlank(), + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 48.dp) + .padding(vertical = 8.dp), + ) { + Text( + stringResource(R.string.susfs_set_sdcard_path), + maxLines = 2 + ) + } + } + } + + // 重置按钮 + if (onReset != null) { + Spacer(modifier = Modifier.height(16.dp)) + Card( + onClick = onReset, + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.defaultColors( + color = colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + cornerRadius = 8.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.RestoreFromTrash, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = colorScheme.primary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.susfs_reset_path_title), + style = MiuixTheme.textStyles.body1, + fontWeight = FontWeight.Medium, + color = colorScheme.primary + ) + } + } + } + } +} + +/** + * 启用功能状态内容组件 + */ +@Composable +fun EnabledFeaturesContent( + enabledFeatures: List, + onRefresh: () -> Unit +) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.defaultColors( + color = colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + cornerRadius = 8.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = null, + tint = colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(10.dp)) + Text( + text = stringResource(R.string.susfs_enabled_features_description), + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceVariantSummary, + lineHeight = 18.sp + ) + } + } + + if (enabledFeatures.isEmpty()) { + EmptyStateCard( + message = stringResource(R.string.susfs_no_features_found) + ) + } else { + enabledFeatures.forEach { feature -> + FeatureStatusCard( + feature = feature, + onRefresh = onRefresh + ) + } + } + + // 刷新按钮 + Spacer(modifier = Modifier.height(16.dp)) + Card( + onClick = onRefresh, + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.defaultColors( + color = colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + cornerRadius = 8.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = colorScheme.primary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.refresh), + style = MiuixTheme.textStyles.body1, + fontWeight = FontWeight.Medium, + color = colorScheme.primary + ) + } + } + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt new file mode 100644 index 00000000..7a0c085e --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt @@ -0,0 +1,1462 @@ +package com.sukisu.ultra.ui.susfs.util + +import android.annotation.SuppressLint +import android.content.Context +import android.content.SharedPreferences +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.os.Build +import android.util.Log +import android.widget.Toast +import com.sukisu.ultra.R +import com.topjohnwu.superuser.Shell +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import androidx.core.content.edit +import com.sukisu.ultra.ui.util.getRootShell +import com.sukisu.ultra.ui.util.getSuSFSVersion +import com.sukisu.ultra.ui.util.getSuSFSFeatures +import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import org.json.JSONArray +import org.json.JSONObject +import java.text.SimpleDateFormat +import java.util.* + +/** + * SuSFS 配置管理器 + * 用于管理SuSFS相关的配置和命令执行 + */ +object SuSFSManager { + private const val PREFS_NAME = "susfs_config" + private const val KEY_UNAME_VALUE = "uname_value" + private const val KEY_BUILD_TIME_VALUE = "build_time_value" + private const val KEY_AUTO_START_ENABLED = "auto_start_enabled" + private const val KEY_SUS_PATHS = "sus_paths" + private const val KEY_SUS_LOOP_PATHS = "sus_loop_paths" + + private const val KEY_SUS_MAPS = "sus_maps" + private const val KEY_SUS_MOUNTS = "sus_mounts" + private const val KEY_TRY_UMOUNTS = "try_umounts" + private const val KEY_ANDROID_DATA_PATH = "android_data_path" + private const val KEY_SDCARD_PATH = "sdcard_path" + private const val KEY_ENABLE_LOG = "enable_log" + private const val KEY_EXECUTE_IN_POST_FS_DATA = "execute_in_post_fs_data" + private const val KEY_KSTAT_CONFIGS = "kstat_configs" + private const val KEY_ADD_KSTAT_PATHS = "add_kstat_paths" + private const val KEY_HIDE_SUS_MOUNTS_FOR_ALL_PROCS = "hide_sus_mounts_for_all_procs" + private const val KEY_ENABLE_CLEANUP_RESIDUE = "enable_cleanup_residue" + private const val KEY_ENABLE_HIDE_BL = "enable_hide_bl" + private const val KEY_UMOUNT_FOR_ZYGOTE_ISO_SERVICE = "umount_for_zygote_iso_service" + private const val KEY_ENABLE_AVC_LOG_SPOOFING = "enable_avc_log_spoofing" + + + // 常量 + private const val SUSFS_BINARY_TARGET_NAME = "ksu_susfs" + private const val DEFAULT_UNAME = "default" + private const val DEFAULT_BUILD_TIME = "default" + private const val MODULE_ID = "susfs_manager" + private const val MODULE_PATH = "/data/adb/modules/$MODULE_ID" + const val MAX_SUSFS_VERSION = "2.0.0" + private const val BACKUP_FILE_EXTENSION = ".susfs_backup" + private const val MEDIA_DATA_PATH = "/data/media/0/Android/data" + private const val CGROUP_UID_PATH_PREFIX = "/sys/fs/cgroup/uid_" + + data class SlotInfo(val slotName: String, val uname: String, val buildTime: String) + data class CommandResult(val isSuccess: Boolean, val output: String, val errorOutput: String = "") + data class EnabledFeature( + val name: String, + val isEnabled: Boolean, + val statusText: String, + val canConfigure: Boolean = false + ) { + companion object { + fun create(context: Context, name: String, isEnabled: Boolean): EnabledFeature { + val statusText = if (isEnabled) { + context.getString(R.string.susfs_feature_enabled) + } else { + context.getString(R.string.susfs_feature_disabled) + } + return EnabledFeature(name, isEnabled, statusText, false) + } + } + } + + /** + * 应用信息数据类 + */ + data class AppInfo( + val packageName: String, + val appName: String, + val packageInfo: PackageInfo, + val isSystemApp: Boolean + ) + + /** + * 备份数据类 + */ + data class BackupData( + val version: String, + val timestamp: Long, + val deviceInfo: String, + val configurations: Map + ) { + fun toJson(): String { + val jsonObject = JSONObject().apply { + put("version", version) + put("timestamp", timestamp) + put("deviceInfo", deviceInfo) + put("configurations", JSONObject(configurations)) + } + return jsonObject.toString(2) + } + + companion object { + fun fromJson(jsonString: String): BackupData? { + return try { + val jsonObject = JSONObject(jsonString) + val configurationsJson = jsonObject.getJSONObject("configurations") + val configurations = mutableMapOf() + + configurationsJson.keys().forEach { key -> + val value = configurationsJson.get(key) + configurations[key] = when (value) { + is JSONArray -> { + val set = mutableSetOf() + for (i in 0 until value.length()) { + set.add(value.getString(i)) + } + set + } + else -> value + } + } + + BackupData( + version = jsonObject.getString("version"), + timestamp = jsonObject.getLong("timestamp"), + deviceInfo = jsonObject.getString("deviceInfo"), + configurations = configurations + ) + } catch (e: Exception) { + e.printStackTrace() + null + } + } + } + } + + /** + * 模块配置数据类 + */ + data class ModuleConfig( + val targetPath: String, + val unameValue: String, + val buildTimeValue: String, + val executeInPostFsData: Boolean, + val susPaths: Set, + val susLoopPaths: Set, + val susMaps: Set, + val susMounts: Set, + val tryUmounts: Set, + val androidDataPath: String, + val sdcardPath: String, + val enableLog: Boolean, + val kstatConfigs: Set, + val addKstatPaths: Set, + val hideSusMountsForAllProcs: Boolean, + val enableHideBl: Boolean, + val enableCleanupResidue: Boolean, + val umountForZygoteIsoService: Boolean, + val enableAvcLogSpoofing: Boolean + ) { + /** + * 检查是否有需要自启动的配置 + */ + fun hasAutoStartConfig(): Boolean { + return unameValue != DEFAULT_UNAME || + buildTimeValue != DEFAULT_BUILD_TIME || + susPaths.isNotEmpty() || + susLoopPaths.isNotEmpty() || + susMaps.isNotEmpty() || + susMounts.isNotEmpty() || + tryUmounts.isNotEmpty() || + kstatConfigs.isNotEmpty() || + addKstatPaths.isNotEmpty() + } + } + + // 基础工具方法 + private fun getPrefs(context: Context): SharedPreferences = + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + private fun getSuSFSVersionUse(context: Context): String = try { + val version = getSuSFSVersion() + val binaryName = "${SUSFS_BINARY_TARGET_NAME}_${version.removePrefix("v")}" + if (isBinaryAvailable(context, binaryName)) { + version + } else { + MAX_SUSFS_VERSION + } + } catch (_: Exception) { + MAX_SUSFS_VERSION + } + + fun isBinaryAvailable(context: Context, binaryName: String): Boolean = try { + context.assets.open(binaryName).use { true } + } catch (_: IOException) { false } + + private fun getSuSFSBinaryName(context: Context): String = "${SUSFS_BINARY_TARGET_NAME}_${getSuSFSVersionUse(context).removePrefix("v")}" + + private fun getSuSFSTargetPath(): String = "/data/adb/ksu/bin/$SUSFS_BINARY_TARGET_NAME" + + private fun runCmd(shell: Shell, cmd: String): String { + return shell.newJob() + .add(cmd) + .to(mutableListOf(), null) + .exec().out + .joinToString("\n") + } + + private fun runCmdWithResult(cmd: String): CommandResult { + val result = Shell.getShell().newJob().add(cmd).exec() + return CommandResult(result.isSuccess, result.out.joinToString("\n"), result.err.joinToString("\n")) + } + + + /** + * 获取当前模块配置 + */ + private fun getCurrentModuleConfig(context: Context): ModuleConfig { + return ModuleConfig( + targetPath = getSuSFSTargetPath(), + unameValue = getUnameValue(context), + buildTimeValue = getBuildTimeValue(context), + executeInPostFsData = getExecuteInPostFsData(context), + susPaths = getSusPaths(context), + susLoopPaths = getSusLoopPaths(context), + susMaps = getSusMaps(context), + susMounts = getSusMounts(context), + tryUmounts = getTryUmounts(context), + androidDataPath = getAndroidDataPath(context), + sdcardPath = getSdcardPath(context), + enableLog = getEnableLogState(context), + kstatConfigs = getKstatConfigs(context), + addKstatPaths = getAddKstatPaths(context), + hideSusMountsForAllProcs = getHideSusMountsForAllProcs(context), + enableHideBl = getEnableHideBl(context), + enableCleanupResidue = getEnableCleanupResidue(context), + umountForZygoteIsoService = getUmountForZygoteIsoService(context), + enableAvcLogSpoofing = getEnableAvcLogSpoofing(context) + ) + } + + // 配置存取方法 + fun saveUnameValue(context: Context, value: String) = + getPrefs(context).edit { putString(KEY_UNAME_VALUE, value) } + + fun getUnameValue(context: Context): String = + getPrefs(context).getString(KEY_UNAME_VALUE, DEFAULT_UNAME) ?: DEFAULT_UNAME + + fun saveBuildTimeValue(context: Context, value: String) = + getPrefs(context).edit { putString(KEY_BUILD_TIME_VALUE, value)} + + fun getBuildTimeValue(context: Context): String = + getPrefs(context).getString(KEY_BUILD_TIME_VALUE, DEFAULT_BUILD_TIME) ?: DEFAULT_BUILD_TIME + + fun setAutoStartEnabled(context: Context, enabled: Boolean) = + getPrefs(context).edit { putBoolean(KEY_AUTO_START_ENABLED, enabled) } + + fun isAutoStartEnabled(context: Context): Boolean = + getPrefs(context).getBoolean(KEY_AUTO_START_ENABLED, false) + + fun saveEnableLogState(context: Context, enabled: Boolean) = + getPrefs(context).edit { putBoolean(KEY_ENABLE_LOG, enabled) } + + fun getEnableLogState(context: Context): Boolean = + getPrefs(context).getBoolean(KEY_ENABLE_LOG, false) + + fun getExecuteInPostFsData(context: Context): Boolean = + getPrefs(context).getBoolean(KEY_EXECUTE_IN_POST_FS_DATA, false) + + // SUS挂载隐藏控制 + fun saveHideSusMountsForAllProcs(context: Context, hideForAll: Boolean) = + getPrefs(context).edit { putBoolean(KEY_HIDE_SUS_MOUNTS_FOR_ALL_PROCS, hideForAll) } + + fun getHideSusMountsForAllProcs(context: Context): Boolean = + getPrefs(context).getBoolean(KEY_HIDE_SUS_MOUNTS_FOR_ALL_PROCS, true) + + // 隐藏BL锁脚本 + fun saveEnableHideBl(context: Context, enabled: Boolean) = + getPrefs(context).edit { putBoolean(KEY_ENABLE_HIDE_BL, enabled) } + + fun getEnableHideBl(context: Context): Boolean = + getPrefs(context).getBoolean(KEY_ENABLE_HIDE_BL, true) + + + // 清理残留配置 + fun saveEnableCleanupResidue(context: Context, enabled: Boolean) = + getPrefs(context).edit { putBoolean(KEY_ENABLE_CLEANUP_RESIDUE, enabled) } + + fun getEnableCleanupResidue(context: Context): Boolean = + getPrefs(context).getBoolean(KEY_ENABLE_CLEANUP_RESIDUE, false) + + // Zygote隔离服务卸载控制 + fun saveUmountForZygoteIsoService(context: Context, enabled: Boolean) = + getPrefs(context).edit { putBoolean(KEY_UMOUNT_FOR_ZYGOTE_ISO_SERVICE, enabled) } + + fun getUmountForZygoteIsoService(context: Context): Boolean = + getPrefs(context).getBoolean(KEY_UMOUNT_FOR_ZYGOTE_ISO_SERVICE, false) + + // AVC日志欺骗配置 + fun saveEnableAvcLogSpoofing(context: Context, enabled: Boolean) = + getPrefs(context).edit { putBoolean(KEY_ENABLE_AVC_LOG_SPOOFING, enabled) } + + fun getEnableAvcLogSpoofing(context: Context): Boolean = + getPrefs(context).getBoolean(KEY_ENABLE_AVC_LOG_SPOOFING, false) + + + // 路径和配置管理 + fun saveSusPaths(context: Context, paths: Set) = + getPrefs(context).edit { putStringSet(KEY_SUS_PATHS, paths) } + + fun getSusPaths(context: Context): Set = + getPrefs(context).getStringSet(KEY_SUS_PATHS, emptySet()) ?: emptySet() + + // 循环路径管理 + fun saveSusLoopPaths(context: Context, paths: Set) = + getPrefs(context).edit { putStringSet(KEY_SUS_LOOP_PATHS, paths) } + + fun getSusLoopPaths(context: Context): Set = + getPrefs(context).getStringSet(KEY_SUS_LOOP_PATHS, emptySet()) ?: emptySet() + + fun saveSusMaps(context: Context, maps: Set) = + getPrefs(context).edit { putStringSet(KEY_SUS_MAPS, maps) } + + fun getSusMaps(context: Context): Set = + getPrefs(context).getStringSet(KEY_SUS_MAPS, emptySet()) ?: emptySet() + + fun saveSusMounts(context: Context, mounts: Set) = + getPrefs(context).edit { putStringSet(KEY_SUS_MOUNTS, mounts) } + + fun getSusMounts(context: Context): Set = + getPrefs(context).getStringSet(KEY_SUS_MOUNTS, emptySet()) ?: emptySet() + + fun saveTryUmounts(context: Context, umounts: Set) = + getPrefs(context).edit { putStringSet(KEY_TRY_UMOUNTS, umounts) } + + fun getTryUmounts(context: Context): Set = + getPrefs(context).getStringSet(KEY_TRY_UMOUNTS, emptySet()) ?: emptySet() + + fun saveKstatConfigs(context: Context, configs: Set) = + getPrefs(context).edit { putStringSet(KEY_KSTAT_CONFIGS, configs) } + + fun getKstatConfigs(context: Context): Set = + getPrefs(context).getStringSet(KEY_KSTAT_CONFIGS, emptySet()) ?: emptySet() + + fun saveAddKstatPaths(context: Context, paths: Set) = + getPrefs(context).edit { putStringSet(KEY_ADD_KSTAT_PATHS, paths) } + + fun getAddKstatPaths(context: Context): Set = + getPrefs(context).getStringSet(KEY_ADD_KSTAT_PATHS, emptySet()) ?: emptySet() + + @SuppressLint("SdCardPath") + fun saveAndroidDataPath(context: Context, path: String) = + getPrefs(context).edit { putString(KEY_ANDROID_DATA_PATH, path) } + + @SuppressLint("SdCardPath") + fun getAndroidDataPath(context: Context): String = + getPrefs(context).getString(KEY_ANDROID_DATA_PATH, "/sdcard/Android/data") ?: "/sdcard/Android/data" + + @SuppressLint("SdCardPath") + fun saveSdcardPath(context: Context, path: String) = + getPrefs(context).edit { putString(KEY_SDCARD_PATH, path) } + + @SuppressLint("SdCardPath") + fun getSdcardPath(context: Context): String = + getPrefs(context).getString(KEY_SDCARD_PATH, "/sdcard") ?: "/sdcard" + + // 获取已安装的应用列表 + @SuppressLint("QueryPermissionsNeeded") + suspend fun getInstalledApps(): List = withContext(Dispatchers.IO) { + try { + val allApps = mutableMapOf() + + SuperUserViewModel.getAppsSafely().forEach { superUserApp -> + try { + val isSystemApp = superUserApp.packageInfo.applicationInfo?.let { + (it.flags and ApplicationInfo.FLAG_SYSTEM) != 0 + } ?: false + if (!isSystemApp) { + allApps[superUserApp.packageName] = AppInfo( + packageName = superUserApp.packageName, + appName = superUserApp.label, + packageInfo = superUserApp.packageInfo, + isSystemApp = false + ) + } + } catch (_: Exception) { + } + } + + // 检查每个应用的数据目录是否存在 + val filteredApps = allApps.values.map { appInfo -> + async(Dispatchers.IO) { + val dataPath = "$MEDIA_DATA_PATH/${appInfo.packageName}" + val exists = try { + val shell = getRootShell() + val outputList = mutableListOf() + val errorList = mutableListOf() + + val result = shell.newJob() + .add("[ -d \"$dataPath\" ] && echo 'exists' || echo 'not_exists'") + .to(outputList, errorList) + .exec() + + result.isSuccess && outputList.isNotEmpty() && outputList[0].trim() == "exists" + } catch (e: Exception) { + Log.w("SuSFSManager", "Failed to check directory for ${appInfo.packageName}: ${e.message}") + false + } + if (exists) appInfo else null + } + }.awaitAll().filterNotNull() + + filteredApps.sortedBy { it.appName } + } catch (e: Exception) { + e.printStackTrace() + emptyList() + } + } + + // 获取应用的UID + private suspend fun getAppUid(context: Context, packageName: String): Int? = withContext(Dispatchers.IO) { + try { + val superUserApp = SuperUserViewModel.getAppsSafely().find { it.packageName == packageName } + if (superUserApp != null) { + return@withContext superUserApp.packageInfo.applicationInfo?.uid + } + + // 从PackageManager中查找 + val packageManager = context.packageManager + val packageInfo = packageManager.getPackageInfo(packageName, 0) + packageInfo.applicationInfo?.uid + } catch (e: Exception) { + Log.w("SuSFSManager", "Failed to get UID for package $packageName: ${e.message}") + null + } + } + + private fun buildUidPath(uid: Int): String = "$CGROUP_UID_PATH_PREFIX$uid" + + + // 快捷添加应用路径 + suspend fun addAppPaths(context: Context, packageName: String): Boolean { + val androidDataPath = getAndroidDataPath(context) + getSdcardPath(context) + + val path1 = "$androidDataPath/$packageName" + val path2 = "$MEDIA_DATA_PATH/$packageName" + + val uid = getAppUid(context, packageName) + if (uid == null) { + Log.w("SuSFSManager", "Failed to get UID for package: $packageName") + return false + } + + val path3 = buildUidPath(uid) + + var successCount = 0 + val totalCount = 3 + + // 添加第一个路径(Android/data路径) + if (addSusPath(context, path1)) { + successCount++ + } + + // 添加第二个路径(媒体数据路径) + if (addSusPath(context, path2)) { + successCount++ + } + + // 添加第三个路径(UID路径) + if (addSusPath(context, path3)) { + successCount++ + } + + val success = successCount > 0 + + Log.d("SuSFSManager", "Added $successCount/$totalCount paths for $packageName (UID: $uid)") + + return success + } + + // 获取所有配置的Map + private fun getAllConfigurations(context: Context): Map { + return mapOf( + KEY_UNAME_VALUE to getUnameValue(context), + KEY_BUILD_TIME_VALUE to getBuildTimeValue(context), + KEY_AUTO_START_ENABLED to isAutoStartEnabled(context), + KEY_SUS_PATHS to getSusPaths(context), + KEY_SUS_LOOP_PATHS to getSusLoopPaths(context), + KEY_SUS_MAPS to getSusMaps(context), + KEY_SUS_MOUNTS to getSusMounts(context), + KEY_TRY_UMOUNTS to getTryUmounts(context), + KEY_ANDROID_DATA_PATH to getAndroidDataPath(context), + KEY_SDCARD_PATH to getSdcardPath(context), + KEY_ENABLE_LOG to getEnableLogState(context), + KEY_EXECUTE_IN_POST_FS_DATA to getExecuteInPostFsData(context), + KEY_KSTAT_CONFIGS to getKstatConfigs(context), + KEY_ADD_KSTAT_PATHS to getAddKstatPaths(context), + KEY_HIDE_SUS_MOUNTS_FOR_ALL_PROCS to getHideSusMountsForAllProcs(context), + KEY_ENABLE_HIDE_BL to getEnableHideBl(context), + KEY_ENABLE_CLEANUP_RESIDUE to getEnableCleanupResidue(context), + KEY_UMOUNT_FOR_ZYGOTE_ISO_SERVICE to getUmountForZygoteIsoService(context), + KEY_ENABLE_AVC_LOG_SPOOFING to getEnableAvcLogSpoofing(context), + ) + } + + //生成备份文件名 + private fun generateBackupFileName(): String { + val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()) + val timestamp = dateFormat.format(Date()) + return "SuSFS_Config_$timestamp$BACKUP_FILE_EXTENSION" + } + + // 获取设备信息 + private fun getDeviceInfo(): String { + return try { + "${Build.MANUFACTURER} ${Build.MODEL} (${Build.VERSION.RELEASE})" + } catch (_: Exception) { + "Unknown Device" + } + } + + // 创建配置备份 + suspend fun createBackup(context: Context, backupFilePath: String): Boolean = withContext(Dispatchers.IO) { + try { + val configurations = getAllConfigurations(context) + val backupData = BackupData( + version = getSuSFSVersion(), + timestamp = System.currentTimeMillis(), + deviceInfo = getDeviceInfo(), + configurations = configurations + ) + + val backupFile = File(backupFilePath) + backupFile.parentFile?.mkdirs() + + backupFile.writeText(backupData.toJson()) + + showToast(context, context.getString(R.string.susfs_backup_success, backupFile.name)) + true + } catch (e: Exception) { + e.printStackTrace() + showToast(context, context.getString(R.string.susfs_backup_failed, e.message ?: "Unknown error")) + false + } + } + + //从备份文件还原配置 + suspend fun restoreFromBackup(context: Context, backupFilePath: String): Boolean = withContext(Dispatchers.IO) { + try { + val backupFile = File(backupFilePath) + if (!backupFile.exists()) { + showToast(context, context.getString(R.string.susfs_backup_file_not_found)) + return@withContext false + } + + val backupContent = backupFile.readText() + val backupData = BackupData.fromJson(backupContent) + + if (backupData == null) { + showToast(context, context.getString(R.string.susfs_backup_invalid_format)) + return@withContext false + } + + // 检查备份版本兼容性 + if (backupData.version != getSuSFSVersion()) { + showToast(context, context.getString(R.string.susfs_backup_version_mismatch)) + } + + // 还原所有配置 + restoreConfigurations(context, backupData.configurations) + + // 如果自启动已启用,更新模块 + if (isAutoStartEnabled(context)) { + updateMagiskModule(context) + } + + val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + val backupDate = dateFormat.format(Date(backupData.timestamp)) + + showToast(context, context.getString(R.string.susfs_restore_success, backupDate, backupData.deviceInfo)) + true + } catch (e: Exception) { + e.printStackTrace() + showToast(context, context.getString(R.string.susfs_restore_failed, e.message ?: "Unknown error")) + false + } + } + + + // 还原配置到SharedPreferences + private fun restoreConfigurations(context: Context, configurations: Map) { + val prefs = getPrefs(context) + prefs.edit { + configurations.forEach { (key, value) -> + when (value) { + is String -> putString(key, value) + is Boolean -> putBoolean(key, value) + is Set<*> -> { + @Suppress("UNCHECKED_CAST") + putStringSet(key, value as Set) + } + is Int -> putInt(key, value) + is Long -> putLong(key, value) + is Float -> putFloat(key, value) + } + } + } + } + + // 验证备份文件 + suspend fun validateBackupFile(backupFilePath: String): BackupData? = withContext(Dispatchers.IO) { + try { + val backupFile = File(backupFilePath) + if (!backupFile.exists()) { + return@withContext null + } + + val backupContent = backupFile.readText() + BackupData.fromJson(backupContent) + } catch (e: Exception) { + e.printStackTrace() + null + } + } + + // 获取备份文件路径 + fun getDefaultBackupFileName(): String { + return generateBackupFileName() + } + + // 槽位信息获取 + suspend fun getCurrentSlotInfo(): List = withContext(Dispatchers.IO) { + try { + val slotInfoList = mutableListOf() + val shell = Shell.getShell() + + listOf("boot_a", "boot_b").forEach { slot -> + val unameCmd = + "strings -n 20 /dev/block/by-name/$slot | awk '/Linux version/ && ++c==2 {print $3; exit}'" + val buildTimeCmd = "strings -n 20 /dev/block/by-name/$slot | sed -n '/Linux version.*#/{s/.*#/#/p;q}'" + + val uname = runCmd(shell, unameCmd).trim() + val buildTime = runCmd(shell, buildTimeCmd).trim() + + if (uname.isNotEmpty() && buildTime.isNotEmpty()) { + slotInfoList.add(SlotInfo(slot, uname.ifEmpty { "unknown" }, buildTime.ifEmpty { "unknown" })) + } + } + + slotInfoList + } catch (e: Exception) { + e.printStackTrace() + emptyList() + } + } + + suspend fun getCurrentActiveSlot(): String = withContext(Dispatchers.IO) { + try { + val shell = Shell.getShell() + val suffix = runCmd(shell, "getprop ro.boot.slot_suffix").trim() + when (suffix) { + "_a" -> "boot_a" + "_b" -> "boot_b" + else -> "unknown" + } + } catch (_: Exception) { + "unknown" + } + } + + // 二进制文件管理 + private suspend fun copyBinaryFromAssets(context: Context): String? = withContext(Dispatchers.IO) { + try { + val binaryName = getSuSFSBinaryName(context) + val targetPath = getSuSFSTargetPath() + val tempFile = File(context.cacheDir, binaryName) + + context.assets.open(binaryName).use { input -> + FileOutputStream(tempFile).use { output -> + input.copyTo(output) + } + } + + val success = runCmdWithResult("cp '${tempFile.absolutePath}' '$targetPath' && chmod 755 '$targetPath'").isSuccess + tempFile.delete() + + if (success && runCmdWithResult("test -f '$targetPath'").isSuccess) targetPath else null + } catch (e: IOException) { + e.printStackTrace() + null + } + } + + // 命令执行 + private suspend fun executeSusfsCommand(context: Context, command: String): Boolean = withContext(Dispatchers.IO) { + try { + val binaryPath = copyBinaryFromAssets(context) ?: run { + showToast(context, context.getString(R.string.susfs_binary_not_found)) + return@withContext false + } + + val result = runCmdWithResult("$binaryPath $command") + + if (!result.isSuccess) { + showToast(context, "${context.getString(R.string.susfs_command_failed)}\n${result.output}\n${result.errorOutput}") + } + + result.isSuccess + } catch (e: Exception) { + e.printStackTrace() + showToast(context, context.getString(R.string.susfs_command_error, e.message ?: "Unknown error")) + false + } + } + + private suspend fun executeSusfsCommandWithOutput(context: Context, command: String): CommandResult = withContext(Dispatchers.IO) { + try { + val binaryPath = copyBinaryFromAssets(context) ?: return@withContext CommandResult( + false, "", context.getString(R.string.susfs_binary_not_found) + ) + runCmdWithResult("$binaryPath $command") + } catch (e: Exception) { + e.printStackTrace() + CommandResult(false, "", e.message ?: "Unknown error") + } + } + + private suspend fun showToast(context: Context, message: String) = withContext(Dispatchers.Main) { + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } + + /** + * 模块管理 + */ + private suspend fun updateMagiskModule(context: Context): Boolean { + return removeMagiskModule() && createMagiskModule(context) + } + + /** + * 模块创建方法 + */ + private suspend fun createMagiskModule(context: Context): Boolean = withContext(Dispatchers.IO) { + try { + val config = getCurrentModuleConfig(context) + + // 创建模块目录 + if (!runCmdWithResult("mkdir -p $MODULE_PATH").isSuccess) return@withContext false + + // 创建module.prop + val moduleProp = ScriptGenerator.generateModuleProp(MODULE_ID) + if (!runCmdWithResult("cat > $MODULE_PATH/module.prop << 'EOF'\n$moduleProp\nEOF").isSuccess) return@withContext false + + // 生成并创建所有脚本文件 + val scripts = ScriptGenerator.generateAllScripts(config) + + scripts.all { (filename, content) -> + runCmdWithResult("cat > $MODULE_PATH/$filename << 'EOF'\n$content\nEOF").isSuccess && + runCmdWithResult("chmod 755 $MODULE_PATH/$filename").isSuccess + } + } catch (e: Exception) { + e.printStackTrace() + false + } + } + + private suspend fun removeMagiskModule(): Boolean = withContext(Dispatchers.IO) { + try { + runCmdWithResult("rm -rf $MODULE_PATH").isSuccess + } catch (e: Exception) { + e.printStackTrace() + false + } + } + + // 功能状态获取 + suspend fun getEnabledFeatures(context: Context): List = withContext(Dispatchers.IO) { + try { + val featuresOutput = getSuSFSFeatures() + + if (featuresOutput.isNotBlank() && featuresOutput != "Invalid") { + parseEnabledFeaturesFromOutput(context, featuresOutput) + } else { + getDefaultDisabledFeatures(context) + } + } catch (e: Exception) { + e.printStackTrace() + getDefaultDisabledFeatures(context) + } + } + + private fun parseEnabledFeaturesFromOutput(context: Context, featuresOutput: String): List { + val enabledConfigs = featuresOutput.lines() + .map { it.trim() } + .filter { it.isNotEmpty() } + .toSet() + + val featureMap = mapOf( + "CONFIG_KSU_SUSFS_SUS_PATH" to context.getString(R.string.sus_path_feature_label), + "CONFIG_KSU_SUSFS_SUS_MOUNT" to context.getString(R.string.sus_mount_feature_label), + "CONFIG_KSU_SUSFS_TRY_UMOUNT" to context.getString(R.string.try_umount_feature_label), + "CONFIG_KSU_SUSFS_SPOOF_UNAME" to context.getString(R.string.spoof_uname_feature_label), + "CONFIG_KSU_SUSFS_SPOOF_CMDLINE_OR_BOOTCONFIG" to context.getString(R.string.spoof_cmdline_feature_label), + "CONFIG_KSU_SUSFS_OPEN_REDIRECT" to context.getString(R.string.open_redirect_feature_label), + "CONFIG_KSU_SUSFS_ENABLE_LOG" to context.getString(R.string.enable_log_feature_label), + "CONFIG_KSU_SUSFS_AUTO_ADD_TRY_UMOUNT_FOR_BIND_MOUNT" to context.getString(R.string.auto_try_umount_bind_feature_label), + "CONFIG_KSU_SUSFS_HIDE_KSU_SUSFS_SYMBOLS" to context.getString(R.string.hide_symbols_feature_label), + "CONFIG_KSU_SUSFS_SUS_KSTAT" to context.getString(R.string.sus_kstat_feature_label), + ) + + + return featureMap.map { (configKey, displayName) -> + val isEnabled = enabledConfigs.contains(configKey) + + val statusText = if (isEnabled) { + context.getString(R.string.susfs_feature_enabled) + } else { + context.getString(R.string.susfs_feature_disabled) + } + + val canConfigure = displayName == context.getString(R.string.enable_log_feature_label) + + EnabledFeature(displayName, isEnabled, statusText, canConfigure) + }.sortedBy { it.name } + } + + private fun getDefaultDisabledFeatures(context: Context): List { + val defaultFeatures = listOf( + "sus_path_feature_label" to context.getString(R.string.sus_path_feature_label), + "sus_mount_feature_label" to context.getString(R.string.sus_mount_feature_label), + "try_umount_feature_label" to context.getString(R.string.try_umount_feature_label), + "spoof_uname_feature_label" to context.getString(R.string.spoof_uname_feature_label), + "spoof_cmdline_feature_label" to context.getString(R.string.spoof_cmdline_feature_label), + "open_redirect_feature_label" to context.getString(R.string.open_redirect_feature_label), + "enable_log_feature_label" to context.getString(R.string.enable_log_feature_label), + "auto_try_umount_bind_feature_label" to context.getString(R.string.auto_try_umount_bind_feature_label), + "hide_symbols_feature_label" to context.getString(R.string.hide_symbols_feature_label), + "sus_kstat_feature_label" to context.getString(R.string.sus_kstat_feature_label), + + ) + + return defaultFeatures.map { (_, displayName) -> + EnabledFeature( + name = displayName, + isEnabled = false, + statusText = context.getString(R.string.susfs_feature_disabled), + canConfigure = displayName == context.getString(R.string.enable_log_feature_label) + ) + }.sortedBy { it.name } + } + + // sus日志开关 + suspend fun setEnableLog(context: Context, enabled: Boolean): Boolean { + val success = executeSusfsCommand(context, "enable_log ${if (enabled) 1 else 0}") + if (success) { + saveEnableLogState(context, enabled) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, if (enabled) context.getString(R.string.susfs_log_enabled) else context.getString(R.string.susfs_log_disabled)) + } + return success + } + + // AVC日志欺骗开关 + suspend fun setEnableAvcLogSpoofing(context: Context, enabled: Boolean): Boolean { + val success = executeSusfsCommand(context, "enable_avc_log_spoofing ${if (enabled) 1 else 0}") + if (success) { + saveEnableAvcLogSpoofing(context, enabled) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, if (enabled) + context.getString(R.string.avc_log_spoofing_enabled) + else + context.getString(R.string.avc_log_spoofing_disabled) + ) + } + return success + } + + // SUS挂载隐藏控制 + suspend fun setHideSusMountsForAllProcs(context: Context, hideForAll: Boolean): Boolean { + val success = executeSusfsCommand(context, "hide_sus_mnts_for_all_procs ${if (hideForAll) 1 else 0}") + if (success) { + saveHideSusMountsForAllProcs(context, hideForAll) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, if (hideForAll) + context.getString(R.string.susfs_hide_mounts_all_enabled) + else + context.getString(R.string.susfs_hide_mounts_all_disabled) + ) + } + return success + } + + // uname和构建时间 + @SuppressLint("StringFormatMatches") + suspend fun setUname(context: Context, unameValue: String, buildTimeValue: String): Boolean { + val success = executeSusfsCommand(context, "set_uname '$unameValue' '$buildTimeValue'") + if (success) { + saveUnameValue(context, unameValue) + saveBuildTimeValue(context, buildTimeValue) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, context.getString(R.string.susfs_uname_set_success, unameValue, buildTimeValue)) + } + return success + } + + // 添加SUS路径 + @SuppressLint("StringFormatInvalid") + suspend fun addSusPath(context: Context, path: String): Boolean { + // 先设置路径配置 + val androidDataPath = getAndroidDataPath(context) + val sdcardPath = getSdcardPath(context) + + // 先设置Android Data路径 + val androidDataSuccess = executeSusfsCommand(context, "set_android_data_root_path '$androidDataPath'") + if (androidDataSuccess) { + showToast(context, context.getString(R.string.susfs_android_data_path_set, androidDataPath)) + } + + // 再设置SD卡路径 + val sdcardSuccess = executeSusfsCommand(context, "set_sdcard_root_path '$sdcardPath'") + if (sdcardSuccess) { + showToast(context, context.getString(R.string.susfs_sdcard_path_set, sdcardPath)) + } + + // 如果路径设置失败,记录但不阻止继续执行 + if (!androidDataSuccess || !sdcardSuccess) { + showToast(context, context.getString(R.string.susfs_path_setup_warning)) + } + + // 执行添加SUS路径命令 + val result = executeSusfsCommandWithOutput(context, "add_sus_path '$path'") + val isActuallySuccessful = result.isSuccess && !result.output.contains("not found, skip adding") + + if (isActuallySuccessful) { + saveSusPaths(context, getSusPaths(context) + path) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, context.getString(R.string.susfs_sus_path_added_success, path)) + } else { + val errorMessage = if (result.output.contains("not found, skip adding")) { + context.getString(R.string.susfs_path_not_found_error, path) + } else { + "${context.getString(R.string.susfs_command_failed)}\n${result.output}\n${result.errorOutput}" + } + showToast(context, errorMessage) + } + return isActuallySuccessful + } + + suspend fun removeSusPath(context: Context, path: String): Boolean { + saveSusPaths(context, getSusPaths(context) - path) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, "SUS path removed: $path") + return true + } + + // 编辑SUS路径 + suspend fun editSusPath(context: Context, oldPath: String, newPath: String): Boolean { + return try { + val currentPaths = getSusPaths(context).toMutableSet() + if (!currentPaths.remove(oldPath)) { + showToast(context, "Original path not found: $oldPath") + return false + } + + saveSusPaths(context, currentPaths) + + val success = addSusPath(context, newPath) + + if (success) { + showToast(context, "SUS path updated: $oldPath -> $newPath") + return true + } else { + // 如果添加新路径失败,恢复旧路径 + currentPaths.add(oldPath) + saveSusPaths(context, currentPaths) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, "Failed to update path, reverted to original") + return false + } + } catch (e: Exception) { + e.printStackTrace() + showToast(context, "Error updating SUS path: ${e.message}") + false + } + } + + // 循环路径相关方法 + @SuppressLint("SdCardPath") + private fun isValidLoopPath(path: String): Boolean { + return !path.startsWith("/storage/") && !path.startsWith("/sdcard/") + } + + @SuppressLint("StringFormatInvalid") + suspend fun addSusLoopPath(context: Context, path: String): Boolean { + // 检查路径是否有效 + if (!isValidLoopPath(path)) { + showToast(context, context.getString(R.string.susfs_loop_path_invalid_location)) + return false + } + + // 执行添加循环路径命令 + val result = executeSusfsCommandWithOutput(context, "add_sus_path_loop '$path'") + val isActuallySuccessful = result.isSuccess && !result.output.contains("not found, skip adding") + + if (isActuallySuccessful) { + saveSusLoopPaths(context, getSusLoopPaths(context) + path) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, context.getString(R.string.susfs_loop_path_added_success, path)) + } else { + val errorMessage = if (result.output.contains("not found, skip adding")) { + context.getString(R.string.susfs_path_not_found_error, path) + } else { + "${context.getString(R.string.susfs_command_failed)}\n${result.output}\n${result.errorOutput}" + } + showToast(context, errorMessage) + } + return isActuallySuccessful + } + + suspend fun removeSusLoopPath(context: Context, path: String): Boolean { + saveSusLoopPaths(context, getSusLoopPaths(context) - path) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, context.getString(R.string.susfs_loop_path_removed, path)) + return true + } + + // 编辑循环路径 + suspend fun editSusLoopPath(context: Context, oldPath: String, newPath: String): Boolean { + // 检查新路径是否有效 + if (!isValidLoopPath(newPath)) { + showToast(context, context.getString(R.string.susfs_loop_path_invalid_location)) + return false + } + + return try { + val currentPaths = getSusLoopPaths(context).toMutableSet() + if (!currentPaths.remove(oldPath)) { + showToast(context, "Original loop path not found: $oldPath") + return false + } + + saveSusLoopPaths(context, currentPaths) + + val success = addSusLoopPath(context, newPath) + + if (success) { + showToast(context, context.getString(R.string.susfs_loop_path_updated, oldPath, newPath)) + return true + } else { + // 如果添加新路径失败,恢复旧路径 + currentPaths.add(oldPath) + saveSusLoopPaths(context, currentPaths) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, "Failed to update loop path, reverted to original") + return false + } + } catch (e: Exception) { + e.printStackTrace() + showToast(context, "Error updating SUS loop path: ${e.message}") + false + } + } + + // 添加 SUS Maps + suspend fun addSusMap(context: Context, map: String): Boolean { + val success = executeSusfsCommand(context, "add_sus_map '$map'") + if (success) { + saveSusMaps(context, getSusMaps(context) + map) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, context.getString(R.string.susfs_sus_map_added_success, map)) + } + return success + } + + suspend fun removeSusMap(context: Context, map: String): Boolean { + saveSusMaps(context, getSusMaps(context) - map) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, context.getString(R.string.susfs_sus_map_removed, map)) + return true + } + + suspend fun editSusMap(context: Context, oldMap: String, newMap: String): Boolean { + return try { + val currentMaps = getSusMaps(context).toMutableSet() + if (!currentMaps.remove(oldMap)) { + showToast(context, "Original SUS map not found: $oldMap") + return false + } + + saveSusMaps(context, currentMaps) + + val success = addSusMap(context, newMap) + + if (success) { + showToast(context, context.getString(R.string.susfs_sus_map_updated, oldMap, newMap)) + return true + } else { + // 如果添加新映射失败,恢复旧映射 + currentMaps.add(oldMap) + saveSusMaps(context, currentMaps) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, "Failed to update SUS map, reverted to original") + return false + } + } catch (e: Exception) { + e.printStackTrace() + showToast(context, "Error updating SUS map: ${e.message}") + false + } + } + + // 添加SUS挂载 + suspend fun addSusMount(context: Context, mount: String): Boolean { + val success = executeSusfsCommand(context, "add_sus_mount '$mount'") + if (success) { + saveSusMounts(context, getSusMounts(context) + mount) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + } + return success + } + + suspend fun removeSusMount(context: Context, mount: String): Boolean { + saveSusMounts(context, getSusMounts(context) - mount) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, "Removed SUS mount: $mount") + return true + } + + // 编辑SUS挂载 + suspend fun editSusMount(context: Context, oldMount: String, newMount: String): Boolean { + return try { + val currentMounts = getSusMounts(context).toMutableSet() + if (!currentMounts.remove(oldMount)) { + showToast(context, "Original mount not found: $oldMount") + return false + } + + saveSusMounts(context, currentMounts) + + val success = addSusMount(context, newMount) + + if (success) { + showToast(context, "SUS mount updated: $oldMount -> $newMount") + return true + } else { + // 如果添加新挂载点失败,恢复旧挂载点 + currentMounts.add(oldMount) + saveSusMounts(context, currentMounts) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, "Failed to update mount, reverted to original") + return false + } + } catch (e: Exception) { + e.printStackTrace() + showToast(context, "Error updating SUS mount: ${e.message}") + false + } + } + + // 添加尝试卸载 + suspend fun addTryUmount(context: Context, path: String, mode: Int): Boolean { + val commandSuccess = executeSusfsCommand(context, "add_try_umount '$path' $mode") + saveTryUmounts(context, getTryUmounts(context) + "$path|$mode") + if (isAutoStartEnabled(context)) updateMagiskModule(context) + + showToast(context, if (commandSuccess) { + context.getString(R.string.susfs_try_umount_added_success, path) + } else { + context.getString(R.string.susfs_try_umount_added_saved, path) + }) + return true + } + + suspend fun removeTryUmount(context: Context, umountEntry: String): Boolean { + saveTryUmounts(context, getTryUmounts(context) - umountEntry) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + val path = umountEntry.split("|").firstOrNull() ?: umountEntry + showToast(context, "Removed Try to uninstall: $path") + return true + } + + // 编辑尝试卸载 + suspend fun editTryUmount(context: Context, oldEntry: String, newPath: String, newMode: Int): Boolean { + return try { + val currentUmounts = getTryUmounts(context).toMutableSet() + if (!currentUmounts.remove(oldEntry)) { + showToast(context, "Original umount entry not found: $oldEntry") + return false + } + + saveTryUmounts(context, currentUmounts) + + val success = addTryUmount(context, newPath, newMode) + + if (success) { + showToast(context, "Try umount updated: $oldEntry -> $newPath|$newMode") + return true + } else { + // 如果添加新条目失败,恢复旧条目 + currentUmounts.add(oldEntry) + saveTryUmounts(context, currentUmounts) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, "Failed to update umount entry, reverted to original") + return false + } + } catch (e: Exception) { + e.printStackTrace() + showToast(context, "Error updating try umount: ${e.message}") + false + } + } + + // Zygote隔离服务卸载控制 + suspend fun setUmountForZygoteIsoService(context: Context, enabled: Boolean): Boolean { + val result = executeSusfsCommandWithOutput(context, "umount_for_zygote_iso_service ${if (enabled) 1 else 0}") + val success = result.isSuccess && result.output.isEmpty() + + if (success) { + saveUmountForZygoteIsoService(context, enabled) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, if (enabled) + context.getString(R.string.umount_zygote_iso_service_enabled) + else + context.getString(R.string.umount_zygote_iso_service_disabled) + ) + } else { + showToast(context, context.getString(R.string.susfs_command_failed)) + } + return success + } + + // 添加kstat配置 + suspend fun addKstatStatically(context: Context, path: String, ino: String, dev: String, nlink: String, + size: String, atime: String, atimeNsec: String, mtime: String, mtimeNsec: String, + ctime: String, ctimeNsec: String, blocks: String, blksize: String): Boolean { + val command = "add_sus_kstat_statically '$path' '$ino' '$dev' '$nlink' '$size' '$atime' '$atimeNsec' '$mtime' '$mtimeNsec' '$ctime' '$ctimeNsec' '$blocks' '$blksize'" + val success = executeSusfsCommand(context, command) + if (success) { + val configEntry = "$path|$ino|$dev|$nlink|$size|$atime|$atimeNsec|$mtime|$mtimeNsec|$ctime|$ctimeNsec|$blocks|$blksize" + saveKstatConfigs(context, getKstatConfigs(context) + configEntry) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, context.getString(R.string.kstat_static_config_added, path)) + } + return success + } + + suspend fun removeKstatConfig(context: Context, config: String): Boolean { + saveKstatConfigs(context, getKstatConfigs(context) - config) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + val path = config.split("|").firstOrNull() ?: config + showToast(context, context.getString(R.string.kstat_config_removed, path)) + return true + } + + // 编辑kstat配置 + @SuppressLint("StringFormatInvalid") + suspend fun editKstatConfig(context: Context, oldConfig: String, path: String, ino: String, dev: String, nlink: String, + size: String, atime: String, atimeNsec: String, mtime: String, mtimeNsec: String, + ctime: String, ctimeNsec: String, blocks: String, blksize: String): Boolean { + return try { + val currentConfigs = getKstatConfigs(context).toMutableSet() + if (!currentConfigs.remove(oldConfig)) { + showToast(context, "Original kstat config not found") + return false + } + + saveKstatConfigs(context, currentConfigs) + + val success = addKstatStatically(context, path, ino, dev, nlink, size, atime, atimeNsec, + mtime, mtimeNsec, ctime, ctimeNsec, blocks, blksize) + + if (success) { + showToast(context, context.getString(R.string.kstat_config_updated, path)) + return true + } else { + // 如果添加新配置失败,恢复旧配置 + currentConfigs.add(oldConfig) + saveKstatConfigs(context, currentConfigs) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, "Failed to update kstat config, reverted to original") + return false + } + } catch (e: Exception) { + e.printStackTrace() + showToast(context, "Error updating kstat config: ${e.message}") + false + } + } + + // 添加kstat路径 + suspend fun addKstat(context: Context, path: String): Boolean { + val success = executeSusfsCommand(context, "add_sus_kstat '$path'") + if (success) { + saveAddKstatPaths(context, getAddKstatPaths(context) + path) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, context.getString(R.string.kstat_path_added, path)) + } + return success + } + + suspend fun removeAddKstat(context: Context, path: String): Boolean { + saveAddKstatPaths(context, getAddKstatPaths(context) - path) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, context.getString(R.string.kstat_path_removed, path)) + return true + } + + // 编辑kstat路径 + @SuppressLint("StringFormatInvalid") + suspend fun editAddKstat(context: Context, oldPath: String, newPath: String): Boolean { + return try { + val currentPaths = getAddKstatPaths(context).toMutableSet() + if (!currentPaths.remove(oldPath)) { + showToast(context, "Original kstat path not found: $oldPath") + return false + } + + saveAddKstatPaths(context, currentPaths) + + val success = addKstat(context, newPath) + + if (success) { + showToast(context, context.getString(R.string.kstat_path_updated, oldPath, newPath)) + return true + } else { + // 如果添加新路径失败,恢复旧路径 + currentPaths.add(oldPath) + saveAddKstatPaths(context, currentPaths) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, "Failed to update kstat path, reverted to original") + return false + } + } catch (e: Exception) { + e.printStackTrace() + showToast(context, "Error updating kstat path: ${e.message}") + false + } + } + + // 更新kstat + suspend fun updateKstat(context: Context, path: String): Boolean { + val success = executeSusfsCommand(context, "update_sus_kstat '$path'") + if (success) showToast(context, context.getString(R.string.kstat_updated, path)) + return success + } + + // 更新kstat全克隆 + suspend fun updateKstatFullClone(context: Context, path: String): Boolean { + val success = executeSusfsCommand(context, "update_sus_kstat_full_clone '$path'") + if (success) showToast(context, context.getString(R.string.kstat_full_clone_updated, path)) + return success + } + + // 设置Android数据路径 + suspend fun setAndroidDataPath(context: Context, path: String): Boolean { + val success = executeSusfsCommand(context, "set_android_data_root_path '$path'") + if (success) { + saveAndroidDataPath(context, path) + if (isAutoStartEnabled(context)) { + CoroutineScope(Dispatchers.Default).launch { + updateMagiskModule(context) + } + } + } + return success + } + + // 设置SD卡路径 + suspend fun setSdcardPath(context: Context, path: String): Boolean { + val success = executeSusfsCommand(context, "set_sdcard_root_path '$path'") + if (success) { + saveSdcardPath(context, path) + if (isAutoStartEnabled(context)) { + CoroutineScope(Dispatchers.Default).launch { + updateMagiskModule(context) + } + } + } + return success + } + + /** + * 自启动配置检查 + */ + fun hasConfigurationForAutoStart(context: Context): Boolean { + val config = getCurrentModuleConfig(context) + return config.hasAutoStartConfig() || runBlocking { + getEnabledFeatures(context).any { it.isEnabled } + } + } + + /** + * 自启动配置方法 + */ + suspend fun configureAutoStart(context: Context, enabled: Boolean): Boolean = withContext(Dispatchers.IO) { + try { + if (enabled) { + if (!hasConfigurationForAutoStart(context)) { + showToast(context, context.getString(R.string.susfs_no_config_to_autostart)) + return@withContext false + } + + val targetPath = getSuSFSTargetPath() + if (!runCmdWithResult("test -f '$targetPath'").isSuccess) { + copyBinaryFromAssets(context) ?: run { + showToast(context, context.getString(R.string.susfs_binary_not_found)) + return@withContext false + } + } + + val success = createMagiskModule(context) + if (success) { + setAutoStartEnabled(context, true) + showToast(context, context.getString(R.string.susfs_autostart_enabled_success, MODULE_PATH)) + } else { + showToast(context, context.getString(R.string.susfs_autostart_enable_failed)) + } + success + } else { + val success = removeMagiskModule() + if (success) { + setAutoStartEnabled(context, false) + showToast(context, context.getString(R.string.susfs_autostart_disabled_success)) + } else { + showToast(context, context.getString(R.string.susfs_autostart_disable_failed)) + } + success + } + } catch (e: Exception) { + e.printStackTrace() + showToast(context, context.getString(R.string.susfs_autostart_error, e.message ?: "Unknown error")) + false + } + } + + suspend fun resetToDefault(context: Context): Boolean { + val success = setUname(context, DEFAULT_UNAME, DEFAULT_BUILD_TIME) + if (success && isAutoStartEnabled(context)) { + configureAutoStart(context, false) + } + return success + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSModuleScripts.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSModuleScripts.kt new file mode 100644 index 00000000..73ec1f9a --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSModuleScripts.kt @@ -0,0 +1,550 @@ +package com.sukisu.ultra.ui.susfs.util + +import android.annotation.SuppressLint + +/** + * Magisk模块脚本生成器 + * 用于生成各种启动脚本的内容 + */ +object ScriptGenerator { + + // 常量定义 + private const val DEFAULT_UNAME = "default" + private const val DEFAULT_BUILD_TIME = "default" + private const val LOG_DIR = "/data/adb/ksu/log" + + /** + * 生成所有脚本文件 + */ + fun generateAllScripts(config: SuSFSManager.ModuleConfig): Map { + return mapOf( + "service.sh" to generateServiceScript(config), + "post-fs-data.sh" to generatePostFsDataScript(config), + "post-mount.sh" to generatePostMountScript(config), + "boot-completed.sh" to generateBootCompletedScript(config) + ) + } + + // 日志相关的通用脚本片段 + private fun generateLogSetup(logFileName: String): String = """ + # 日志目录 + LOG_DIR="$LOG_DIR" + LOG_FILE="${'$'}LOG_DIR/$logFileName" + + # 创建日志目录 + mkdir -p "${'$'}LOG_DIR" + + # 获取当前时间 + get_current_time() { + date '+%Y-%m-%d %H:%M:%S' + } + """.trimIndent() + + // 二进制文件检查的通用脚本片段 + private fun generateBinaryCheck(targetPath: String): String = """ + # 检查SuSFS二进制文件 + SUSFS_BIN="$targetPath" + if [ ! -f "${'$'}SUSFS_BIN" ]; then + echo "$(get_current_time): SuSFS二进制文件未找到: ${'$'}SUSFS_BIN" >> "${'$'}LOG_FILE" + exit 1 + fi + """.trimIndent() + + /** + * 生成service.sh脚本内容 + */ + @SuppressLint("SdCardPath") + private fun generateServiceScript(config: SuSFSManager.ModuleConfig): String { + return buildString { + appendLine("#!/system/bin/sh") + appendLine("# SuSFS Service Script") + appendLine("# 在系统服务启动后执行") + appendLine() + appendLine(generateLogSetup("susfs_service.log")) + appendLine() + appendLine(generateBinaryCheck(config.targetPath)) + appendLine() + + if (shouldConfigureInService(config)) { + // 添加SUS路径 + if (config.susPaths.isNotEmpty()) { + appendLine() + appendLine("until [ -d \"/sdcard/Android\" ]; do sleep 1; done") + appendLine("sleep 45") + generateSusPathsSection(config.susPaths) + } + + // 设置uname和构建时间 + generateUnameSection(config) + + // 添加Kstat配置 + generateKstatSection(config.kstatConfigs, config.addKstatPaths) + } + + // 添加日志设置 + generateLogSettingSection(config.enableLog) + + // 隐藏BL相关配置 + if (config.enableHideBl) { + generateHideBlSection() + } + + // 清理工具残留 + if (config.enableCleanupResidue) { + generateCleanupResidueSection() + } + + appendLine("echo \"$(get_current_time): Service脚本执行完成\" >> \"${'$'}LOG_FILE\"") + } + } + + /** + * 判断是否需要在service中配置 + */ + private fun shouldConfigureInService(config: SuSFSManager.ModuleConfig): Boolean { + return config.susPaths.isNotEmpty() || + config.susLoopPaths.isNotEmpty() || + config.kstatConfigs.isNotEmpty() || + config.addKstatPaths.isNotEmpty() || + (!config.executeInPostFsData && (config.unameValue != DEFAULT_UNAME || config.buildTimeValue != DEFAULT_BUILD_TIME)) + } + + private fun StringBuilder.generateLogSettingSection(enableLog: Boolean) { + appendLine("# 设置日志启用状态") + val logValue = if (enableLog) 1 else 0 + appendLine("\"${'$'}SUSFS_BIN\" enable_log $logValue") + appendLine("echo \"$(get_current_time): 日志功能设置为: ${if (enableLog) "启用" else "禁用"}\" >> \"${'$'}LOG_FILE\"") + appendLine() + } + + private fun StringBuilder.generateAvcLogSpoofingSection(enableAvcLogSpoofing: Boolean) { + appendLine("# 设置AVC日志欺骗状态") + val avcLogValue = if (enableAvcLogSpoofing) 1 else 0 + appendLine("\"${'$'}SUSFS_BIN\" enable_avc_log_spoofing $avcLogValue") + appendLine("echo \"$(get_current_time): AVC日志欺骗功能设置为: ${if (enableAvcLogSpoofing) "启用" else "禁用"}\" >> \"${'$'}LOG_FILE\"") + appendLine() + } + + private fun StringBuilder.generateSusPathsSection(susPaths: Set) { + if (susPaths.isNotEmpty()) { + appendLine("# 添加SUS路径") + susPaths.forEach { path -> + appendLine("\"${'$'}SUSFS_BIN\" add_sus_path '$path'") + appendLine("echo \"$(get_current_time): 添加SUS路径: $path\" >> \"${'$'}LOG_FILE\"") + } + appendLine() + } + } + + private fun StringBuilder.generateSusLoopPathsSection(susLoopPaths: Set) { + if (susLoopPaths.isNotEmpty()) { + appendLine("# 添加SUS循环路径") + susLoopPaths.forEach { path -> + appendLine("\"${'$'}SUSFS_BIN\" add_sus_path_loop '$path'") + appendLine("echo \"$(get_current_time): 添加SUS循环路径: $path\" >> \"${'$'}LOG_FILE\"") + } + appendLine() + } + } + + @SuppressLint("SdCardPath") + private fun StringBuilder.generateKstatSection( + kstatConfigs: Set, + addKstatPaths: Set + ) { + // 添加Kstat路径 + if (addKstatPaths.isNotEmpty()) { + appendLine("# 添加Kstat路径") + addKstatPaths.forEach { path -> + appendLine("\"${'$'}SUSFS_BIN\" add_sus_kstat '$path'") + appendLine("echo \"$(get_current_time): 添加Kstat路径: $path\" >> \"${'$'}LOG_FILE\"") + } + appendLine() + } + + // 添加Kstat静态配置 + if (kstatConfigs.isNotEmpty()) { + appendLine("# 添加Kstat静态配置") + kstatConfigs.forEach { config -> + val parts = config.split("|") + if (parts.size >= 13) { + val path = parts[0] + val params = parts.drop(1).joinToString("' '", "'", "'") + appendLine() + appendLine("\"${'$'}SUSFS_BIN\" add_sus_kstat_statically '$path' $params") + appendLine("echo \"$(get_current_time): 添加Kstat静态配置: $path\" >> \"${'$'}LOG_FILE\"") + appendLine() + appendLine("\"${'$'}SUSFS_BIN\" update_sus_kstat '$path'") + appendLine("echo \"$(get_current_time): 更新Kstat配置: $path\" >> \"${'$'}LOG_FILE\"") + } + } + appendLine() + } + } + + private fun StringBuilder.generateUnameSection(config: SuSFSManager.ModuleConfig) { + if (!config.executeInPostFsData && (config.unameValue != DEFAULT_UNAME || config.buildTimeValue != DEFAULT_BUILD_TIME)) { + appendLine("# 设置uname和构建时间") + appendLine("\"${'$'}SUSFS_BIN\" set_uname '${config.unameValue}' '${config.buildTimeValue}'") + appendLine("echo \"$(get_current_time): 设置uname为: ${config.unameValue}, 构建时间为: ${config.buildTimeValue}\" >> \"${'$'}LOG_FILE\"") + appendLine() + } + } + + private fun StringBuilder.generateHideBlSection() { + appendLine("# 隐藏BL 来自 Shamiko 脚本") + appendLine( + """ + RESETPROP_BIN="/data/adb/ksu/bin/resetprop" + + check_reset_prop() { + local NAME=$1 + local EXPECTED=$2 + local VALUE=$("${'$'}RESETPROP_BIN" ${'$'}NAME) + [ -z ${'$'}VALUE ] || [ ${'$'}VALUE = ${'$'}EXPECTED ] || "${'$'}RESETPROP_BIN" ${'$'}NAME ${'$'}EXPECTED + } + + check_missing_prop() { + local NAME=$1 + local EXPECTED=$2 + local VALUE=$("${'$'}RESETPROP_BIN" ${'$'}NAME) + [ -z ${'$'}VALUE ] && "${'$'}RESETPROP_BIN" ${'$'}NAME ${'$'}EXPECTED + } + + check_missing_match_prop() { + local NAME=$1 + local EXPECTED=$2 + local VALUE=$("${'$'}RESETPROP_BIN" ${'$'}NAME) + [ -z ${'$'}VALUE ] || [ ${'$'}VALUE = ${'$'}EXPECTED ] || "${'$'}RESETPROP_BIN" ${'$'}NAME ${'$'}EXPECTED + [ -z ${'$'}VALUE ] && "${'$'}RESETPROP_BIN" ${'$'}NAME ${'$'}EXPECTED + } + + contains_reset_prop() { + local NAME=$1 + local CONTAINS=$2 + local NEWVAL=$3 + case "$("${'$'}RESETPROP_BIN" ${'$'}NAME)" in + *"${'$'}CONTAINS"*) "${'$'}RESETPROP_BIN" ${'$'}NAME ${'$'}NEWVAL ;; + esac + } + """.trimIndent()) + appendLine() + appendLine("sleep 30") + appendLine() + appendLine("\"${'$'}RESETPROP_BIN\" -w sys.boot_completed 0") + + // 添加所有系统属性重置 + val systemProps = listOf( + "ro.boot.vbmeta.invalidate_on_error" to "yes", + "ro.boot.vbmeta.avb_version" to "1.2", + "ro.boot.vbmeta.hash_alg" to "sha256", + "ro.boot.vbmeta.size" to "19968", + "ro.boot.vbmeta.device_state" to "locked", + "ro.boot.verifiedbootstate" to "green", + "ro.boot.flash.locked" to "1", + "ro.boot.veritymode" to "enforcing", + "ro.boot.warranty_bit" to "0", + "ro.warranty_bit" to "0", + "ro.debuggable" to "0", + "ro.force.debuggable" to "0", + "ro.secure" to "1", + "ro.adb.secure" to "1", + "ro.build.type" to "user", + "ro.build.tags" to "release-keys", + "ro.vendor.boot.warranty_bit" to "0", + "ro.vendor.warranty_bit" to "0", + "vendor.boot.vbmeta.device_state" to "locked", + "vendor.boot.verifiedbootstate" to "green", + "sys.oem_unlock_allowed" to "0", + "ro.secureboot.lockstate" to "locked", + "ro.boot.realmebootstate" to "green", + "ro.boot.realme.lockstate" to "1", + "ro.crypto.state" to "encrypted" + ) + + systemProps.forEach { (prop, value) -> + when { + prop.startsWith("ro.boot.vbmeta") && prop.endsWith("_on_error") -> + appendLine("check_missing_prop \"$prop\" \"$value\"") + prop.contains("device_state") || prop.contains("verifiedbootstate") -> + appendLine("check_missing_match_prop \"$prop\" \"$value\"") + else -> + appendLine("check_reset_prop \"$prop\" \"$value\"") + } + } + + appendLine() + appendLine("# Hide adb debugging traces") + appendLine("resetprop \"sys.usb.adb.disabled\" \" \"") + appendLine() + + appendLine("# Hide recovery boot mode") + appendLine("contains_reset_prop \"ro.bootmode\" \"recovery\" \"unknown\"") + appendLine("contains_reset_prop \"ro.boot.bootmode\" \"recovery\" \"unknown\"") + appendLine("contains_reset_prop \"vendor.boot.bootmode\" \"recovery\" \"unknown\"") + appendLine() + + appendLine("# Hide cloudphone detection") + appendLine("[ -n \"$(resetprop ro.kernel.qemu)\" ] && resetprop ro.kernel.qemu \"\"") + appendLine() + } + + // 清理残留脚本生成 + private fun StringBuilder.generateCleanupResidueSection() { + appendLine("# 清理工具残留文件") + appendLine("echo \"$(get_current_time): 开始清理工具残留\" >> \"${'$'}LOG_FILE\"") + appendLine() + + // 定义清理函数 + appendLine(""" + cleanup_path() { + local path="$1" + local desc="$2" + local current="$3" + local total="$4" + + if [ -n "${'$'}desc" ]; then + echo "$(get_current_time): [${'$'}current/${'$'}total] 清理: ${'$'}path (${'$'}desc)" >> "${'$'}LOG_FILE" + else + echo "$(get_current_time): [${'$'}current/${'$'}total] 清理: ${'$'}path" >> "${'$'}LOG_FILE" + fi + + if rm -rf "${'$'}path" 2>/dev/null; then + echo "$(get_current_time): ✓ 成功清理: ${'$'}path" >> "${'$'}LOG_FILE" + else + echo "$(get_current_time): ✗ 清理失败或不存在: ${'$'}path" >> "${'$'}LOG_FILE" + fi + } + """.trimIndent()) + + appendLine() + appendLine("# 开始清理各种工具残留") + appendLine("TOTAL=33") + appendLine() + + val cleanupPaths = listOf( + "/data/local/stryker/" to "Stryker残留", + "/data/system/AppRetention" to "AppRetention残留", + "/data/local/tmp/luckys" to "Luck Tool残留", + "/data/local/tmp/HyperCeiler" to "西米露残留", + "/data/local/tmp/simpleHook" to "simple Hook残留", + "/data/local/tmp/DisabledAllGoogleServices" to "谷歌省电模块残留", + "/data/local/MIO" to "解包软件", + "/data/DNA" to "解包软件", + "/data/local/tmp/cleaner_starter" to "质感清理残留", + "/data/local/tmp/byyang" to "", + "/data/local/tmp/mount_mask" to "", + "/data/local/tmp/mount_mark" to "", + "/data/local/tmp/scriptTMP" to "", + "/data/local/luckys" to "", + "/data/local/tmp/horae_control.log" to "", + "/data/gpu_freq_table.conf" to "", + "/storage/emulated/0/Download/advanced/" to "", + "/storage/emulated/0/Documents/advanced/" to "爱玩机", + "/storage/emulated/0/Android/naki/" to "旧版asoulopt", + "/data/swap_config.conf" to "scene附加模块2", + "/data/local/tmp/resetprop" to "", + "/dev/cpuset/AppOpt/" to "AppOpt模块", + "/storage/emulated/0/Android/Clash/" to "Clash for Magisk模块", + "/storage/emulated/0/Android/Yume-Yunyun/" to "网易云后台优化模块", + "/data/local/tmp/Surfing_update" to "Surfing模块缓存", + "/data/encore/custom_default_cpu_gov" to "encore模块", + "/data/encore/default_cpu_gov" to "encore模块", + "/data/local/tmp/yshell" to "", + "/data/local/tmp/encore_logo.png" to "", + "/storage/emulated/legacy/" to "", + "/storage/emulated/elgg/" to "", + "/data/system/junge/" to "", + "/data/local/tmp/mount_namespace" to "挂载命名空间残留" + ) + + cleanupPaths.forEachIndexed { index, (path, desc) -> + val current = index + 1 + appendLine("cleanup_path '$path' '$desc' $current \$TOTAL") + } + + appendLine() + appendLine("echo \"$(get_current_time): 工具残留清理完成\" >> \"${'$'}LOG_FILE\"") + appendLine() + } + + /** + * 生成post-fs-data.sh脚本内容 + */ + private fun generatePostFsDataScript(config: SuSFSManager.ModuleConfig): String { + return buildString { + appendLine("#!/system/bin/sh") + appendLine("# SuSFS Post-FS-Data Script") + appendLine("# 在文件系统挂载后但在系统完全启动前执行") + appendLine() + appendLine(generateLogSetup("susfs_post_fs_data.log")) + appendLine() + appendLine(generateBinaryCheck(config.targetPath)) + appendLine() + appendLine("echo \"$(get_current_time): Post-FS-Data脚本开始执行\" >> \"${'$'}LOG_FILE\"") + appendLine() + + // 设置uname和构建时间 - 只有在选择在post-fs-data中执行时才执行 + if (config.executeInPostFsData && (config.unameValue != DEFAULT_UNAME || config.buildTimeValue != DEFAULT_BUILD_TIME)) { + appendLine("# 设置uname和构建时间") + appendLine("\"${'$'}SUSFS_BIN\" set_uname '${config.unameValue}' '${config.buildTimeValue}'") + appendLine("echo \"$(get_current_time): 设置uname为: ${config.unameValue}, 构建时间为: ${config.buildTimeValue}\" >> \"${'$'}LOG_FILE\"") + appendLine() + } + + generateUmountZygoteIsoServiceSection(config.umountForZygoteIsoService) + + // 添加AVC日志欺骗设置 + generateAvcLogSpoofingSection(config.enableAvcLogSpoofing) + + appendLine("echo \"$(get_current_time): Post-FS-Data脚本执行完成\" >> \"${'$'}LOG_FILE\"") + } + } + + // 添加新的生成方法 + private fun StringBuilder.generateUmountZygoteIsoServiceSection(umountForZygoteIsoService: Boolean) { + appendLine("# 设置Zygote隔离服务卸载状态") + val umountValue = if (umountForZygoteIsoService) 1 else 0 + appendLine("\"${'$'}SUSFS_BIN\" umount_for_zygote_iso_service $umountValue") + appendLine("echo \"$(get_current_time): Zygote隔离服务卸载设置为: ${if (umountForZygoteIsoService) "启用" else "禁用"}\" >> \"${'$'}LOG_FILE\"") + appendLine() + } + + /** + * 生成post-mount.sh脚本内容 + */ + private fun generatePostMountScript(config: SuSFSManager.ModuleConfig): String { + return buildString { + appendLine("#!/system/bin/sh") + appendLine("# SuSFS Post-Mount Script") + appendLine("# 在所有分区挂载完成后执行") + appendLine() + appendLine(generateLogSetup("susfs_post_mount.log")) + appendLine() + appendLine("echo \"$(get_current_time): Post-Mount脚本开始执行\" >> \"${'$'}LOG_FILE\"") + appendLine() + appendLine(generateBinaryCheck(config.targetPath)) + appendLine() + + // 添加SUS挂载 + if (config.susMounts.isNotEmpty()) { + appendLine("# 添加SUS挂载") + config.susMounts.forEach { mount -> + appendLine("\"${'$'}SUSFS_BIN\" add_sus_mount '$mount'") + appendLine("echo \"$(get_current_time): 添加SUS挂载: $mount\" >> \"${'$'}LOG_FILE\"") + } + appendLine() + } + + // 添加尝试卸载 + if (config.tryUmounts.isNotEmpty()) { + appendLine("# 添加尝试卸载") + config.tryUmounts.forEach { umount -> + val parts = umount.split("|") + if (parts.size == 2) { + val path = parts[0] + val mode = parts[1] + appendLine("\"${'$'}SUSFS_BIN\" add_try_umount '$path' $mode") + appendLine("echo \"$(get_current_time): 添加尝试卸载: $path (模式: $mode)\" >> \"${'$'}LOG_FILE\"") + } + } + appendLine() + } + + appendLine("echo \"$(get_current_time): Post-Mount脚本执行完成\" >> \"${'$'}LOG_FILE\"") + } + } + + /** + * 生成boot-completed.sh脚本内容 + */ + @SuppressLint("SdCardPath") + private fun generateBootCompletedScript(config: SuSFSManager.ModuleConfig): String { + return buildString { + appendLine("#!/system/bin/sh") + appendLine("# SuSFS Boot-Completed Script") + appendLine("# 在系统完全启动后执行") + appendLine() + appendLine(generateLogSetup("susfs_boot_completed.log")) + appendLine() + appendLine("echo \"$(get_current_time): Boot-Completed脚本开始执行\" >> \"${'$'}LOG_FILE\"") + appendLine() + appendLine(generateBinaryCheck(config.targetPath)) + appendLine() + + // SUS挂载隐藏控制 + val hideValue = if (config.hideSusMountsForAllProcs) 1 else 0 + appendLine("# 设置SUS挂载隐藏控制") + appendLine("\"${'$'}SUSFS_BIN\" hide_sus_mnts_for_all_procs $hideValue") + appendLine("echo \"$(get_current_time): SUS挂载隐藏控制设置为: ${if (config.hideSusMountsForAllProcs) "对所有进程隐藏" else "仅对非KSU进程隐藏"}\" >> \"${'$'}LOG_FILE\"") + appendLine() + + // 路径设置和SUS路径设置 + if (config.susPaths.isNotEmpty() || config.susLoopPaths.isNotEmpty()) { + generatePathSettingSection(config.androidDataPath, config.sdcardPath) + appendLine() + + // 添加普通SUS路径 + if (config.susPaths.isNotEmpty()) { + generateSusPathsSection(config.susPaths) + } + + // 添加循环SUS路径 + if (config.susLoopPaths.isNotEmpty()) { + generateSusLoopPathsSection(config.susLoopPaths) + } + + if (config.susMaps.isNotEmpty()) { + generateSusMapsSection(config.susMaps) + } + } + + appendLine("echo \"$(get_current_time): Boot-Completed脚本执行完成\" >> \"${'$'}LOG_FILE\"") + } + } + + private fun StringBuilder.generateSusMapsSection(susMaps: Set) { + if (susMaps.isNotEmpty()) { + appendLine("# 添加SUS映射") + susMaps.forEach { map -> + appendLine("\"${'$'}SUSFS_BIN\" add_sus_map '$map'") + appendLine("echo \"$(get_current_time): 添加SUS映射: $map\" >> \"${'$'}LOG_FILE\"") + } + appendLine() + } + } + + @SuppressLint("SdCardPath") + private fun StringBuilder.generatePathSettingSection(androidDataPath: String, sdcardPath: String) { + appendLine("# 路径配置") + appendLine("# 设置Android Data路径") + appendLine("until [ -d \"/sdcard/Android\" ]; do sleep 1; done") + appendLine("sleep 60") + appendLine() + appendLine("\"${'$'}SUSFS_BIN\" set_android_data_root_path '$androidDataPath'") + appendLine("echo \"$(get_current_time): Android Data路径设置为: $androidDataPath\" >> \"${'$'}LOG_FILE\"") + appendLine() + appendLine("# 设置SD卡路径") + appendLine("\"${'$'}SUSFS_BIN\" set_sdcard_root_path '$sdcardPath'") + appendLine("echo \"$(get_current_time): SD卡路径设置为: $sdcardPath\" >> \"${'$'}LOG_FILE\"") + appendLine() + } + + /** + * 生成module.prop文件内容 + */ + fun generateModuleProp(moduleId: String): String { + val moduleVersion = "v4.0.0" + val moduleVersionCode = "40000" + + return """ + id=$moduleId + name=SuSFS Manager + version=$moduleVersion + versionCode=$moduleVersionCode + author=ShirkNeko + description=SuSFS Manager Auto Configuration Module (自动生成请不要手动卸载或删除该模块! / Automatically generated Please do not manually uninstall or delete the module!) + updateJson= + """.trimIndent() + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt index 1c820df2..73ceaf71 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt @@ -51,6 +51,15 @@ class SuperUserViewModel : ViewModel() { val appDetail = appList.find { it.packageName == packageName } return appDetail?.packageInfo?.applicationInfo?.loadIcon(context.packageManager) } + + @JvmStatic + fun getAppsSafely(): List { + return try { + synchronized(appsLock) { apps } + } catch (e: Exception) { + emptyList() + } + } } 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 37d0b338..32c44590 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -357,4 +357,251 @@ 将安装以下内核:%1$s 将安装以下文件:%1$s + + SuSFS 配置 + 配置SuSFS模块设置 + 配置说明 + 此功能允许您自定义 SuSFS 的 uname 值和构建时间伪装。输入您想要设置的值,点击应用即可生效 + Uname 值 + 请输入自定义 uname 值 + 构建时间伪装 + 请输入构建时间伪装值 + 当前值: %s + 当前构建时间: %s + 重置为默认值 + 应用 + + 确认重置 + + 无法找到 ksu_susfs 文件 + SuSFS 命令执行失败 + 执行 SuSFS 命令时出错: %s + SuSFS 内核名称和构建时间设置成功: %s, %s + + SuSFS 配置 + + 开机自启动 + 重启时自动应用所有非默认配置 + 需要添加配置后才能启用 + 启用开机自启动失败 + 禁用开机自启动失败 + 开机自启动配置错误: %s + 没有可用的配置进行开机自启动 + + 基本设置 + SuS 路径 + SuS 挂载 + 尝试卸载 + 路径设置 + 启用功能状态 + + 添加 SuS 路径 + 添加 SuS 挂载 + 添加尝试卸载 + SuS 路径添加成功 + 路径未找到错误 + 路径 + 挂载路径 + 例如: /system/addon.d + 暂无 SuS 路径配置 + 暂无 SuS 挂载配置 + 暂无尝试卸载配置 + + 卸载模式 + 普通卸载(0) + 分离卸载(1) + 普通 + 分离 + 模式: %1$s (%2$s) + 尝试 umount 路径添加成功: %s + 尝试 umount 路径保存成功: %s + + + 重置 SuS 路径 + 这将清除所有 SuS 路径配置,确定要继续吗? + 重置 SuS 挂载 + 这将清除所有 SuS 挂载配置,确定要继续吗? + 重置尝试卸载 + 这将清除所有尝试卸载配置,确定要继续吗? + 重置路径设置 + + Android Data 路径 + SDCard 路径 + 设置 Android Data 路径 + 设置 SDCard 路径 + + 显示当前 SuSFS 启用的功能状态 + 未找到功能状态信息 + 已启用 + 已禁用 + + SuS 路径支持 + SuS 挂载支持 + 尝试卸载支持 + 欺骗 uname 支持 + 欺骗 Cmdline/Bootconfig + 开放重定向支持 + 日志记录支持 + 自动默认挂载 + 自动绑定挂载 + 自动尝试卸载绑定挂载 + 隐藏 KSU SuSFS 符号 + SuS Kstat 支持 + SuS SU 模式切换功能 + + 可配置的 SuSFS 功能 + SuSFS 启用日志 + 启用或者关闭 SuSFS 的日志 + SuSFS 日志配置 + 启用 SuSFS 日志 + 关闭 SuSFS 日志 + 执行位置 + 当前执行位置:%s + Service + Post-FS-Data + 在系统服务启动后执行 + 在文件系统挂载后但系统完全启动前执行,可能会导致循环重启 + 槽位信息 + 查看当前启动槽位信息并复制数值 + 当前活动槽位:%s + Uname:%s + 构建时间:%s + 当前 + 使用 Uname + 使用构建时间 + 无法获取槽位信息 + + SuSFS 自启动模块已启用,模块路径:%s + SuSFS 自启动模块已禁用 + + Kstat 配置 + Kstat 静态配置已添加:%1$s + 已移除 Kstat 配置:%1$s + Kstat 路径已添加:%1$s + 已移除 Kstat 路径:%1$s + Kstat 已更新:%1$s + Kstat 完整克隆已更新:%1$s + 添加 Kstat 静态配置 + 文件/目录路径 + 提示:可以使用 "default" 来使用原始值 + 添加 Kstat 路径 + 添加 + 重置 Kstat 配置 + 确定要清除所有 Kstat 配置吗?此操作不可撤销 + Kstat 配置说明 + • add_sus_kstat_statically: 静态配置文件/目录的 stat 信息 + • add_sus_kstat: 在绑定挂载前添加路径,存储原始 stat 信息 + • update_sus_kstat: 更新目标 ino,保持 size 和 blocks 不变 + • update_sus_kstat_full_clone: 仅更新 ino,其他保持原始值 + 静态 Kstat 配置 + Kstat 路径管理 + 暂无 Kstat 配置,点击下方按钮添加配置 + + SuS 挂载隐藏控制 + 控制 SuS 挂载对进程的隐藏行为 + 对所有进程隐藏 SuS 挂载 + 启用后,SuS 挂载将对所有进程隐藏,包括 KSU 进程 + 禁用后,SuS 挂载仅对非 KSU 进程隐藏,KSU 进程可以看到挂载 + 已启用对所有进程隐藏 SuS 挂载 + 已禁用对所有进程隐藏 SuS 挂载 + 建议在屏幕解锁后或在 service.sh 或 boot-completed.sh 阶段设置为禁用,这可以修复一些依赖 KSU 进程挂载的 root 应用的问题 + 当前设置: %s + 对所有进程隐藏 + 仅对非 KSU 进程隐藏 + Android Data 路径已设置为: %s + SDCard 路径已设置为: %s + 路径设置可能未完全成功,但将继续添加 SuS 路径 + + 备份 + 创建所有 SuSFS 配置的备份。备份文件将包含所有设置、路径和配置信息。 + 创建备份 + 备份创建成功:%s + 备份创建失败:%s + 备份文件未找到 + 无效的备份文件格式 + 备份版本不匹配,但将尝试还原 + 还原 + 从备份文件还原 SuSFS 配置。这将覆盖所有当前设置。 + 选择备份文件 + 配置还原成功,备份创建于 %s,来自设备:%s + 还原失败:%s + 确认还原 + 这将覆盖所有当前的 SuSFS 配置。您确定要继续吗? + 还原 + 备份日期:%s + 设备:%s + 版本:%s + 上锁状态 + 覆盖引导锁状态属性于 late_start 服务模式 + 清理工具残留 + 清理各种模块以及工具的残留文件和目录(可能会误删导致丢失以及无法启动,谨慎使用) + 编辑 SuS 路径 + 编辑 SuS 挂载 + 编辑尝试卸载 + 编辑 Kstat 静态配置 + 编辑 Kstat 路径 + 保存 + 编辑 + 删除 + 更新 + Kstat 配置更新 + Kstat 路径更新 + Susfs 完整克隆更新 + 卸载 Zygote 隔离服务 + 启用此选项将在系统启动时卸载 Zygote 隔离服务挂载点 + Zygote 隔离服务卸载已启用 + Zygote 隔离服务卸载已禁用 + 应用路径 + 其他路径 + 其他 + 应用 + 添加应用路径 + SuSFS 库版本不匹配,内核:%1$s vs 管理器:%2$s,建议更新内核或管理器 + 警告 + 搜索应用 + %1$d 个已选应用 + %1$d 个已添加应用 + 所有应用均已添加 + 未找到应用 + + SuS 循环路径 + 添加 SuS 循环路径 + 编辑 SuS 循环路径 + SuS 循环路径添加成功: %1$s + SuS 循环路径已移除: %1$s + SuS 循环路径已更新: %1$s -> %2$s + 未配置 SuS 循环路径 + 重置循环路径 + 确定要清空所有 SuS 循环路径吗?此操作无法撤销。 + 循环路径 + /data/example/path + 注意:只有不在 /storage/ 和 /sdcard/ 内的路径才能通过循环路径添加。 + 错误:循环路径不能位于 /storage/ 或 /sdcard/ 目录内 + 循环路径 + 添加循环路径 + + 循环路径配置 + 循环路径会在每次非 root 用户应用或隔离服务启动时重新标记为 SUS_PATH。这有助于解决添加的路径可能因 inode 状态重置或内核中 inode 重新创建而失效的问题 + AVC 日志欺骗 + AVC 日志欺骗已启用 + AVC 日志欺骗已禁用 + 禁用: 禁用在内核 AVC 日志中欺骗 \'su\' 的 sus tcontext。\n启用: 启用在内核 AVC 日志中将 \'su\' 的 sus tcontext 欺骗为 \'kernel\' + 重要提示:\n- 内核中默认设置为 \'0\'\n- 启用此功能有时会使开发人员在调试权限或 SELinux 问题时难以识别原因,因此建议用户在调试时禁用此功能。 + + SUS映射 + 库文件路径 + /data/adb/modules/my_module/zygisk/arm64-v8a.so + 添加SUS映射 + 编辑SUS映射 + SUS映射添加成功: %1$s + SUS映射已移除: %1$s + SUS映射已更新: %1$s -> %2$s + 未配置SUS映射 + 重置SUS映射 + 这将移除所有已配置的SUS映射。此操作无法撤销。 + 内存映射隐藏 + 隐藏/proc/self/中各种映射中的mmap真实文件 + 从 /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap] 中隐藏内存映射的真实文件路径。请注意:此功能不支持隐藏匿名内存映射,也无法隐藏由注入库本身产生的内联钩子或 PLT 钩子。 + 重要提示:对于具备完善注入检测机制的应用,此功能可能无法有效绕过检测。 + 首先通过 ps -enf 查找目标应用的 PID 和 UID,然后检查 /proc/<pid>/maps 中的相关路径,并与 /proc/1/mountinfo 中的设备号进行比对以确保一致性。只有当设备号一致时,隐藏映射才能正常工作。 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 96c150bc..0d3457bd 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -365,4 +365,250 @@ The following kernels will be installed: %1$s The following files will be installed: %1$s + + SuSFS Configuration + Configure SuSFS module settings + Configuration Description + This feature allows you to customize the SuSFS uname value and build time spoofing. Enter the values you want to set and click Apply to take effect + Uname Value + Please enter custom uname value + Build Time Spoofing + Please enter build time spoofing value + Current value: %s + Current build time: %s + Reset to Default + Apply + + Confirm Reset + + Cannot find ksu_susfs file + SuSFS command execution failed + Error executing SuSFS command: %s + SuSFS uname and build time set successfully: %s, %s + + SuSFS Configuration + + Auto Start + Automatically apply all non-default configurations on reboot + Configuration needs to be added to enable + Failed to enable auto start + Failed to disable auto start + Auto start configuration error: %s + No available configuration for auto start + + Basic Settings + SUS Paths + SUS Mounts + Try Umount + Path Settings + Enabled Features Status + + Add SUS Path + Add SUS Mount + Add Try Umount + SUS path added successfully + Path not found error + Path + Mount Path + e.g.: /system/addon.d + No SUS paths configured + No SUS mounts configured + No try umount configured + + Umount Mode + Normal Umount (0) + Detach Umount (1) + Normal + Detach + Mode: %1$s (%2$s) + Try to umount path added successfully: %s + Attempted umount path save succeeded: %s + + Reset SUS Paths + This will clear all SUS path configurations. Are you sure you want to continue? + Reset SUS Mounts + This will clear all SUS mount configurations. Are you sure you want to continue? + Reset Try Umount + This will clear all try umount configurations. Are you sure you want to continue? + Reset Path Settings + + Android Data Path + SD Card Path + Set Android Data Path + Set SD Card Path + + Display current SuSFS enabled features status + No feature status information found + Enabled + Disabled + + SUS Path Support + SUS Mount Support + Try Umount Support + Spoof uname Support + Spoof Cmdline/Bootconfig + Open Redirect Support + Logging Support + Auto Default Mount + Auto Bind Mount + Auto Try Umount Bind Mount + Hide KSU SUSFS Symbols + SUS Kstat Support + SUS SU mode switching function + + Configurable SuSFS Features + SuSFS Enable Log + Enable or disable logging for SuSFS + SuSFS Logging Configuration + Enabling SuSFS Logging + Turn off SuSFS logging + Execution Location + Current execution location: %s + Service + Post-FS-Data + Execute after system services start + Execute after file system is mounted but before system is fully booted,May cause a boot loop + Slot Information + View current boot slot information and copy values + Current Active Slot: %s + Uname: %s + Build Time: %s + Current + Use Uname + Use Build Time + Unable to retrieve slot information + + SuSFS auto-start module enabled, module path: %s + SuSFS auto-start module disabled + + Kstat Configuration + Kstat static configuration added: %1$s + Kstat configuration removed: %1$s + Kstat path added: %1$s + Kstat path removed: %1$s + Kstat updated: %1$s + Kstat full clone updated: %1$s + Add Kstat Static Configuration + File/Directory Path + Hint: You can use "default" to use the original value + Add Kstat Path + Add + Reset Kstat Configuration + Are you sure you want to clear all Kstat configurations? This action cannot be undone + Kstat Configuration Description + • add_sus_kstat_statically: Static stat info of files/directories + • add_sus_kstat: Add path before bind mount, storing original stat info + • update_sus_kstat: Update target ino, keep size and blocks unchanged + • update_sus_kstat_full_clone: Update ino only, keep other original values + Static Kstat Configuration + Kstat Path Management + No Kstat configuration yet, click the button below to add + + SUS Mount Hiding Control + Control the hiding behavior of SUS mounts for processes + Hide SUS mounts for all processes + When enabled, SUS mounts will be hidden from all processes, including KSU processes + When disabled, SUS mounts will only be hidden from non-KSU processes, KSU processes can see the mounts + Enabled hiding SUS mounts for all processes + Disabled hiding SUS mounts for all processes + It is recommended to set to disabled after screen is unlocked, or during service.sh or boot-completed.sh stage, as this should fix the issue on some rooted apps that rely on mounts mounted by KSU process + Current setting: %s + Hide for all processes + Hide only for non-KSU processes + Android Data path has been set to: %s + SD card path has been set to: %s + Path setup may not be fully successful, but SUS paths will continue to be added + + Backup + Create a backup of all SuSFS configurations. The backup file will include all settings, paths, and configurations + Create Backup + Backup created successfully: %s + Backup creation failed: %s + Backup file not found + Invalid backup file format + Backup version mismatch, but will attempt to restore + Restore + Restore SuSFS configurations from a backup file. This will overwrite all current settings + Select Backup File + Configuration restored successfully from backup created on %s from device: %s + Restore failed: %s + Confirm Restore + This will overwrite all current SuSFS configurations. Are you sure you want to continue? + Restore + Backup Date: %s + Device: %s + Version: %s + Lock state + Overwrite bootloader locking status attribute in late_start service mode + Cleanup Residue + Clean up the residual files and directories of various modules and tools (May be deleted by mistake, resulting in loss and failure to start, use with caution) + Edit SUS Path + Edit SUS Mount + Edit Try Umount + Edit Kstat Static Configuration + Edit Kstat Path + Save + Edit + Delete + Update + Kstat config update + Kstat path update + Susfs update full clone + Unmount Zygote Isolation Service + Enable this option to unmount Zygote isolation service mount points at system startup + Zygote isolation service unmount enabled + Zygote isolation service unmount disabled + Application Path + Other paths + Other + App + Add App Path + SuSFS library version mismatch, kernel: %1$s vs manager: %2$s, It is recommended to update the kernel or manager + Warning + Search Apps + %1$d apps selected + %1$d apps already added + All apps have been added + No apps found + + SUS Loop Paths + Add SUS Loop Path + Edit SUS Loop Path + SUS loop path added successfully: %1$s + SUS loop path removed: %1$s + SUS loop path updated: %1$s -> %2$s + No SUS loop paths configured + Reset Loop Paths + Are you sure you want to clear all SUS loop paths? This action cannot be undone + Loop Path + /data/example/path + Note: Only paths NOT inside /storage/ and /sdcard/ can be added via loop paths + Error: Loop paths cannot be inside /storage/ or /sdcard/ directories + Loop Paths + Add Loop Path + + Loop Path Configuration + Loop paths are re-flagged as SUS_PATH on each non-root user app or isolated service startup. This helps address issues where added paths may have their inode status reset or inode re-created in the kernel + AVC Log Spoofing + AVC log spoofing has been enabled + AVC log spoofing has been disabled + Disabled: Disable spoofing the sus tcontext of \'su\' shown in avc log in kernel\nEnabled: Enable spoofing the sus tcontext of \'su\' with \'kernel\' shown in avc log in kernel + Important Note:\n- It is set to \'0\' by default in kernel\n- Enabling this will sometimes make developers hard to identify the cause when they are debugging with some permission or SELinux issue, so users are advised to disable this when doing + + SUS Maps + Library Path + /data/adb/modules/my_module/zygisk/arm64-v8a.so + Add SUS Map + Edit SUS Map + SUS map added successfully: %1$s + SUS map removed: %1$s + SUS map updated: %1$s -> %2$s + No SUS maps configured + Reset SUS Maps + This will remove all configured SUS maps. This action cannot be undone. + Memory Map Hiding + Hide the mmapped real file from various maps in /proc/self/ + 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.