diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt index 83a500a9..50561dd0 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt @@ -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 = { 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 789d2cc0..30ca7187 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 @@ -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()) } + var currentActiveSlot by remember { mutableStateOf("") } + var isLoadingSlotInfo by remember { mutableStateOf(false) } + var showSlotInfoDialog by remember { mutableStateOf(false) } + // 路径管理相关状态 var susPaths by remember { mutableStateOf(emptySet()) } var susMounts by remember { mutableStateOf(emptySet()) } @@ -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 + ) + } + } + } } } 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 58684676..6e1657ce 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 @@ -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(), null) + .exec().out + .joinToString("\n") + } + + /** + * 获取当前槽位信息 + */ + suspend fun getCurrentSlotInfo(): List = withContext(Dispatchers.IO) { + try { + val shell = getRootShell() + val slotInfoList = mutableListOf() + + // 获取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() } diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt index c82f1d3a..0be0945c 100644 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt @@ -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() 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 95395394..8cfcc524 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -529,4 +529,18 @@ Post-FS-Data 在系统服务启动后执行 在文件系统挂载后但系统完全启动前执行,可能会导致循环重启 + 槽位信息 + 查看当前启动槽位信息并复制数值 + 当前活动槽位:%s + 槽位:%s + Uname:%s + 构建时间:%s + 当前 + 使用Uname + 使用构建时间 + 无法获取槽位信息 + 正在加载槽位信息… + + SuSFS自启动模块已启用,模块路径:%s + SuSFS自启动模块已禁用 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index b55ab6a6..cfb5173c 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -531,4 +531,18 @@ 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 + Slot: %s + Uname: %s + Build Time: %s + Current + Use Uname + Use Build Time + Unable to retrieve slot information + Loading slot information… + + SuSFS auto-start module enabled, module path: %s + SuSFS auto-start module disabled \ No newline at end of file