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>
This commit is contained in:
@@ -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<String?>(null) }
|
||||
var backupInfo by remember { mutableStateOf<SuSFSManager.BackupData?>(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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String, Any>
|
||||
) {
|
||||
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<String, Any>()
|
||||
|
||||
configurationsJson.keys().forEach { key ->
|
||||
val value = configurationsJson.get(key)
|
||||
configurations[key] = when (value) {
|
||||
is org.json.JSONArray -> {
|
||||
val set = mutableSetOf<String>()
|
||||
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<String, Any> {
|
||||
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<String, Any>) {
|
||||
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<String>)
|
||||
}
|
||||
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<SlotInfo> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user