manager:Add SuSFS to obtain slot uname and build time information

This commit is contained in:
ShirkNeko
2025-06-18 15:41:08 +08:00
parent bfb6ea3613
commit b537b51034
6 changed files with 355 additions and 22 deletions

View File

@@ -77,6 +77,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination
import com.ramcosta.composedestinations.generated.destinations.SuSFSConfigScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.sukisu.ultra.KernelVersion
import com.sukisu.ultra.Natives
@@ -91,6 +92,8 @@ import com.sukisu.ultra.ui.theme.getCardElevation
import com.sukisu.ultra.ui.util.checkNewVersion
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
import com.sukisu.ultra.ui.util.reboot
import com.sukisu.ultra.ui.util.getSuSFS
import com.sukisu.ultra.ui.util.SuSFSManager
import com.sukisu.ultra.ui.viewmodel.HomeViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -127,7 +130,8 @@ fun HomeScreen(navigator: DestinationsNavigator) {
Scaffold(
topBar = {
TopBar(
scrollBehavior = scrollBehavior
scrollBehavior = scrollBehavior,
navigator = navigator
)
},
contentWindowInsets = WindowInsets.safeDrawing.only(
@@ -257,15 +261,17 @@ fun UpdateCard() {
@Composable
fun RebootDropdownItem(@StringRes id: Int, reason: String = "") {
DropdownMenuItem(
text = {Text(stringResource(id))},
text = {Text(stringResource(id))},
onClick = {reboot(reason)})
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(
scrollBehavior: TopAppBarScrollBehavior? = null
scrollBehavior: TopAppBarScrollBehavior? = null,
navigator: DestinationsNavigator
) {
val context = LocalContext.current
val colorScheme = MaterialTheme.colorScheme
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
colorScheme.surfaceContainerLow
@@ -285,6 +291,19 @@ private fun TopBar(
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
),
actions = {
// SuSFS 配置按钮
if (getSuSFS() == "Supported" && SuSFSManager.isBinaryAvailable(context)) {
IconButton(onClick = {
navigator.navigate(SuSFSConfigScreenDestination)
}) {
Icon(
imageVector = Icons.Filled.Tune,
contentDescription = stringResource(R.string.susfs_config_setting_title)
)
}
}
// 重启按钮
var showDropdown by remember { mutableStateOf(false) }
KsuIsValid {
IconButton(onClick = {

View File

@@ -28,6 +28,7 @@ import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.RestoreFromTrash
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Storage
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
@@ -122,6 +123,12 @@ fun SuSFSConfigScreen(
var lastAppliedBuildTime by remember { mutableStateOf("") }
var executeInPostFsData by remember { mutableStateOf(false) } // 是否在post-fs-data中执行
// 槽位信息相关状态
var slotInfoList by remember { mutableStateOf(emptyList<SuSFSManager.SlotInfo>()) }
var currentActiveSlot by remember { mutableStateOf("") }
var isLoadingSlotInfo by remember { mutableStateOf(false) }
var showSlotInfoDialog by remember { mutableStateOf(false) }
// 路径管理相关状态
var susPaths by remember { mutableStateOf(emptySet<String>()) }
var susMounts by remember { mutableStateOf(emptySet<String>()) }
@@ -167,6 +174,16 @@ fun SuSFSConfigScreen(
}
}
// 加载槽位信息
fun loadSlotInfo() {
coroutineScope.launch {
isLoadingSlotInfo = true
slotInfoList = SuSFSManager.getCurrentSlotInfo()
currentActiveSlot = SuSFSManager.getCurrentActiveSlot()
isLoadingSlotInfo = false
}
}
// 加载当前配置
LaunchedEffect(Unit) {
unameValue = SuSFSManager.getUnameValue(context)
@@ -180,6 +197,9 @@ fun SuSFSConfigScreen(
tryUmounts = SuSFSManager.getTryUmounts(context)
androidDataPath = SuSFSManager.getAndroidDataPath(context)
sdcardPath = SuSFSManager.getSdcardPath(context)
// 加载槽位信息
loadSlotInfo()
}
// 当切换到启用功能状态标签页时加载数据
@@ -197,6 +217,153 @@ fun SuSFSConfigScreen(
}
}
// 槽位信息对话框
if (showSlotInfoDialog) {
AlertDialog(
onDismissRequest = { showSlotInfoDialog = false },
title = {
Text(
text = stringResource(R.string.susfs_slot_info_title),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
},
text = {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = stringResource(R.string.susfs_current_active_slot, currentActiveSlot),
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary
)
if (slotInfoList.isNotEmpty()) {
slotInfoList.forEach { slotInfo ->
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = if (slotInfo.slotName == currentActiveSlot) {
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
} else {
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
}
),
shape = RoundedCornerShape(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) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = slotInfo.slotName,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = if (slotInfo.slotName == currentActiveSlot) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurface
}
)
if (slotInfo.slotName == currentActiveSlot) {
Spacer(modifier = Modifier.width(6.dp))
Surface(
shape = RoundedCornerShape(4.dp),
color = MaterialTheme.colorScheme.primary
) {
Text(
text = stringResource(R.string.susfs_slot_current_badge),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
)
}
}
}
Text(
text = stringResource(R.string.susfs_slot_uname, slotInfo.uname),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = stringResource(R.string.susfs_slot_build_time, slotInfo.buildTime),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = {
unameValue = slotInfo.uname
showSlotInfoDialog = false
},
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(6.dp)
) {
Text(stringResource(R.string.susfs_slot_use_uname), fontSize = 12.sp)
}
Button(
onClick = {
buildTimeValue = slotInfo.buildTime
showSlotInfoDialog = false
},
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(6.dp)
) {
Text(stringResource(R.string.susfs_slot_use_build_time), fontSize = 12.sp)
}
}
}
}
}
} else {
Text(
text = stringResource(R.string.susfs_slot_info_unavailable),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error
)
}
}
},
confirmButton = {
Button(
onClick = { loadSlotInfo() },
enabled = !isLoadingSlotInfo,
shape = RoundedCornerShape(8.dp)
) {
Text(stringResource(R.string.refresh))
}
},
dismissButton = {
TextButton(
onClick = { showSlotInfoDialog = false },
shape = RoundedCornerShape(8.dp)
) {
Text(stringResource(R.string.close))
}
},
shape = RoundedCornerShape(12.dp)
)
}
// 各种对话框的定义保持不变
// 添加路径对话框
if (showAddPathDialog) {
@@ -940,6 +1107,7 @@ fun SuSFSConfigScreen(
}
}
},
onShowSlotInfo = { showSlotInfoDialog = true },
context = context
)
}
@@ -1043,6 +1211,7 @@ private fun BasicSettingsContent(
canEnableAutoStart: Boolean,
isLoading: Boolean,
onAutoStartToggle: (Boolean) -> Unit,
onShowSlotInfo: () -> Unit,
context: android.content.Context
) {
var scriptLocationExpanded by remember { mutableStateOf(false) }
@@ -1252,6 +1421,62 @@ private fun BasicSettingsContent(
)
}
}
// 槽位信息按钮
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
shape = RoundedCornerShape(12.dp)
) {
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.susfs_slot_info_title),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface
)
}
Text(
text = stringResource(R.string.susfs_slot_info_description),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
lineHeight = 14.sp
)
OutlinedButton(
onClick = onShowSlotInfo,
enabled = !isLoading,
shape = RoundedCornerShape(8.dp),
modifier = Modifier.fillMaxWidth()
) {
Icon(
imageVector = Icons.Default.Storage,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(6.dp))
Text(
stringResource(R.string.susfs_slot_info_title),
fontWeight = FontWeight.Medium
)
}
}
}
}
}

View File

@@ -42,6 +42,15 @@ object SuSFSManager {
private const val MODULE_ID = "susfs_manager"
private const val MODULE_PATH = "/data/adb/modules/$MODULE_ID"
/**
* 槽位信息数据类
*/
data class SlotInfo(
val slotName: String,
val uname: String,
val buildTime: String
)
private fun getSuSFS(): String {
return try {
getSuSFSVersion()
@@ -86,6 +95,74 @@ object SuSFSManager {
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
}
/**
* 执行命令的通用方法
*/
private fun runCmd(shell: Shell, cmd: String): String {
return shell.newJob()
.add(cmd)
.to(mutableListOf<String>(), null)
.exec().out
.joinToString("\n")
}
/**
* 获取当前槽位信息
*/
suspend fun getCurrentSlotInfo(): List<SlotInfo> = withContext(Dispatchers.IO) {
try {
val shell = getRootShell()
val slotInfoList = mutableListOf<SlotInfo>()
// 获取boot_a槽位信息
val bootAUname = runCmd(shell,
"strings -n 20 /dev/block/by-name/boot_a | awk '/Linux version/ && ++c==2 {print $3; exit}'"
).trim()
val bootABuildTime = runCmd(shell, "strings -n 20 /dev/block/by-name/boot_a | sed -n '/Linux version.*#/{s/.*#/#/p;q}'").trim()
if (bootAUname.isNotEmpty() && bootABuildTime.isNotEmpty()) {
val uname = bootAUname.ifEmpty { "unknown" }
val buildTime = bootABuildTime.ifEmpty { "unknown" }
slotInfoList.add(SlotInfo("boot_a", uname, buildTime))
}
// 获取boot_b槽位信息
val bootBUname = runCmd(shell,
"strings -n 20 /dev/block/by-name/boot_b | awk '/Linux version/ && ++c==2 {print $3; exit}'"
).trim()
val bootBBuildTime = runCmd(shell, "strings -n 20 /dev/block/by-name/boot_b | sed -n '/Linux version.*#/{s/.*#/#/p;q}'").trim()
if (bootBUname.isNotEmpty() && bootBBuildTime.isNotEmpty()) {
val uname = bootBUname.ifEmpty { "unknown" }
val buildTime = bootBBuildTime.ifEmpty { "unknown" }
slotInfoList.add(SlotInfo("boot_b", uname, buildTime))
}
slotInfoList
} catch (e: Exception) {
e.printStackTrace()
emptyList()
}
}
/**
* 获取当前活动槽位
*/
suspend fun getCurrentActiveSlot(): String = withContext(Dispatchers.IO) {
try {
val shell = getRootShell()
val suffix = runCmd(shell, "getprop ro.boot.slot_suffix").trim()
when (suffix) {
"_a" -> "boot_a"
"_b" -> "boot_b"
else -> "unknown"
}
} catch (e: Exception) {
e.printStackTrace()
"unknown"
}
}
/**
* 保存uname值
*/
@@ -884,7 +961,7 @@ object SuSFSManager {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
"SuSFS self-startup module is enabled, module path$MODULE_PATH",
context.getString(R.string.susfs_autostart_enabled_success, MODULE_PATH),
Toast.LENGTH_LONG
).show()
}
@@ -906,7 +983,7 @@ object SuSFSManager {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
"SuSFS自启动模块已禁用",
context.getString(R.string.susfs_autostart_disabled_success),
Toast.LENGTH_SHORT
).show()
}

View File

@@ -89,7 +89,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.unit.sp
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.sukisu.ultra.ksuApp
import com.ramcosta.composedestinations.generated.destinations.SuSFSConfigScreenDestination
/**
* @author ShirkNeko
@@ -1158,21 +1157,6 @@ fun MoreSettingsScreen(
}
)
// 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 = {
navigator.navigate(SuSFSConfigScreenDestination)
}
)
}
// SuSFS 开关(仅在支持时显示)
val suSFS = getSuSFS()
val isSUS_SU = getSuSFSFeatures()

View File

@@ -529,4 +529,18 @@
<string name="susfs_execution_location_post_fs_data">Post-FS-Data</string>
<string name="susfs_execution_location_service_description">在系统服务启动后执行</string>
<string name="susfs_execution_location_post_fs_data_description">在文件系统挂载后但系统完全启动前执行,可能会导致循环重启</string>
<string name="susfs_slot_info_title">槽位信息</string>
<string name="susfs_slot_info_description">查看当前启动槽位信息并复制数值</string>
<string name="susfs_current_active_slot">当前活动槽位:%s</string>
<string name="susfs_slot_name">槽位:%s</string>
<string name="susfs_slot_uname">Uname%s</string>
<string name="susfs_slot_build_time">构建时间:%s</string>
<string name="susfs_slot_current_badge">当前</string>
<string name="susfs_slot_use_uname">使用Uname</string>
<string name="susfs_slot_use_build_time">使用构建时间</string>
<string name="susfs_slot_info_unavailable">无法获取槽位信息</string>
<string name="susfs_slot_info_loading">正在加载槽位信息…</string>
<!-- SuSFS 自启动相关字符串 -->
<string name="susfs_autostart_enabled_success">SuSFS自启动模块已启用模块路径%s</string>
<string name="susfs_autostart_disabled_success">SuSFS自启动模块已禁用</string>
</resources>

View File

@@ -531,4 +531,18 @@
<string name="susfs_execution_location_post_fs_data">Post-FS-Data</string>
<string name="susfs_execution_location_service_description">Execute after system services start</string>
<string name="susfs_execution_location_post_fs_data_description">Execute after file system is mounted but before system is fully bootedMay cause a boot loop</string>
<string name="susfs_slot_info_title">Slot Information</string>
<string name="susfs_slot_info_description">View current boot slot information and copy values</string>
<string name="susfs_current_active_slot">Current Active Slot: %s</string>
<string name="susfs_slot_name">Slot: %s</string>
<string name="susfs_slot_uname">Uname: %s</string>
<string name="susfs_slot_build_time">Build Time: %s</string>
<string name="susfs_slot_current_badge">Current</string>
<string name="susfs_slot_use_uname">Use Uname</string>
<string name="susfs_slot_use_build_time">Use Build Time</string>
<string name="susfs_slot_info_unavailable">Unable to retrieve slot information</string>
<string name="susfs_slot_info_loading">Loading slot information…</string>
<!-- SuSFS 自启动相关字符串 -->
<string name="susfs_autostart_enabled_success">SuSFS auto-start module enabled, module path: %s</string>
<string name="susfs_autostart_disabled_success">SuSFS auto-start module disabled</string>
</resources>