manager: Add configure susfs uname value in more settings

Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
This commit is contained in:
ShirkNeko
2025-06-14 01:10:40 +08:00
parent d7a5e80d34
commit d6c8ef3737
6 changed files with 811 additions and 1 deletions

View File

@@ -0,0 +1,327 @@
package com.sukisu.ultra.ui.component
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AutoMode
import androidx.compose.material.icons.filled.RestoreFromTrash
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.util.SuSFSManager
import kotlinx.coroutines.launch
/**
* SuSFS配置对话框
*/
@Composable
fun SuSFSConfigDialog(
onDismiss: () -> Unit
) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
var unameValue by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
var showConfirmReset by remember { mutableStateOf(false) }
var autoStartEnabled by remember { mutableStateOf(false) }
var lastAppliedValue by remember { mutableStateOf("") }
// 实时判断是否可以启用开机自启动
val canEnableAutoStart by remember {
derivedStateOf {
unameValue.trim().isNotBlank() && unameValue.trim() != "default"
}
}
// 加载当前配置
LaunchedEffect(Unit) {
unameValue = SuSFSManager.getUnameValue(context)
autoStartEnabled = SuSFSManager.isAutoStartEnabled(context)
lastAppliedValue = SuSFSManager.getLastAppliedValue(context)
}
// 当输入值变化时,自动调整开机自启动状态
LaunchedEffect(canEnableAutoStart) {
if (!canEnableAutoStart && autoStartEnabled) {
// 如果输入值变为default或空自动关闭开机自启动
autoStartEnabled = false
SuSFSManager.configureAutoStart(context, false)
}
}
// 重置确认对话框
if (showConfirmReset) {
AlertDialog(
onDismissRequest = { showConfirmReset = false },
title = {
Text(
text = stringResource(R.string.susfs_reset_confirm_title),
style = MaterialTheme.typography.titleMedium
)
},
text = {
Text(stringResource(R.string.susfs_reset_confirm_message))
},
confirmButton = {
TextButton(
onClick = {
showConfirmReset = false
coroutineScope.launch {
isLoading = true
if (SuSFSManager.resetToDefault(context)) {
unameValue = "default"
lastAppliedValue = "default"
autoStartEnabled = false
}
isLoading = false
}
}
) {
Text(stringResource(R.string.susfs_reset_confirm))
}
},
dismissButton = {
TextButton(
onClick = { showConfirmReset = false }
) {
Text(stringResource(R.string.cancel))
}
}
)
}
AlertDialog(
onDismissRequest = onDismiss,
title = {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.susfs_config_title),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
}
},
text = {
Column(
modifier = Modifier.fillMaxWidth()
) {
// 说明卡片
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
),
shape = RoundedCornerShape(8.dp)
) {
Column(
modifier = Modifier.padding(12.dp)
) {
Text(
text = stringResource(R.string.susfs_config_description),
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(R.string.susfs_config_description_text),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// 输入框
OutlinedTextField(
value = unameValue,
onValueChange = { unameValue = it },
label = { Text(stringResource(R.string.susfs_uname_label)) },
placeholder = { Text(stringResource(R.string.susfs_uname_placeholder)) },
modifier = Modifier.fillMaxWidth(),
enabled = !isLoading,
singleLine = true
)
Spacer(modifier = Modifier.height(8.dp))
// 当前值显示
Text(
text = stringResource(R.string.susfs_current_value, SuSFSManager.getUnameValue(context)),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(16.dp))
// 开机自启动开关
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = if (canEnableAutoStart) {
MaterialTheme.colorScheme.surface
} else {
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
}
),
shape = RoundedCornerShape(8.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.weight(1f)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.AutoMode,
contentDescription = null,
tint = if (canEnableAutoStart) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
},
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = stringResource(R.string.susfs_autostart_title),
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Medium,
color = if (canEnableAutoStart) {
MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
}
)
}
Spacer(modifier = Modifier.height(4.dp))
Text(
text = if (canEnableAutoStart) {
stringResource(R.string.susfs_autostart_description)
} else {
stringResource(R.string.susfs_autostart_tis)
},
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(
alpha = if (canEnableAutoStart) 1f else 0.5f
)
)
}
Switch(
checked = autoStartEnabled,
onCheckedChange = { enabled ->
if (canEnableAutoStart) {
coroutineScope.launch {
isLoading = true
if (SuSFSManager.configureAutoStart(context, enabled)) {
autoStartEnabled = enabled
}
isLoading = false
}
}
},
enabled = !isLoading && canEnableAutoStart
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// 重置按钮
OutlinedButton(
onClick = { showConfirmReset = true },
modifier = Modifier.fillMaxWidth(),
enabled = !isLoading
) {
Icon(
imageVector = Icons.Default.RestoreFromTrash,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp)
)
Text(stringResource(R.string.susfs_reset_to_default))
}
}
},
confirmButton = {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
TextButton(
onClick = onDismiss,
enabled = !isLoading
) {
Text(stringResource(R.string.cancel))
}
Button(
onClick = {
if (unameValue.isNotBlank()) {
coroutineScope.launch {
isLoading = true
val success = SuSFSManager.setUname(context, unameValue.trim())
if (success) {
lastAppliedValue = unameValue.trim()
onDismiss()
}
isLoading = false
}
}
},
enabled = !isLoading && unameValue.isNotBlank()
) {
Text(
stringResource(R.string.susfs_apply)
)
}
}
},
dismissButton = null
)
}

View File

@@ -0,0 +1,399 @@
package com.sukisu.ultra.ui.util
import android.content.Context
import android.content.SharedPreferences
import android.widget.Toast
import com.sukisu.ultra.R
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.FileOutputStream
import java.io.IOException
import java.io.File
/**
* SuSFS 配置管理器
* 用于管理SuSFS相关的配置和命令执行
*/
object SuSFSManager {
private const val PREFS_NAME = "susfs_config"
private const val KEY_UNAME_VALUE = "uname_value"
private const val KEY_IS_ENABLED = "is_enabled"
private const val KEY_AUTO_START_ENABLED = "auto_start_enabled"
private const val KEY_LAST_APPLIED_VALUE = "last_applied_value"
private const val SUSFS_BINARY_NAME = "ksu_susfs"
private const val DEFAULT_UNAME = "default"
private const val STARTUP_SCRIPT_PATH = "/data/adb/service.d/susfs_startup.sh"
private const val SUSFS_TARGET_PATH = "/data/adb/ksu/bin/$SUSFS_BINARY_NAME"
/**
* 获取Root Shell实例
*/
private fun getRootShell(): Shell {
return Shell.getShell()
}
/**
* 获取SuSFS配置的SharedPreferences
*/
private fun getPrefs(context: Context): SharedPreferences {
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
}
/**
* 保存uname值
*/
fun saveUnameValue(context: Context, value: String) {
getPrefs(context).edit().apply {
putString(KEY_UNAME_VALUE, value)
apply()
}
}
/**
* 获取保存的uname值
*/
fun getUnameValue(context: Context): String {
return getPrefs(context).getString(KEY_UNAME_VALUE, DEFAULT_UNAME) ?: DEFAULT_UNAME
}
/**
* 保存最后应用的值
*/
private fun saveLastAppliedValue(context: Context, value: String) {
getPrefs(context).edit().apply {
putString(KEY_LAST_APPLIED_VALUE, value)
apply()
}
}
/**
* 获取最后应用的值
*/
fun getLastAppliedValue(context: Context): String {
return getPrefs(context).getString(KEY_LAST_APPLIED_VALUE, DEFAULT_UNAME) ?: DEFAULT_UNAME
}
/**
* 保存SuSFS启用状态
*/
fun setEnabled(context: Context, enabled: Boolean) {
getPrefs(context).edit().apply {
putBoolean(KEY_IS_ENABLED, enabled)
apply()
}
}
/**
* 设置开机自启动状态
*/
fun setAutoStartEnabled(context: Context, enabled: Boolean) {
getPrefs(context).edit().apply {
putBoolean(KEY_AUTO_START_ENABLED, enabled)
apply()
}
}
/**
* 获取开机自启动状态
*/
fun isAutoStartEnabled(context: Context): Boolean {
return getPrefs(context).getBoolean(KEY_AUTO_START_ENABLED, false)
}
/**
* 从assets复制ksu_susfs文件到/data/adb/ksu/bin/
*/
private suspend fun copyBinaryFromAssets(context: Context): String? = withContext(Dispatchers.IO) {
try {
val inputStream = context.assets.open(SUSFS_BINARY_NAME)
val tempFile = File(context.cacheDir, SUSFS_BINARY_NAME)
FileOutputStream(tempFile).use { outputStream ->
inputStream.copyTo(outputStream)
}
// 创建目标目录并复制文件到/data/adb/ksu/bin/
val shell = getRootShell()
val commands = arrayOf(
"cp '${tempFile.absolutePath}' '$SUSFS_TARGET_PATH'",
"chmod 755 '$SUSFS_TARGET_PATH'",
)
var success = true
for (command in commands) {
val result = shell.newJob().add(command).exec()
if (!result.isSuccess) {
success = false
break
}
}
// 清理临时文件
tempFile.delete()
if (success) {
val verifyResult = shell.newJob().add("test -f '$SUSFS_TARGET_PATH'").exec()
if (verifyResult.isSuccess) {
SUSFS_TARGET_PATH
} else {
null
}
} else {
null
}
} catch (e: IOException) {
e.printStackTrace()
null
}
}
/**
* 创建开机自启动脚本
*/
private suspend fun createStartupScript(unameValue: String): Boolean = withContext(Dispatchers.IO) {
try {
val scriptContent = """#!/system/bin/sh
# SuSFS 开机自启动脚本
# 由 KernelSU Manager 自动生成
# 等待系统完全启动
sleep 30
# 检查二进制文件是否存在
if [ -f "$SUSFS_TARGET_PATH" ]; then
# 执行 SuSFS setUname 命令
$SUSFS_TARGET_PATH set_uname '$unameValue' '$DEFAULT_UNAME'
# 记录日志
echo "\$(date): SuSFS setUname executed with value: $unameValue" >> /data/adb/ksu/log/susfs_startup.log
else
echo "\$(date): SuSFS binary not found at $SUSFS_TARGET_PATH" >> /data/adb/ksu/log/susfs_startup.log
fi
"""
val shell = getRootShell()
val commands = arrayOf(
"mkdir -p /data/adb/service.d",
"cat > $STARTUP_SCRIPT_PATH << 'EOF'\n$scriptContent\nEOF",
"chmod 755 $STARTUP_SCRIPT_PATH"
)
var success = true
for (command in commands) {
val result = shell.newJob().add(command).exec()
if (!result.isSuccess) {
success = false
break
}
}
success
} catch (e: Exception) {
e.printStackTrace()
false
}
}
/**
* 删除开机自启动脚本
*/
private suspend fun removeStartupScript(): Boolean = withContext(Dispatchers.IO) {
try {
val shell = getRootShell()
val result = shell.newJob().add("rm -f $STARTUP_SCRIPT_PATH").exec()
result.isSuccess
} catch (e: Exception) {
e.printStackTrace()
false
}
}
/**
* 执行SuSFS命令设置uname
*/
suspend fun setUname(context: Context, unameValue: String): Boolean = withContext(Dispatchers.IO) {
try {
// 首先复制二进制文件到/data/adb/ksu/bin/
val binaryPath = copyBinaryFromAssets(context)
if (binaryPath == null) {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.susfs_binary_not_found),
Toast.LENGTH_SHORT
).show()
}
return@withContext false
}
// 构建命令
val command = "$binaryPath set_uname '$unameValue' '$DEFAULT_UNAME'"
// 执行命令
val result = getRootShell().newJob().add(command).exec()
if (result.isSuccess) {
// 保存配置
saveUnameValue(context, unameValue)
saveLastAppliedValue(context, unameValue)
setEnabled(context, true)
// 如果开启了开机自启动,更新启动脚本
if (isAutoStartEnabled(context)) {
createStartupScript(unameValue)
}
withContext(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.susfs_uname_set_success, unameValue),
Toast.LENGTH_SHORT
).show()
}
true
} else {
withContext(Dispatchers.Main) {
val errorOutput = result.out.joinToString("\n") + "\n" + result.err.joinToString("\n")
Toast.makeText(
context,
context.getString(R.string.susfs_command_failed) + "\n$errorOutput",
Toast.LENGTH_LONG
).show()
}
false
}
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.susfs_command_error, e.message ?: "Unknown error"),
Toast.LENGTH_SHORT
).show()
}
false
}
}
/**
* 配置开机自启动
*/
suspend fun configureAutoStart(context: Context, enabled: Boolean): Boolean = withContext(Dispatchers.IO) {
try {
if (enabled) {
// 启用开机自启动
val lastValue = getLastAppliedValue(context)
if (lastValue == DEFAULT_UNAME) {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.susfs_no_config_to_autostart),
Toast.LENGTH_SHORT
).show()
}
return@withContext false
}
// 确保二进制文件存在于目标位置
val shell = getRootShell()
val checkResult = shell.newJob().add("test -f '$SUSFS_TARGET_PATH'").exec()
if (!checkResult.isSuccess) {
// 如果不存在,尝试复制
val binaryPath = copyBinaryFromAssets(context)
if (binaryPath == null) {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.susfs_binary_not_found),
Toast.LENGTH_SHORT
).show()
}
return@withContext false
}
}
val success = createStartupScript(lastValue)
if (success) {
setAutoStartEnabled(context, true)
withContext(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.susfs_autostart_enabled),
Toast.LENGTH_SHORT
).show()
}
} else {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.susfs_autostart_enable_failed),
Toast.LENGTH_SHORT
).show()
}
}
success
} else {
// 禁用开机自启动
val success = removeStartupScript()
if (success) {
setAutoStartEnabled(context, false)
withContext(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.susfs_autostart_disabled),
Toast.LENGTH_SHORT
).show()
}
} else {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.susfs_autostart_disable_failed),
Toast.LENGTH_SHORT
).show()
}
}
success
}
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.susfs_autostart_error, e.message ?: "Unknown error"),
Toast.LENGTH_SHORT
).show()
}
false
}
}
/**
* 重置为默认值
*/
suspend fun resetToDefault(context: Context): Boolean {
val success = setUname(context, DEFAULT_UNAME)
if (success) {
// 重置时清除最后应用的值
saveLastAppliedValue(context, DEFAULT_UNAME)
// 如果开启了开机自启动,需要禁用它
if (isAutoStartEnabled(context)) {
configureAutoStart(context, false)
}
}
return success
}
/**
* 检查ksu_susfs文件是否存在于assets中
*/
fun isBinaryAvailable(context: Context): Boolean {
return try {
context.assets.open(SUSFS_BINARY_NAME).use { true }
} catch (_: IOException) {
false
}
}
}

View File

@@ -63,6 +63,7 @@ import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.ImageEditorDialog
import com.sukisu.ultra.ui.component.KsuIsValid
import com.sukisu.ultra.ui.component.SuSFSConfigDialog
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
import com.sukisu.ultra.ui.theme.*
import com.sukisu.ultra.ui.util.*
@@ -145,6 +146,7 @@ fun MoreSettingsScreen(
var showThemeColorDialog by remember { mutableStateOf(false) }
var showDpiConfirmDialog by remember { mutableStateOf(false) }
var showImageEditor by remember { mutableStateOf(false) }
var showSuSFSConfigDialog by remember { mutableStateOf(false) }
// 主题模式选项
val themeOptions = listOf(
@@ -475,6 +477,13 @@ fun MoreSettingsScreen(
)
}
// SuSFS配置对话框
if (showSuSFSConfigDialog) {
SuSFSConfigDialog(
onDismiss = { showSuSFSConfigDialog = false }
)
}
// 主题模式选择对话框
if (showThemeModeDialog) {
SingleChoiceDialog(
@@ -1120,7 +1129,20 @@ fun MoreSettingsScreen(
)
}
// SuSFS 配置(仅在支持时显示)
// SuSFS 配置(仅在支持时显示
if (getSuSFS() == "Supported" && SuSFSManager.isBinaryAvailable(context)) {
SettingItem(
icon = Icons.Default.Settings,
title = stringResource(R.string.susfs_config_setting_title),
subtitle = stringResource(
R.string.susfs_config_setting_summary,
SuSFSManager.getUnameValue(context)
),
onClick = { showSuSFSConfigDialog = true }
)
}
// SuSFS 开关(仅在支持时显示)
val suSFS = getSuSFS()
val isSUS_SU = getSuSFSFeatures()
if (suSFS == "Supported" && isSUS_SU == "CONFIG_KSU_SUSFS_SUS_SU") {