From 8399f14fad4ff9b677c149874f8458c16323dd7c Mon Sep 17 00:00:00 2001 From: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com> Date: Sat, 28 Jun 2025 23:19:18 +0800 Subject: [PATCH] manager: Add SuSFS configuration backup and restore feature - Optimize susfs self-boot scripts - Solve some invalid issues where startup duration does not match or is too fast. Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com> --- .../com/sukisu/ultra/ui/screen/SuSFSConfig.kt | 308 +++++++++++++++++- .../com/sukisu/ultra/ui/util/SuSFSManager.kt | 209 +++++++++++- .../ultra/ui/util/SuSFSModuleScripts.kt | 78 +++-- .../app/src/main/res/values-ja/strings.xml | 3 - .../app/src/main/res/values-ru/strings.xml | 3 - .../app/src/main/res/values-tr/strings.xml | 1 - .../app/src/main/res/values-vi/strings.xml | 3 - .../src/main/res/values-zh-rCN/strings.xml | 23 +- .../src/main/res/values-zh-rHK/strings.xml | 3 - .../src/main/res/values-zh-rTW/strings.xml | 3 - manager/app/src/main/res/values/strings.xml | 23 +- 11 files changed, 589 insertions(+), 68 deletions(-) diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt index 229caf6d..75738aa9 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt @@ -1,6 +1,8 @@ package com.sukisu.ultra.ui.screen import android.annotation.SuppressLint +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -80,6 +82,8 @@ import com.sukisu.ultra.ui.theme.CardConfig import com.sukisu.ultra.ui.util.SuSFSManager import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion_1_5_8 import kotlinx.coroutines.launch +import java.text.SimpleDateFormat +import java.util.* /** * 标签页枚举类 @@ -123,8 +127,6 @@ fun SuSFSConfigScreen( var isLoading by remember { mutableStateOf(false) } var showConfirmReset by remember { mutableStateOf(false) } var autoStartEnabled by remember { mutableStateOf(false) } - var lastAppliedValue by remember { mutableStateOf("") } - var lastAppliedBuildTime by remember { mutableStateOf("") } var executeInPostFsData by remember { mutableStateOf(false) } // 槽位信息相关状态 @@ -165,6 +167,13 @@ fun SuSFSConfigScreen( 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) } + val allTabs = SuSFSTab.getAllTabs(isSusVersion_1_5_8()) // 实时判断是否可以启用开机自启动 @@ -174,6 +183,65 @@ fun SuSFSConfigScreen( } } + // 文件选择器 + val backupFileLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.CreateDocument("application/json") + ) { uri -> + uri?.let { fileUri -> + val fileName = SuSFSManager.getRecommendedBackupPath(context) + coroutineScope.launch { + isLoading = true + val success = SuSFSManager.createBackup(context, fileName) + if (success) { + // 复制到用户选择的位置 + try { + context.contentResolver.openOutputStream(fileUri)?.use { outputStream -> + java.io.File(fileName).inputStream().use { inputStream -> + inputStream.copyTo(outputStream) + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + isLoading = false + showBackupDialog = false + } + } + } + + val restoreFileLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument() + ) { uri -> + uri?.let { fileUri -> + coroutineScope.launch { + try { + // 复制到临时文件 + val tempFile = java.io.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 + } else { + // 显示错误消息 + } + tempFile.deleteOnExit() + } catch (e: Exception) { + e.printStackTrace() + } + showRestoreDialog = false + } + } + } + // 加载启用功能状态 fun loadEnabledFeatures() { coroutineScope.launch { @@ -198,8 +266,6 @@ fun SuSFSConfigScreen( unameValue = SuSFSManager.getUnameValue(context) buildTimeValue = SuSFSManager.getBuildTimeValue(context) autoStartEnabled = SuSFSManager.isAutoStartEnabled(context) - lastAppliedValue = SuSFSManager.getLastAppliedValue(context) - lastAppliedBuildTime = SuSFSManager.getLastAppliedBuildTime(context) executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context) susPaths = SuSFSManager.getSusPaths(context) susMounts = SuSFSManager.getSusMounts(context) @@ -228,6 +294,183 @@ fun SuSFSConfigScreen( } } + // 备份对话框 + if (showBackupDialog) { + AlertDialog( + onDismissRequest = { showBackupDialog = false }, + title = { + Text( + text = stringResource(R.string.susfs_backup_title), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + }, + text = { + Text(stringResource(R.string.susfs_backup_description)) + }, + confirmButton = { + Button( + onClick = { + val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()) + val timestamp = dateFormat.format(Date()) + backupFileLauncher.launch("SuSFS_Config_$timestamp.susfs_backup") + }, + enabled = !isLoading, + shape = RoundedCornerShape(8.dp) + ) { + Text(stringResource(R.string.susfs_backup_create)) + } + }, + dismissButton = { + TextButton( + onClick = { showBackupDialog = false }, + shape = RoundedCornerShape(8.dp) + ) { + Text(stringResource(R.string.cancel)) + } + }, + shape = RoundedCornerShape(12.dp) + ) + } + + // 还原对话框 + if (showRestoreDialog) { + AlertDialog( + onDismissRequest = { showRestoreDialog = false }, + title = { + Text( + text = stringResource(R.string.susfs_restore_title), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + }, + text = { + Text(stringResource(R.string.susfs_restore_description)) + }, + confirmButton = { + Button( + onClick = { + restoreFileLauncher.launch(arrayOf("application/json", "*/*")) + }, + enabled = !isLoading, + shape = RoundedCornerShape(8.dp) + ) { + Text(stringResource(R.string.susfs_restore_select_file)) + } + }, + dismissButton = { + TextButton( + onClick = { showRestoreDialog = false }, + shape = RoundedCornerShape(8.dp) + ) { + Text(stringResource(R.string.cancel)) + } + }, + shape = RoundedCornerShape(12.dp) + ) + } + + // 还原确认对话框 + if (showRestoreConfirmDialog && backupInfo != null) { + AlertDialog( + onDismissRequest = { + showRestoreConfirmDialog = false + selectedBackupFile = null + backupInfo = null + }, + title = { + Text( + text = stringResource(R.string.susfs_restore_confirm_title), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text(stringResource(R.string.susfs_restore_confirm_description)) + + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + shape = RoundedCornerShape(8.dp) + ) { + 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))), + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = stringResource(R.string.susfs_backup_info_device, backupInfo!!.deviceInfo), + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = stringResource(R.string.susfs_backup_info_version, backupInfo!!.version), + style = MaterialTheme.typography.bodyMedium + ) + } + } + } + }, + confirmButton = { + Button( + onClick = { + selectedBackupFile?.let { filePath -> + coroutineScope.launch { + isLoading = true + val success = SuSFSManager.restoreFromBackup(context, filePath) + if (success) { + // 重新加载所有配置 + unameValue = SuSFSManager.getUnameValue(context) + buildTimeValue = SuSFSManager.getBuildTimeValue(context) + autoStartEnabled = SuSFSManager.isAutoStartEnabled(context) + executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context) + susPaths = SuSFSManager.getSusPaths(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) + } + isLoading = false + showRestoreConfirmDialog = false + selectedBackupFile = null + backupInfo = null + } + } + }, + enabled = !isLoading, + shape = RoundedCornerShape(8.dp) + ) { + Text(stringResource(R.string.susfs_restore_confirm)) + } + }, + dismissButton = { + TextButton( + onClick = { + showRestoreConfirmDialog = false + selectedBackupFile = null + backupInfo = null + }, + shape = RoundedCornerShape(8.dp) + ) { + Text(stringResource(R.string.cancel)) + } + }, + shape = RoundedCornerShape(12.dp) + ) + } + // 槽位信息对话框 SlotInfoDialog( showDialog = showSlotInfoDialog, @@ -366,8 +609,6 @@ fun SuSFSConfigScreen( if (SuSFSManager.resetToDefault(context)) { unameValue = "default" buildTimeValue = "default" - lastAppliedValue = "default" - lastAppliedBuildTime = "default" autoStartEnabled = false } isLoading = false @@ -530,8 +771,6 @@ fun SuSFSConfigScreen( val finalBuildTimeValue = buildTimeValue.trim().ifBlank { "default" } val success = SuSFSManager.setUname(context, finalUnameValue, finalBuildTimeValue) if (success) { - lastAppliedValue = finalUnameValue - lastAppliedBuildTime = finalBuildTimeValue SuSFSManager.saveExecuteInPostFsData(context, executeInPostFsData) } isLoading = false @@ -780,7 +1019,9 @@ fun SuSFSConfigScreen( } }, onShowSlotInfo = { showSlotInfoDialog = true }, - context = context + context = context, + onShowBackupDialog = { showBackupDialog = true }, + onShowRestoreDialog = { showRestoreDialog = true } ) } SuSFSTab.SUS_PATHS -> { @@ -939,7 +1180,9 @@ private fun BasicSettingsContent( isLoading: Boolean, onAutoStartToggle: (Boolean) -> Unit, onShowSlotInfo: () -> Unit, - context: android.content.Context + context: android.content.Context, + onShowBackupDialog: () -> Unit, + onShowRestoreDialog: () -> Unit ) { var scriptLocationExpanded by remember { mutableStateOf(false) } @@ -1204,6 +1447,51 @@ private fun BasicSettingsContent( } } } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + // 备份按钮 + OutlinedButton( + onClick = onShowBackupDialog, + enabled = !isLoading, + shape = RoundedCornerShape(8.dp), + modifier = Modifier + .weight(1f) + .height(40.dp) + ) { + Icon( + imageVector = Icons.Default.Backup, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + stringResource(R.string.susfs_backup_title), + fontWeight = FontWeight.Medium + ) + } + // 还原按钮 + OutlinedButton( + onClick = onShowRestoreDialog, + enabled = !isLoading, + shape = RoundedCornerShape(8.dp), + modifier = Modifier + .weight(1f) + .height(40.dp) + ) { + Icon( + imageVector = Icons.Default.Restore, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + stringResource(R.string.susfs_restore_title), + fontWeight = FontWeight.Medium + ) + } + } } } diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt index 8236505c..ef00e2af 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt @@ -16,6 +16,9 @@ import java.io.File import java.io.FileOutputStream import java.io.IOException import androidx.core.content.edit +import org.json.JSONObject +import java.text.SimpleDateFormat +import java.util.* /** * SuSFS 配置管理器 @@ -44,6 +47,7 @@ object SuSFSManager { private const val MODULE_ID = "susfs_manager" private const val MODULE_PATH = "/data/adb/modules/$MODULE_ID" private const val MIN_VERSION_FOR_HIDE_MOUNT = "1.5.8" + private const val BACKUP_FILE_EXTENSION = ".susfs_backup" data class SlotInfo(val slotName: String, val uname: String, val buildTime: String) data class CommandResult(val isSuccess: Boolean, val output: String, val errorOutput: String = "") @@ -54,6 +58,60 @@ object SuSFSManager { val canConfigure: Boolean = false ) + /** + * 备份数据类 + */ + 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 org.json.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 + } + } + } + } + /** * 模块配置数据类 */ @@ -180,9 +238,6 @@ object SuSFSManager { fun getBuildTimeValue(context: Context): String = getPrefs(context).getString(KEY_BUILD_TIME_VALUE, DEFAULT_BUILD_TIME) ?: DEFAULT_BUILD_TIME - fun getLastAppliedValue(context: Context): String = getUnameValue(context) - fun getLastAppliedBuildTime(context: Context): String = getBuildTimeValue(context) - fun setAutoStartEnabled(context: Context, enabled: Boolean) = getPrefs(context).edit { putBoolean(KEY_AUTO_START_ENABLED, enabled) } @@ -261,6 +316,154 @@ object SuSFSManager { fun getSdcardPath(context: Context): String = getPrefs(context).getString(KEY_SDCARD_PATH, "/sdcard") ?: "/sdcard" + // 获取所有配置的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_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) + ) + } + + //生成备份文件名 + 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 { + "${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL} (${android.os.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 getRecommendedBackupPath(context: Context): String { + val documentsDir = File(context.getExternalFilesDir(null), "SuSFS_Backups") + if (!documentsDir.exists()) { + documentsDir.mkdirs() + } + return File(documentsDir, generateBackupFileName()).absolutePath + } + // 槽位信息获取 suspend fun getCurrentSlotInfo(): List = withContext(Dispatchers.IO) { try { diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSModuleScripts.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSModuleScripts.kt index bccf377b..608cd3c0 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSModuleScripts.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSModuleScripts.kt @@ -53,6 +53,7 @@ object ScriptGenerator { /** * 生成service.sh脚本内容 */ + @SuppressLint("SdCardPath") private fun generateServiceScript(config: SuSFSManager.ModuleConfig): String { return buildString { appendLine("#!/system/bin/sh") @@ -67,6 +68,9 @@ object ScriptGenerator { if (shouldConfigureInService(config)) { // 添加SUS路径 (仅在不支持隐藏挂载时) if (!config.support158 && config.susPaths.isNotEmpty()) { + appendLine() + appendLine("until [ -d \"/sdcard/Android\" ]; do sleep 1; done") + appendLine("sleep 45") generateSusPathsSection(config.susPaths) } @@ -162,45 +166,48 @@ object ScriptGenerator { appendLine("# 隐藏BL 来自 Shamiko 脚本") appendLine( """ - check_reset_prop() { - local NAME=$1 - local EXPECTED=$2 - local VALUE=$(resetprop ${'$'}NAME) - [ -z ${'$'}VALUE ] || [ ${'$'}VALUE = ${'$'}EXPECTED ] || resetprop ${'$'}NAME ${'$'}EXPECTED - } - - check_missing_prop() { - local NAME=$1 - local EXPECTED=$2 - local VALUE=$(resetprop ${'$'}NAME) - [ -z ${'$'}VALUE ] && resetprop ${'$'}NAME ${'$'}EXPECTED - } - - check_missing_match_prop() { - local NAME=$1 - local EXPECTED=$2 - local VALUE=$(resetprop ${'$'}NAME) - [ -z ${'$'}VALUE ] || [ ${'$'}VALUE = ${'$'}EXPECTED ] || resetprop ${'$'}NAME ${'$'}EXPECTED - [ -z ${'$'}VALUE ] && resetprop ${'$'}NAME ${'$'}EXPECTED - } - - contains_reset_prop() { - local NAME=$1 - local CONTAINS=$2 - local NEWVAL=$3 - [[ "$(resetprop ${'$'}NAME)" = *"${'$'}CONTAINS"* ]] && resetprop ${'$'}NAME ${'$'}NEWVAL - } - """.trimIndent()) + 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 + [[ "$("${'$'}RESETPROP_BIN" ${'$'}NAME)" = *"${'$'}CONTAINS"* ]] && "${'$'}RESETPROP_BIN" ${'$'}NAME ${'$'}NEWVAL + } + """.trimIndent()) appendLine() - - appendLine("resetprop -w sys.boot_completed 0") + 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", @@ -327,6 +334,7 @@ object ScriptGenerator { /** * 生成boot-completed.sh脚本内容 */ + @SuppressLint("SdCardPath") private fun generateBootCompletedScript(config: SuSFSManager.ModuleConfig): String { return buildString { appendLine("#!/system/bin/sh") @@ -352,6 +360,10 @@ object ScriptGenerator { // 路径设置和SUS路径设置 if (config.susPaths.isNotEmpty()) { generatePathSettingSection(config.androidDataPath, config.sdcardPath) + appendLine() + appendLine("until [ -d \"/sdcard/Android\" ]; do sleep 1; done") + appendLine("sleep 45") + appendLine() generateSusPathsSection(config.susPaths) } } @@ -378,8 +390,8 @@ object ScriptGenerator { * 生成module.prop文件内容 */ fun generateModuleProp(moduleId: String): String { - val moduleVersion = "v1.0.1" - val moduleVersionCode = "1001" + val moduleVersion = "v1.0.2" + val moduleVersionCode = "1002" return """ id=$moduleId diff --git a/manager/app/src/main/res/values-ja/strings.xml b/manager/app/src/main/res/values-ja/strings.xml index 8a1bdf9f..6025ca3c 100644 --- a/manager/app/src/main/res/values-ja/strings.xml +++ b/manager/app/src/main/res/values-ja/strings.xml @@ -368,7 +368,6 @@ 適用 リセットを確認 - リセットを確認 ksu_susfs ファイルが見つかりません SuSFS コマンドの実行に失敗しました @@ -490,11 +489,9 @@ ファイルまたはディレクトリのパス ヒント: オリジナルの値を使用するには「default」を使用します Kstat のパスを追加 - このコマンドはパスがバインドマウントまたは、オーバーレイを開始する前に追加し、元の状態情報をカーネルメモリに保存するために使用されます。 追加 Kstat の構成をリセット すべての Kstat の構成を消去しますか?この操作は元に戻せません。 - リセットを確認 Kstat の構成の説明 • add_sus_kstat_statically: ファイル、ディレクトリの静的な状態情報 • add_sus_kstat: バインドマウント前にパスを追加して元の状態情報を保存します diff --git a/manager/app/src/main/res/values-ru/strings.xml b/manager/app/src/main/res/values-ru/strings.xml index e080e424..2efa743f 100644 --- a/manager/app/src/main/res/values-ru/strings.xml +++ b/manager/app/src/main/res/values-ru/strings.xml @@ -369,7 +369,6 @@ Применить Подтвердить сброс - Подтвердить сброс Не удалось найти файл ksu_susfs Выполнение команды SuSFS не удалось @@ -476,11 +475,9 @@ Путь к файлу/папке Подсказка: Вы можете использовать «по умолчанию» для использования оригинального значения Добавить путь Kstat - Эта команда используется для добавления перед тем как путь монтируется в bind-mounted или overlaid, сохраняя исходную информацию о состоянии в памяти ядра. Добавить Сбросить конфигурацию Kstat Вы уверены, что хотите очистить все конфигурации Kstat? Это действие нельзя отменить. - Подтвердить сброс Описание конфигурации Kstat • add_sus_kstat_staticall: Статическая статистика информации о файлах/директориях • add_sus_kstat: Добавить путь перед привязкой, сохраняя исходную статистику diff --git a/manager/app/src/main/res/values-tr/strings.xml b/manager/app/src/main/res/values-tr/strings.xml index 84ab78bf..1c5db8e1 100644 --- a/manager/app/src/main/res/values-tr/strings.xml +++ b/manager/app/src/main/res/values-tr/strings.xml @@ -367,7 +367,6 @@ Uygula Sıfırlamayı Onayla - Sıfırlamayı Onayla ksu_susfs dosyası bulunamadı SuSFS komut çalıştırma başarısız diff --git a/manager/app/src/main/res/values-vi/strings.xml b/manager/app/src/main/res/values-vi/strings.xml index d44c5319..9ea0de9c 100644 --- a/manager/app/src/main/res/values-vi/strings.xml +++ b/manager/app/src/main/res/values-vi/strings.xml @@ -367,7 +367,6 @@ Áp dụng Xác nhận khôi phục - Xác nhận khôi phục Không tìm thấy file ksu_susfs Thực hiện lệnh SuSFS thất bại @@ -489,11 +488,9 @@ Đường dẫn File/Folder Gợi ý: Bạn có thể sử dụng \"default\" để thiết lập giá trị ban đầu Thêm Đường dẫn Kstat - Lệnh này được sử dụng để thêm trước khi đường dẫn được mount hoặc ghi đè nhằm lưu trữ thông tin trạng thái ban đầu trong bộ nhớ hạt nhân Thêm Khôi phục Cấu hình Kstat Bạn có chắc chắn muốn xóa tất cả cấu hình Kstat không? Không thể hoàn tác hành động này - Xác nhận khôi phục Mô tả cấu hình Kstat • add_sus_kstat_statically: Thông tin thống kê cấu hình tĩnh của các File/Folder • add_sus_kstat: Thêm đường dẫn trước khi mount để lưu trữ thông tin trạng thái ban đầu 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 d02ec9af..011b42cb 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -367,7 +367,6 @@ 应用 确认重置 - 确认重置 无法找到 ksu_susfs 文件 SuSFS 命令执行失败 @@ -489,11 +488,9 @@ 文件/目录路径 提示:可以使用 “default” 来使用原始值 添加 Kstat 路径 - 此命令用于在路径被绑定挂载或覆盖之前添加,用于在内核内存中存储原始 stat 信息 添加 重置 Kstat 配置 确定要清除所有 Kstat 配置吗?此操作不可撤销 - 确认重置 Kstat 配置说明 • add_sus_kstat_statically: 静态配置文件/目录的 stat 信息 • add_sus_kstat: 在绑定挂载前添加路径,存储原始 stat 信息 @@ -520,4 +517,24 @@ Android Data路径已设置为: %s SD卡路径已设置为: %s 路径设置可能未完全成功,但将继续添加SUS路径 + + 备份 + 创建所有SuSFS配置的备份。备份文件将包含所有设置、路径和配置信息。 + 创建备份 + 备份创建成功:%s + 备份创建失败:%s + 备份文件未找到 + 无效的备份文件格式 + 备份版本不匹配,但将尝试还原 + 还原 + 从备份文件还原SuSFS配置。这将覆盖所有当前设置。 + 选择备份文件 + 配置还原成功,备份创建于 %s,来自设备:%s + 还原失败:%s + 确认还原 + 这将覆盖所有当前的SuSFS配置。您确定要继续吗? + 还原 + 备份日期:%s + 设备:%s + 版本:%s diff --git a/manager/app/src/main/res/values-zh-rHK/strings.xml b/manager/app/src/main/res/values-zh-rHK/strings.xml index 542675c4..6fae3dd2 100644 --- a/manager/app/src/main/res/values-zh-rHK/strings.xml +++ b/manager/app/src/main/res/values-zh-rHK/strings.xml @@ -366,7 +366,6 @@ 應用 確認重置 - 確認重置 無法找到 ksu_susfs 文件 SuSFS 命令執行失敗 @@ -488,11 +487,9 @@ 文件/目錄路徑 提示:可以使用 “default” 來使用原始值 添加 Kstat 路徑 - 此命令用於在路徑被綁定掛載或覆蓋之前添加,用於在核心內存中存儲原始 stat 信息 添加 重置 Kstat 配置 確定要清除所有 Kstat 配置嗎?此操作不可撤銷 - 確認重置 Kstat 配置說明 • add_sus_kstat_statically: 靜態配置文件/目錄嘅 stat 信息 • add_sus_kstat: 在綁定掛載前添加路徑,存儲原始 stat 信息 diff --git a/manager/app/src/main/res/values-zh-rTW/strings.xml b/manager/app/src/main/res/values-zh-rTW/strings.xml index c5c90b4c..3a7744b7 100644 --- a/manager/app/src/main/res/values-zh-rTW/strings.xml +++ b/manager/app/src/main/res/values-zh-rTW/strings.xml @@ -367,7 +367,6 @@ 應用 確認重設 - 確認重設 無法找到 ksu_susfs 檔案 SuSFS 指令執行失敗 @@ -489,11 +488,9 @@ 檔案/目錄路徑 提示:可使用「default」來使用原始值 新增 Kstat 路徑 - 此命令用於在路徑被綁定掛載或覆蓋之前新增,用於在核心記憶體中儲存原始 stat 資訊 新增 重置 Kstat 設定 確定要清除所有 Kstat 設定嗎?此操作不可撤銷 - 確認重置 Kstat 設定說明 • add_sus_kstat_statically:靜態設定檔案/目錄的 stat 資訊 • add_sus_kstat:在綁定掛載前新增路徑,儲存原始 stat 資訊 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index d417a824..659a8077 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -369,7 +369,6 @@ Apply Confirm Reset - Confirm Reset Cannot find ksu_susfs file SuSFS command execution failed @@ -491,11 +490,9 @@ File/Directory Path Hint: You can use ”default“ to use the original value Add Kstat Path - This command is used to add before the path is bind-mounted or overlaid, storing the original stat information in kernel memory. Add Reset Kstat Configuration Are you sure you want to clear all Kstat configurations? This action cannot be undone. - Confirm Reset 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 @@ -522,4 +519,24 @@ 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