diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperDropdown.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperDropdown.kt new file mode 100644 index 00000000..9103e555 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperDropdown.kt @@ -0,0 +1,250 @@ +package com.sukisu.ultra.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SuperDropdown( + items: List, + selectedIndex: Int, + title: String, + summary: String? = null, + icon: ImageVector? = null, + enabled: Boolean = true, + showValue: Boolean = true, + maxHeight: Dp? = 400.dp, + colors: SuperDropdownColors = SuperDropdownDefaults.colors(), + leftAction: (@Composable () -> Unit)? = null, + onSelectedIndexChange: (Int) -> Unit +) { + var showDialog by remember { mutableStateOf(false) } + val selectedItemText = items.getOrNull(selectedIndex) ?: "" + val itemsNotEmpty = items.isNotEmpty() + val actualEnabled = enabled && itemsNotEmpty + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = actualEnabled) { showDialog = true } + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.Top + ) { + if (leftAction != null) { + leftAction() + } else if (icon != null) { + Icon( + imageVector = icon, + contentDescription = null, + tint = if (actualEnabled) colors.iconColor else colors.disabledIconColor, + modifier = Modifier + .padding(end = 16.dp) + .size(24.dp) + ) + } + + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = if (actualEnabled) colors.titleColor else colors.disabledTitleColor + ) + + if (summary != null) { + Spacer(modifier = Modifier.height(3.dp)) + Text( + text = summary, + style = MaterialTheme.typography.bodyMedium, + color = if (actualEnabled) colors.summaryColor else colors.disabledSummaryColor + ) + } + + if (showValue && itemsNotEmpty) { + Spacer(modifier = Modifier.height(3.dp)) + Text( + text = selectedItemText, + style = MaterialTheme.typography.bodyMedium, + color = if (actualEnabled) colors.valueColor else colors.disabledValueColor, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + } + + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = null, + tint = if (actualEnabled) colors.arrowColor else colors.disabledArrowColor, + modifier = Modifier.size(24.dp) + ) + } + + if (showDialog && itemsNotEmpty) { + AlertDialog( + onDismissRequest = { showDialog = false }, + title = { + Text( + text = title, + style = MaterialTheme.typography.headlineSmall + ) + }, + text = { + val dialogMaxHeight = maxHeight ?: 400.dp + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = dialogMaxHeight), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(items.size) { index -> + DropdownItem( + text = items[index], + isSelected = selectedIndex == index, + colors = colors, + onClick = { + onSelectedIndexChange(index) + showDialog = false + } + ) + } + } + }, + confirmButton = { + TextButton(onClick = { showDialog = false }) { + Text(text = stringResource(id = android.R.string.cancel)) + } + }, + containerColor = colors.dialogBackgroundColor, + shape = MaterialTheme.shapes.extraLarge, + tonalElevation = 4.dp + ) + } +} + +@Composable +private fun DropdownItem( + text: String, + isSelected: Boolean, + colors: SuperDropdownColors, + onClick: () -> Unit +) { + val backgroundColor = if (isSelected) { + colors.selectedBackgroundColor + } else { + Color.Transparent + } + + val contentColor = if (isSelected) { + colors.selectedContentColor + } else { + colors.contentColor + } + + Row( + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .background(backgroundColor) + .clickable(onClick = onClick) + .padding(vertical = 12.dp, horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = isSelected, + onClick = null, + colors = RadioButtonDefaults.colors( + selectedColor = colors.selectedContentColor, + unselectedColor = colors.contentColor + ) + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Text( + text = text, + style = MaterialTheme.typography.bodyLarge, + color = contentColor, + modifier = Modifier.weight(1f) + ) + + if (isSelected) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + tint = colors.selectedContentColor, + modifier = Modifier.size(20.dp) + ) + } + } +} + +@Immutable +data class SuperDropdownColors( + val titleColor: Color, + val summaryColor: Color, + val valueColor: Color, + val iconColor: Color, + val arrowColor: Color, + val disabledTitleColor: Color, + val disabledSummaryColor: Color, + val disabledValueColor: Color, + val disabledIconColor: Color, + val disabledArrowColor: Color, + val dialogBackgroundColor: Color, + val contentColor: Color, + val selectedContentColor: Color, + val selectedBackgroundColor: Color +) + +object SuperDropdownDefaults { + @Composable + fun colors( + titleColor: Color = MaterialTheme.colorScheme.onSurface, + summaryColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + valueColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + iconColor: Color = MaterialTheme.colorScheme.primary, + arrowColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + disabledTitleColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), + disabledSummaryColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f), + disabledValueColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f), + disabledIconColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f), + disabledArrowColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f), + dialogBackgroundColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh, + contentColor: Color = MaterialTheme.colorScheme.onSurface, + selectedContentColor: Color = MaterialTheme.colorScheme.primary, + selectedBackgroundColor: Color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) + ): SuperDropdownColors { + return SuperDropdownColors( + titleColor = titleColor, + summaryColor = summaryColor, + valueColor = valueColor, + iconColor = iconColor, + arrowColor = arrowColor, + disabledTitleColor = disabledTitleColor, + disabledSummaryColor = disabledSummaryColor, + disabledValueColor = disabledValueColor, + disabledIconColor = disabledIconColor, + disabledArrowColor = disabledArrowColor, + dialogBackgroundColor = dialogBackgroundColor, + contentColor = contentColor, + selectedContentColor = selectedContentColor, + selectedBackgroundColor = selectedBackgroundColor + ) + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt index 838a5858..e991839c 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt @@ -707,7 +707,7 @@ suspend fun getModuleNameFromUri(context: Context, uri: Uri): String { @Parcelize sealed class FlashIt : Parcelable { - data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean) : FlashIt() + data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean, val partition: String? = null) : FlashIt() data class FlashModule(val uri: Uri) : FlashIt() data class FlashModules(val uris: List, val currentIndex: Int = 0) : FlashIt() data class FlashModuleUpdate(val uri: Uri) : FlashIt() // 模块更新 @@ -736,6 +736,7 @@ fun flashIt( flashIt.boot, flashIt.lkm, flashIt.ota, + flashIt.partition, onFinish, onStdout, onStderr diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt index 7d45c2b5..15aa0781 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt @@ -21,6 +21,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.Input import androidx.compose.material.icons.filled.AutoFixHigh +import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.FileUpload import androidx.compose.material.icons.filled.Security import androidx.compose.material3.* @@ -51,7 +52,7 @@ import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator import com.sukisu.ultra.R import com.sukisu.ultra.getKernelVersion import com.sukisu.ultra.ui.component.DialogHandle -import zako.zako.zako.zakoui.screen.kernelFlash.component.SlotSelectionDialog +import com.sukisu.ultra.ui.component.SuperDropdown import com.sukisu.ultra.ui.component.rememberConfirmDialog import com.sukisu.ultra.ui.component.rememberCustomDialog import com.sukisu.ultra.ui.theme.CardConfig @@ -60,6 +61,7 @@ import com.sukisu.ultra.ui.theme.CardConfig.cardElevation import com.sukisu.ultra.ui.theme.getCardColors import com.sukisu.ultra.ui.theme.getCardElevation import com.sukisu.ultra.ui.util.* +import zako.zako.zako.zakoui.screen.kernelFlash.component.SlotSelectionDialog /** * @author ShirkNeko @@ -87,9 +89,12 @@ fun InstallScreen( var showSlotSelectionDialog by remember { mutableStateOf(false) } var showKpmPatchDialog by remember { mutableStateOf(false) } var tempKernelUri by remember { mutableStateOf(null) } + val kernelVersion = getKernelVersion() val isGKI = kernelVersion.isGKI() - val isAbDevice = isAbDevice() + val isAbDevice = produceState(initialValue = false) { + value = isAbDevice() + }.value val summary = stringResource(R.string.horizon_kernel_summary) // 处理预选的内核文件 @@ -103,6 +108,7 @@ fun InstallScreen( ) installMethod = horizonMethod tempKernelUri = preselectedUri + if (isAbDevice) { showSlotSelectionDialog = true } else { @@ -133,6 +139,10 @@ fun InstallScreen( ) } + var partitionSelectionIndex by remember { mutableIntStateOf(0) } + var partitionsState by remember { mutableStateOf>(emptyList()) } + var hasCustomSelected by remember { mutableStateOf(false) } + val onInstall = { installMethod?.let { method -> when (method) { @@ -149,10 +159,13 @@ fun InstallScreen( } } else -> { + val isOta = method is InstallMethod.DirectInstallToInactiveSlot + val partitionSelection = partitionsState.getOrNull(partitionSelectionIndex) val flashIt = FlashIt.FlashBoot( boot = if (method is InstallMethod.SelectFile) method.uri else null, lkm = lkmSelection, - ota = method is InstallMethod.DirectInstallToInactiveSlot + ota = isOta, + partition = partitionSelection ) navigator.navigate(FlashScreenDestination(flashIt)) } @@ -173,6 +186,7 @@ fun InstallScreen( summary = summary ) installMethod = horizonMethod + if (preselectedKernelUri != null) { showKpmPatchDialog = true } @@ -274,11 +288,72 @@ fun InstallScreen( selectedMethod = installMethod ) + // 选择LKM直接安装分区 + AnimatedVisibility( + visible = installMethod is InstallMethod.DirectInstall || installMethod is InstallMethod.DirectInstallToInactiveSlot, + enter = fadeIn() + expandVertically(), + exit = shrinkVertically() + fadeOut() + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + ElevatedCard( + colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), + elevation = getCardElevation(), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 12.dp), + ) { + val isOta = installMethod is InstallMethod.DirectInstallToInactiveSlot + val suffix = produceState(initialValue = "", isOta) { + value = getSlotSuffix(isOta) + }.value + + val partitions = produceState(initialValue = emptyList()) { + value = getAvailablePartitions() + }.value + + val defaultPartition = produceState(initialValue = "") { + value = getDefaultPartition() + }.value + + partitionsState = partitions + val displayPartitions = partitions.map { name -> + if (defaultPartition == name) "$name (default)" else name + } + + val defaultIndex = partitions.indexOf(defaultPartition).takeIf { it >= 0 } ?: 0 + if (!hasCustomSelected) partitionSelectionIndex = defaultIndex + + SuperDropdown( + items = displayPartitions, + selectedIndex = partitionSelectionIndex, + title = "${stringResource(R.string.install_select_partition)} (${suffix})", + onSelectedIndexChange = { index -> + hasCustomSelected = true + partitionSelectionIndex = index + }, + leftAction = { + Icon( + Icons.Default.Edit, + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(end = 16.dp), + contentDescription = null + ) + } + ) + } + } + } + Column( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { + // 使用本地的LKM文件 ElevatedCard( colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), elevation = getCardElevation(), @@ -311,7 +386,8 @@ fun InstallScreen( .clickable { onLkmUpload() } ) } - (installMethod as? InstallMethod.HorizonKernel)?.let { method -> + + (installMethod as? InstallMethod.HorizonKernel)?.let { method -> if (method.slot != null) { ElevatedCard( colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), @@ -319,12 +395,6 @@ fun InstallScreen( modifier = Modifier .fillMaxWidth() .padding(bottom = 12.dp) - .clip(MaterialTheme.shapes.medium) - .shadow( - elevation = cardElevation, - shape = MaterialTheme.shapes.medium, - spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) ) { Text( text = stringResource( @@ -346,12 +416,6 @@ fun InstallScreen( modifier = Modifier .fillMaxWidth() .padding(bottom = 12.dp) - .clip(MaterialTheme.shapes.medium) - .shadow( - elevation = cardElevation, - shape = MaterialTheme.shapes.medium, - spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) ) { Text( text = when (kpmPatchOption) { @@ -496,15 +560,15 @@ private fun SelectInstallMethod( selectedMethod: InstallMethod? = null ) { val rootAvailable = rootAvailable() - val isAbDevice = isAbDevice() + val isAbDevice = produceState(initialValue = false) { + value = isAbDevice() + }.value + val defaultPartitionName = produceState(initialValue = "boot") { + value = getDefaultPartition() + }.value val horizonKernelSummary = stringResource(R.string.horizon_kernel_summary) val selectFileTip = stringResource( - id = R.string.select_file_tip, - if (isInitBoot()) { - "init_boot / vendor_boot ${stringResource(R.string.select_file_tip_vendor)}" - } else { - "boot" - } + id = R.string.select_file_tip, defaultPartitionName ) val radioOptions = mutableListOf( @@ -601,7 +665,6 @@ private fun SelectInstallMethod( modifier = Modifier .fillMaxWidth() .padding(bottom = 16.dp) - .clip(MaterialTheme.shapes.large) ) { MaterialTheme( colorScheme = MaterialTheme.colorScheme.copy( @@ -640,7 +703,7 @@ private fun SelectInstallMethod( bottom = 16.dp ) ) { - radioOptions.take(3).forEach { option -> + radioOptions.filter { it !is InstallMethod.HorizonKernel }.forEach { option -> val interactionSource = remember { MutableInteractionSource() } Surface( color = if (option.javaClass == selectedOption?.javaClass) @@ -708,7 +771,6 @@ private fun SelectInstallMethod( modifier = Modifier .fillMaxWidth() .padding(bottom = 12.dp) - .clip(MaterialTheme.shapes.large) ) { MaterialTheme( colorScheme = MaterialTheme.colorScheme.copy( diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt index 6b528f5d..2828e3ad 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt @@ -150,7 +150,7 @@ fun SettingScreen(navigator: DestinationsNavigator) { } ) } - SettingDropdown( + SuperDropdown( icon = Icons.Rounded.EnhancedEncryption, title = stringResource(id = R.string.settings_enable_enhanced_security), summary = stringResource(id = R.string.settings_enable_enhanced_security_summary), @@ -193,7 +193,7 @@ fun SettingScreen(navigator: DestinationsNavigator) { } ) } - SettingDropdown( + SuperDropdown( icon = Icons.Rounded.RemoveModerator, title = stringResource(id = R.string.settings_disable_su), summary = stringResource(id = R.string.settings_disable_su_summary), @@ -236,7 +236,7 @@ fun SettingScreen(navigator: DestinationsNavigator) { } ) } - SettingDropdown( + SuperDropdown( icon = Icons.Rounded.RemoveCircle, title = stringResource(id = R.string.settings_disable_kernel_umount), summary = stringResource(id = R.string.settings_disable_kernel_umount_summary), @@ -1077,103 +1077,3 @@ private fun UidScannerSection( ) } } - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SettingDropdown( - icon: ImageVector, - title: String, - summary: String, - items: List, - selectedIndex: Int, - leftAction: (@Composable () -> Unit)? = null, - onSelectedIndexChange: (Int) -> Unit -) { - var showDialog by remember { mutableStateOf(false) } - val selectedItemText = items.getOrNull(selectedIndex) ?: "" - - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { showDialog = true } - .padding(horizontal = SPACING_LARGE, vertical = 12.dp), - verticalAlignment = Alignment.Top - ) { - if (leftAction != null) { - leftAction() - } else { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier - .padding(end = SPACING_LARGE) - .size(24.dp) - ) - } - - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium - ) - Spacer(modifier = Modifier.height(SPACING_SMALL)) - Text( - text = summary, - style = MaterialTheme.typography.bodyMedium - ) - Spacer(modifier = Modifier.height(SPACING_SMALL)) - Text( - text = selectedItemText, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowForward, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(24.dp) - ) - } - - if (showDialog) { - AlertDialog( - onDismissRequest = { showDialog = false }, - title = { Text(text = title) }, - text = { - LazyColumn( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - items(items.size) { index -> - val item = items[index] - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - onSelectedIndexChange(index) - showDialog = false - } - .padding(vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = selectedIndex == index, - onClick = null - ) - Spacer(modifier = Modifier.width(16.dp)) - Text(text = item) - } - } - } - }, - confirmButton = { - TextButton(onClick = { showDialog = false }) { - Text(text = stringResource(android.R.string.cancel)) - } - } - ) - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt index 738ba2ab..00353b2b 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt @@ -1573,7 +1573,9 @@ private fun BasicSettingsContent( onEnableAvcLogSpoofingChange: (Boolean) -> Unit ) { var scriptLocationExpanded by remember { mutableStateOf(false) } - val isAbDevice = isAbDevice() + val isAbDevice = produceState(initialValue = false) { + value = isAbDevice() + }.value val isSusVersion159 = isSusVersion159() Column( @@ -2063,7 +2065,9 @@ private fun SlotInfoDialog( onUseUname: (String) -> Unit, onUseBuildTime: (String) -> Unit ) { - val isAbDevice = isAbDevice() + val isAbDevice = produceState(initialValue = false) { + value = isAbDevice() + }.value if (showDialog && isAbDevice) { AlertDialog( diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt index c16eb8ea..18e3f546 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt @@ -255,6 +255,7 @@ fun installBoot( bootUri: Uri?, lkm: LkmSelection, ota: Boolean, + partition: String?, onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit, @@ -314,6 +315,10 @@ fun installBoot( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) cmd += " -o $downloadsDir" + partition?.let { part -> + cmd += " --partition $part" + } + val result = flashWithIO("${getKsuDaemonPath()} $cmd", onStdout, onStderr) Log.i("KernelSU", "install boot result: ${result.isSuccess}") @@ -344,18 +349,6 @@ fun rootAvailable(): Boolean { return shell.isRoot } -fun isAbDevice(): Boolean { - val shell = getRootShell() - return ShellUtils.fastCmd(shell, "getprop ro.build.ab_update").trim().toBoolean() -} - -fun isInitBoot(): Boolean { - val shell = getRootShell(); - if (shell.isRoot) { - return SuFile("/dev/block/by-name/init_boot").exists() || SuFile("/dev/block/by-name/init_boot_a").exists() - } - return !Os.uname().release.contains("android12-") -} suspend fun getCurrentKmi(): String = withContext(Dispatchers.IO) { val shell = getRootShell() @@ -365,7 +358,36 @@ suspend fun getCurrentKmi(): String = withContext(Dispatchers.IO) { suspend fun getSupportedKmis(): List = withContext(Dispatchers.IO) { val shell = getRootShell() - val cmd = "boot-info supported-kmi" + val cmd = "boot-info supported-kmis" + val out = shell.newJob().add("${getKsuDaemonPath()} $cmd").to(ArrayList(), null).exec().out + out.filter { it.isNotBlank() }.map { it.trim() } +} + +suspend fun isAbDevice(): Boolean = withContext(Dispatchers.IO) { + val shell = getRootShell() + val cmd = "boot-info is-ab-device" + ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd").trim().toBoolean() +} + +suspend fun getDefaultPartition(): String = withContext(Dispatchers.IO) { + val shell = getRootShell() + val cmd = "boot-info default-partition" + ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd").trim() +} + +suspend fun getSlotSuffix(ota: Boolean): String = withContext(Dispatchers.IO) { + val shell = getRootShell() + val cmd = if (ota) { + "boot-info slot-suffix --ota" + } else { + "boot-info slot-suffix" + } + ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd").trim() +} + +suspend fun getAvailablePartitions(): List = withContext(Dispatchers.IO) { + val shell = getRootShell() + val cmd = "boot-info available-partitions" val out = shell.newJob().add("${getKsuDaemonPath()} $cmd").to(ArrayList(), null).exec().out out.filter { it.isNotBlank() }.map { it.trim() } } 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 0cd10fc6..6f8fb32a 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -108,8 +108,9 @@ 直接安装(推荐) 选择一个需要修补的镜像 安装到未使用的槽位(OTA 后) - 将在重启后强制切换到另一个槽位!\n注意只能在 OTA 更新完成后的重启之前使用。\n确认? + 将在重启后强制切换到另一个槽位!注意只能在 OTA 更新完成后的重启之前使用。 下一步 + 选择分区 使用本地 LKM 文件 仅支持选择 .ko 文件 建议选择 %1$s 分区镜像 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 3092da59..de4edc57 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -112,6 +112,7 @@ Install to inactive slot (After OTA) Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue? Next + Select partition Use local LKM file Only .ko files are supported %1$s partition image is recommended diff --git a/userspace/ksud/src/boot_patch.rs b/userspace/ksud/src/boot_patch.rs index b7168582..6c08db4d 100644 --- a/userspace/ksud/src/boot_patch.rs +++ b/userspace/ksud/src/boot_patch.rs @@ -150,126 +150,40 @@ fn parse_kmi_from_boot(magiskboot: &Path, image: &PathBuf, workdir: &Path) -> Re parse_kmi_from_kernel(&image_path, workdir) } -fn do_cpio_cmd(magiskboot: &Path, workdir: &Path, cmd: &str) -> Result<()> { +fn do_cpio_cmd(magiskboot: &Path, workdir: &Path, cpio_path: &Path, cmd: &str) -> Result<()> { let status = Command::new(magiskboot) .current_dir(workdir) .stdout(Stdio::null()) .stderr(Stdio::null()) .arg("cpio") - .arg("ramdisk.cpio") + .arg(cpio_path) .arg(cmd) .status()?; - ensure!(status.success(), "magiskboot cpio {} failed", cmd); Ok(()) } -fn do_vendor_init_boot_cpio_cmd(magiskboot: &Path, workdir: &Path, cmd: &str) -> Result<()> { - let vendor_init_boot_cpio = workdir.join("vendor_ramdisk").join("init_boot.cpio"); +fn is_magisk_patched(magiskboot: &Path, workdir: &Path, cpio_path: &Path) -> Result { let status = Command::new(magiskboot) .current_dir(workdir) .stdout(Stdio::null()) .stderr(Stdio::null()) .arg("cpio") - .arg(vendor_init_boot_cpio) - .arg(cmd) + .arg(cpio_path) + .arg("test") .status()?; - - ensure!(status.success(), "magiskboot cpio {} failed", cmd); - Ok(()) + // 0: stock, 1: magisk + Ok(status.code() == Some(1)) } -fn do_vendor_ramdisk_cpio_cmd(magiskboot: &Path, workdir: &Path, cmd: &str) -> Result<()> { - let vendor_ramdisk_cpio = workdir.join("vendor_ramdisk").join("ramdisk.cpio"); +fn is_kernelsu_patched(magiskboot: &Path, workdir: &Path, cpio_path: &Path) -> Result { let status = Command::new(magiskboot) .current_dir(workdir) .stdout(Stdio::null()) .stderr(Stdio::null()) .arg("cpio") - .arg(vendor_ramdisk_cpio) - .arg(cmd) - .status()?; - - ensure!(status.success(), "magiskboot cpio {} failed", cmd); - Ok(()) -} - -fn is_magisk_patched(magiskboot: &Path, workdir: &Path) -> Result { - let status = Command::new(magiskboot) - .current_dir(workdir) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .args(["cpio", "ramdisk.cpio", "test"]) - .status()?; - - // 0: stock, 1: magisk - Ok(status.code() == Some(1)) -} - -fn is_magisk_patched_vendor_init_boot(magiskboot: &Path, workdir: &Path) -> Result { - let vendor_init_boot_cpio = workdir.join("vendor_ramdisk").join("init_boot.cpio"); - let status = Command::new(magiskboot) - .current_dir(workdir) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .args(["cpio", vendor_init_boot_cpio.to_str().unwrap(), "test"]) - .status()?; - - // 0: stock, 1: magisk - Ok(status.code() == Some(1)) -} - -fn is_magisk_patched_vendor_ramdisk(magiskboot: &Path, workdir: &Path) -> Result { - let vendor_ramdisk_cpio = workdir.join("vendor_ramdisk").join("ramdisk.cpio"); - let status = Command::new(magiskboot) - .current_dir(workdir) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .args(["cpio", vendor_ramdisk_cpio.to_str().unwrap(), "test"]) - .status()?; - - // 0: stock, 1: magisk - Ok(status.code() == Some(1)) -} - -fn is_kernelsu_patched(magiskboot: &Path, workdir: &Path) -> Result { - let status = Command::new(magiskboot) - .current_dir(workdir) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .args(["cpio", "ramdisk.cpio", "exists kernelsu.ko"]) - .status()?; - - Ok(status.success()) -} - -fn is_kernelsu_patched_vendor_init_boot(magiskboot: &Path, workdir: &Path) -> Result { - let vendor_ramdisk_cpio = workdir.join("vendor_ramdisk").join("init_boot.cpio"); - let status = Command::new(magiskboot) - .current_dir(workdir) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .args([ - "cpio", - vendor_ramdisk_cpio.to_str().unwrap(), - "exists kernelsu.ko", - ]) - .status()?; - - Ok(status.success()) -} - -fn is_kernelsu_patched_vendor_ramdisk(magiskboot: &Path, workdir: &Path) -> Result { - let vendor_ramdisk_cpio = workdir.join("vendor_ramdisk").join("ramdisk.cpio"); - let status = Command::new(magiskboot) - .current_dir(workdir) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .args([ - "cpio", - vendor_ramdisk_cpio.to_str().unwrap(), - "exists kernelsu.ko", - ]) + .arg(cpio_path) + .arg("exists kernelsu.ko") .status()?; Ok(status.success()) @@ -305,10 +219,7 @@ pub fn restore( let kmi = get_current_kmi().unwrap_or_else(|_| String::from("")); - let skip_init = kmi.starts_with("android12-"); - - let (bootimage, bootdevice) = - find_boot_image(&image, skip_init, false, false, workdir, &magiskboot)?; + let (bootimage, bootdevice) = find_boot_image(&image, &kmi, false, false, workdir, &None)?; println!("- Unpacking boot image"); let status = Command::new(&magiskboot) @@ -320,33 +231,36 @@ pub fn restore( .status()?; ensure!(status.success(), "magiskboot unpack failed"); - let no_ramdisk = !workdir.join("ramdisk.cpio").exists(); - // let no_ramdisk = !workdir.join("ramdisk.cpio").exists(); - let no_vendor_init_boot = !workdir - .join("vendor_ramdisk") - .join("init_boot.cpio") - .exists(); - let no_vendor_ramdisk = !workdir.join("vendor_ramdisk").join("ramdisk.cpio").exists(); - let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workdir)?; - let is_kernelsu_patched_vendor_init_boot = - is_kernelsu_patched_vendor_init_boot(&magiskboot, workdir)?; - let is_kernelsu_patched_vendor_ramdisk = - is_kernelsu_patched_vendor_ramdisk(&magiskboot, workdir)?; - ensure!( - is_kernelsu_patched - || is_kernelsu_patched_vendor_init_boot - || is_kernelsu_patched_vendor_ramdisk, - "boot image is not patched by KernelSU" - ); + let mut ramdisk = workdir.join("ramdisk.cpio"); + if !ramdisk.exists() { + ramdisk = workdir.join("vendor_ramdisk").join("init_boot.cpio") + } + if !ramdisk.exists() { + ramdisk = workdir.join("vendor_ramdisk").join("ramdisk.cpio"); + } + if !ramdisk.exists() { + bail!("No compatible ramdisk found.") + } + let ramdisk = ramdisk.as_path(); + let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workdir, ramdisk)?; + ensure!(is_kernelsu_patched, "boot image is not patched by KernelSU"); let mut new_boot = None; let mut from_backup = false; #[cfg(target_os = "android")] - if do_cpio_cmd(&magiskboot, workdir, &format!("exists {BACKUP_FILENAME}")).is_ok() { + if do_cpio_cmd( + &magiskboot, + workdir, + ramdisk, + &format!("exists {BACKUP_FILENAME}"), + ) + .is_ok() + { do_cpio_cmd( &magiskboot, workdir, + ramdisk, &format!("extract {BACKUP_FILENAME} {BACKUP_FILENAME}"), )?; let sha = std::fs::read(workdir.join(BACKUP_FILENAME))?; @@ -369,81 +283,13 @@ pub fn restore( } if new_boot.is_none() { - if !no_ramdisk { - println!("- Restoring /ramdisk"); - println!("- Removing /ramdisk/kernelsu.ko"); - // remove kernelsu.ko - do_cpio_cmd(&magiskboot, workdir, "rm kernelsu.ko")?; + // remove kernelsu.ko + do_cpio_cmd(&magiskboot, workdir, ramdisk, "rm kernelsu.ko")?; - // if init.real exists, restore it - println!("- Checking if init.real exists"); - let status = do_cpio_cmd(&magiskboot, workdir, "exists init.real").is_ok(); - if status { - println!("- /ramdisk/init.real exists"); - println!("- Restoring /ramdisk/init.real to init"); - do_cpio_cmd(&magiskboot, workdir, "mv init.real init")?; - } else { - println!("- /ramdisk/init.real not found"); - println!("- Removing ramdisk.cpio"); - let ramdisk = workdir.join("ramdisk.cpio"); - std::fs::remove_file(ramdisk)?; - } - } else if !no_vendor_init_boot { - println!("- Restoring /vendor_ramdisk/init_boot"); - println!("- Removing /vendor_ramdisk/init_boot/kernelsu.ko"); - // vendor init_boot restore - do_vendor_init_boot_cpio_cmd(&magiskboot, workdir, "rm kernelsu.ko")?; - - println!("- Checking if init.real exists"); - let status = - do_vendor_init_boot_cpio_cmd(&magiskboot, workdir, "exists init.real").is_ok(); - if status { - println!("- /vendor_ramdisk/init_boot/init.real exists"); - println!("- Restoring /vendor_ramdisk/init_boot/init.real to init"); - do_vendor_init_boot_cpio_cmd(&magiskboot, workdir, "mv init.real init")?; - } else { - println!("- /vendor_ramdisk/init_boot/init.real not found"); - println!("- Removing vendor_ramdisk/init_boot.cpio"); - let vendor_init_boot = workdir.join("vendor_ramdisk").join("init_boot.cpio"); - std::fs::remove_file(vendor_init_boot)?; - } - } else if !no_vendor_ramdisk { - println!("- Restoring /vendor_ramdisk/ramdisk"); - println!("- Removing /vendor_ramdisk/ramdisk/kernelsu.ko"); - // vendor ramdisk restore - do_vendor_ramdisk_cpio_cmd(&magiskboot, workdir, "rm kernelsu.ko")?; - - let status = - do_vendor_ramdisk_cpio_cmd(&magiskboot, workdir, "exists init.real").is_ok(); - if status { - println!("- /vendor_ramdisk/ramdisk/init.real exists"); - println!("- Restoring /vendor_ramdisk/ramdisk/init.real to init"); - do_vendor_ramdisk_cpio_cmd(&magiskboot, workdir, "mv init.real init")?; - } else { - println!("- /vendor_ramdisk/ramdisk/init.real not found"); - println!("- Removing vendor_ramdisk/ramdisk.cpio"); - let vendor_ramdisk = workdir.join("vendor_ramdisk").join("ramdisk.cpio"); - std::fs::remove_file(vendor_ramdisk)?; - } - } else { - println!("- Restoring /ramdisk"); - println!("- Removing /ramdisk/kernelsu.ko"); - // remove kernelsu.ko - do_cpio_cmd(&magiskboot, workdir, "rm kernelsu.ko")?; - - // if init.real exists, restore it - println!("- Checking if init.real exists"); - let status = do_cpio_cmd(&magiskboot, workdir, "exists init.real").is_ok(); - if status { - println!("- /ramdisk/init.real exists"); - println!("- Restoring /ramdisk/init.real to init"); - do_cpio_cmd(&magiskboot, workdir, "mv init.real init")?; - } else { - println!("- /ramdisk/init.real not found"); - println!("- Removing ramdisk.cpio"); - let ramdisk = workdir.join("ramdisk.cpio"); - std::fs::remove_file(ramdisk)?; - } + // if init.real exists, restore it + let status = do_cpio_cmd(&magiskboot, workdir, ramdisk, "exists init.real").is_ok(); + if status { + do_cpio_cmd(&magiskboot, workdir, ramdisk, "mv init.real init")?; } println!("- Repacking boot image"); @@ -452,7 +298,7 @@ pub fn restore( .stdout(Stdio::null()) .stderr(Stdio::null()) .arg("repack") - .arg(bootimage.display().to_string()) + .arg(&bootimage) .status()?; ensure!(status.success(), "magiskboot repack failed"); new_boot = Some(workdir.join("new-boot.img")); @@ -498,8 +344,11 @@ pub fn patch( out: Option, magiskboot: Option, kmi: Option, + partition: Option, ) -> Result<()> { - let result = do_patch(image, kernel, kmod, init, ota, flash, out, magiskboot, kmi); + let result = do_patch( + image, kernel, kmod, init, ota, flash, out, magiskboot, kmi, partition, + ); if let Err(ref e) = result { println!("- Install Error: {e}"); } @@ -517,6 +366,7 @@ fn do_patch( out: Option, magiskboot_path: Option, kmi: Option, + partition: Option, ) -> Result<()> { println!(include_str!("banner")); @@ -571,18 +421,10 @@ fn do_patch( } }; - let skip_init = kmi.starts_with("android12-"); + let (bootimage, bootdevice) = + find_boot_image(&image, &kmi, ota, is_replace_kernel, workdir, &partition)?; - let (bootimage, bootdevice) = find_boot_image( - &image, - skip_init, - ota, - is_replace_kernel, - workdir, - &magiskboot, - )?; - - let bootimage = bootimage.display().to_string(); + let bootimage = bootimage.as_path(); // try extract magiskboot/bootctl let _ = assets::ensure_binaries(false); @@ -617,98 +459,48 @@ fn do_patch( .stdout(Stdio::null()) .stderr(Stdio::null()) .arg("unpack") - .arg(&bootimage) + .arg(bootimage) .status()?; ensure!(status.success(), "magiskboot unpack failed"); - let no_ramdisk = !workdir.join("ramdisk.cpio").exists(); - let no_vendor_init_boot = !workdir - .join("vendor_ramdisk") - .join("init_boot.cpio") - .exists(); - let no_vendor_ramdisk = !workdir.join("vendor_ramdisk").join("ramdisk.cpio").exists(); - if no_ramdisk && no_vendor_init_boot && no_vendor_ramdisk { - println!("- No compatible ramdisk found."); - println!("- Will create our own ramdisk!"); + let mut ramdisk = workdir.join("ramdisk.cpio"); + if !ramdisk.exists() { + ramdisk = workdir.join("vendor_ramdisk").join("init_boot.cpio") } - let is_magisk_patched = is_magisk_patched(&magiskboot, workdir)?; - let is_magisk_patched_vendor_init_boot = - is_magisk_patched_vendor_init_boot(&magiskboot, workdir)?; - let is_magisk_patched_vendor_ramdisk = is_magisk_patched_vendor_ramdisk(&magiskboot, workdir)?; - ensure!( - !is_magisk_patched - || !is_magisk_patched_vendor_init_boot - || !is_magisk_patched_vendor_ramdisk, - "Cannot work with Magisk patched image" - ); + if !ramdisk.exists() { + ramdisk = workdir.join("vendor_ramdisk").join("ramdisk.cpio"); + } + if !ramdisk.exists() { + println!("- No ramdisk, create by default"); + ramdisk = "ramdisk.cpio".into(); + } + let ramdisk = ramdisk.as_path(); + let is_magisk_patched = is_magisk_patched(&magiskboot, workdir, ramdisk)?; + ensure!(!is_magisk_patched, "Cannot work with Magisk patched image"); println!("- Adding KernelSU LKM"); - let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workdir)?; - let is_kernelsu_patched_vendor_init_boot = - is_kernelsu_patched_vendor_init_boot(&magiskboot, workdir)?; - let is_kernelsu_patched_vendor_ramdisk = - is_kernelsu_patched_vendor_ramdisk(&magiskboot, workdir)?; + let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workdir, ramdisk)?; let mut need_backup = false; - if (no_ramdisk && !is_kernelsu_patched_vendor_init_boot) - || (no_ramdisk && no_vendor_init_boot && !is_kernelsu_patched_vendor_ramdisk) - || !is_kernelsu_patched - { - if !no_ramdisk { - println!("- Checking if /ramdisk/init exists"); - let status = do_cpio_cmd(&magiskboot, workdir, "exists init"); - if status.is_ok() { - println!("- Backing up ramdisk/init"); - do_cpio_cmd(&magiskboot, workdir, "mv init init.real")?; - } - need_backup = flash; - } else if !no_vendor_init_boot { - println!("- Checking if /vendor_ramdisk/init_boot/init exists"); - let status = do_vendor_init_boot_cpio_cmd(&magiskboot, workdir, "exists init"); - if status.is_ok() { - println!("- Backing up vendor_ramdisk/init_boot/init"); - do_vendor_init_boot_cpio_cmd(&magiskboot, workdir, "mv init init.real")?; - } - need_backup = flash; - } else if !no_vendor_ramdisk { - println!("- Checking if /vendor_ramdisk/ramdisk/init exists"); - let status = do_vendor_ramdisk_cpio_cmd(&magiskboot, workdir, "exists init"); - if status.is_ok() { - println!("- Backing up vendor_ramdisk/ramdisk/init"); - do_vendor_ramdisk_cpio_cmd(&magiskboot, workdir, "mv init init.real")?; - } - need_backup = flash; - } else { - println!("- Checking if /ramdisk/init exists"); - let status = do_cpio_cmd(&magiskboot, workdir, "exists init"); - if status.is_ok() { - println!("- Backing up ramdisk/init"); - do_cpio_cmd(&magiskboot, workdir, "mv init init.real")?; - } - need_backup = flash; + if !is_kernelsu_patched { + // kernelsu.ko is not exist, backup init if necessary + let status = do_cpio_cmd(&magiskboot, workdir, ramdisk, "exists init"); + if status.is_ok() { + do_cpio_cmd(&magiskboot, workdir, ramdisk, "mv init init.real")?; } + need_backup = flash; } - if !no_ramdisk { - println!("- Patching /ramdisk"); - do_cpio_cmd(&magiskboot, workdir, "add 0755 init init")?; - do_cpio_cmd(&magiskboot, workdir, "add 0755 kernelsu.ko kernelsu.ko")?; - } else if !no_vendor_init_boot { - println!("- Patching /vendor_ramdisk/init_boot"); - do_vendor_init_boot_cpio_cmd(&magiskboot, workdir, "add 0755 init init")?; - do_vendor_init_boot_cpio_cmd(&magiskboot, workdir, "add 0755 kernelsu.ko kernelsu.ko")?; - } else if !no_vendor_ramdisk { - println!("- Patching /vendor_ramdisk/ramdisk"); - do_vendor_ramdisk_cpio_cmd(&magiskboot, workdir, "add 0750 init init")?; - do_vendor_ramdisk_cpio_cmd(&magiskboot, workdir, "add 0750 kernelsu.ko kernelsu.ko")?; - } else { - println!("- Creating and Patching /ramdisk"); - do_cpio_cmd(&magiskboot, workdir, "add 0755 init init")?; - do_cpio_cmd(&magiskboot, workdir, "add 0755 kernelsu.ko kernelsu.ko")?; - } + do_cpio_cmd(&magiskboot, workdir, ramdisk, "add 0755 init init")?; + do_cpio_cmd( + &magiskboot, + workdir, + ramdisk, + "add 0755 kernelsu.ko kernelsu.ko", + )?; #[cfg(target_os = "android")] - if need_backup && let Err(e) = do_backup(&magiskboot, workdir, &bootimage) { + if need_backup && let Err(e) = do_backup(&magiskboot, workdir, ramdisk, bootimage) { println!("- Backup stock image failed: {e}"); } @@ -719,7 +511,7 @@ fn do_patch( .stdout(Stdio::null()) .stderr(Stdio::null()) .arg("repack") - .arg(&bootimage) + .arg(bootimage) .status()?; ensure!(status.success(), "magiskboot repack failed"); let new_boot = workdir.join("new-boot.img"); @@ -774,7 +566,7 @@ fn calculate_sha1(file_path: impl AsRef) -> Result { } #[cfg(target_os = "android")] -fn do_backup(magiskboot: &Path, workdir: &Path, image: &str) -> Result<()> { +fn do_backup(magiskboot: &Path, workdir: &Path, cpio_path: &Path, image: &Path) -> Result<()> { let sha1 = calculate_sha1(image)?; let filename = format!("{KSU_BACKUP_FILE_PREFIX}{sha1}"); @@ -786,6 +578,7 @@ fn do_backup(magiskboot: &Path, workdir: &Path, image: &str) -> Result<()> { do_cpio_cmd( magiskboot, workdir, + cpio_path, &format!("add 0755 {BACKUP_FILENAME} {BACKUP_FILENAME}"), )?; println!("- Stock image has been backup to"); @@ -855,141 +648,117 @@ fn find_magiskboot(magiskboot_path: Option, workdir: &Path) -> Result

, - skip_init: bool, + kmi: &str, ota: bool, is_replace_kernel: bool, workdir: &Path, - magiskboot: &Path, + partition: &Option, ) -> Result<(PathBuf, Option)> { let bootimage; let mut bootdevice = None; if let Some(ref image) = *image { - ensure!(image.exists(), "- Boot image not found"); + ensure!(image.exists(), "boot image not found"); bootimage = std::fs::canonicalize(image)?; } else { if cfg!(not(target_os = "android")) { println!("- Current OS is not android, refusing auto bootimage/bootdevice detection"); - bail!("- Please specify a boot image"); - } - let mut slot_suffix = - utils::getprop("ro.boot.slot_suffix").unwrap_or_else(|| String::from("")); - - if !slot_suffix.is_empty() && ota { - if slot_suffix == "_a" { - slot_suffix = "_b".to_string() - } else { - slot_suffix = "_a".to_string() - } - }; - - let init_boot_partition = format!("/dev/block/by-name/init_boot{slot_suffix}"); - let vendor_boot_partition = format!("/dev/block/by-name/vendor_boot{slot_suffix}"); - let boot_partition = format!("/dev/block/by-name/boot{slot_suffix}"); - - let init_boot_exist = Path::new(&init_boot_partition).exists(); - let vendor_boot_exist = Path::new(&vendor_boot_partition).exists(); - - // helper: unpack a partition and check for a ramdisk and init - fn unpack_and_check_init( - magiskboot: &Path, - workdir: &Path, - partition: &str, - ramdisk_cpio: &str, - ) -> Result { - let tmp_img = workdir.join("probe.img"); - dd(partition, &tmp_img)?; - let status = Command::new(magiskboot) - .current_dir(workdir) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .arg("unpack") - .arg(&tmp_img) - .status()?; - if !status.success() { - let _ = std::fs::remove_file(&tmp_img); - return Ok(false); - } - let ramdisk_path = workdir.join(ramdisk_cpio); - let has_init = if ramdisk_path.exists() { - Command::new(magiskboot) - .current_dir(workdir) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .arg("cpio") - .arg(ramdisk_cpio) - .arg("exists init") - .status() - .map(|s| s.success()) - .unwrap_or(false) - } else { - false - }; - // Clean up - let _ = std::fs::remove_file(&tmp_img); - let _ = std::fs::remove_file(workdir.join("ramdisk.cpio")); - let _ = std::fs::remove_dir_all(workdir.join("vendor_ramdisk")); - Ok(has_init) + bail!("Please specify a boot image"); } - let mut selected_partition = &boot_partition; + let slot_suffix = get_slot_suffix(ota); + let boot_partition_name = choose_boot_partition(kmi, is_replace_kernel, partition); + let boot_partition = format!("/dev/block/by-name/{boot_partition_name}{slot_suffix}"); - if !is_replace_kernel && init_boot_exist && !skip_init { - // try init_boot/ramdisk.cpio - if unpack_and_check_init(magiskboot, workdir, &init_boot_partition, "ramdisk.cpio")? { - println!("- Using init_boot partition (ramdisk.cpio)."); - selected_partition = &init_boot_partition; - } - } - - // try vendor_boot/vendor_ramdisk/init_boot.cpio - if selected_partition == &boot_partition - && !is_replace_kernel - && vendor_boot_exist - && !skip_init - && unpack_and_check_init( - magiskboot, - workdir, - &vendor_boot_partition, - "vendor_ramdisk/init_boot.cpio", - )? - { - println!("- Using vendor_boot partition (vendor_ramdisk/init_boot.cpio)."); - selected_partition = &vendor_boot_partition; - } - - // try vendor_boot/vendor_ramdisk/ramdisk.cpio - if selected_partition == &boot_partition - && !is_replace_kernel - && vendor_boot_exist - && !skip_init - && unpack_and_check_init( - magiskboot, - workdir, - &vendor_boot_partition, - "vendor_ramdisk/ramdisk.cpio", - )? - { - println!("- Using vendor_boot partition (vendor_ramdisk/ramdisk.cpio)."); - selected_partition = &vendor_boot_partition; - } - - if selected_partition == &boot_partition { - println!("- Using boot partition (ramdisk.cpio)."); - } - - println!("- Bootdevice: {selected_partition}"); + println!("- Bootdevice: {boot_partition}"); let tmp_boot_path = workdir.join("boot.img"); - dd(selected_partition, &tmp_boot_path)?; + dd(&boot_partition, &tmp_boot_path)?; - ensure!(tmp_boot_path.exists(), "- Tmp boot image not found"); + ensure!(tmp_boot_path.exists(), "boot image not found"); bootimage = tmp_boot_path; - bootdevice = Some(selected_partition.to_string()); + bootdevice = Some(boot_partition); }; Ok((bootimage, bootdevice)) } +#[cfg(target_os = "android")] +pub fn choose_boot_partition( + kmi: &str, + is_replace_kernel: bool, + partition: &Option, +) -> String { + let slot_suffix = get_slot_suffix(false); + let skip_init_boot = kmi.starts_with("android12-"); + + let init_boot_exist = Path::new(&format!("/dev/block/by-name/init_boot{slot_suffix}")).exists(); + let vendor_boot_exist = + Path::new(&format!("/dev/block/by-name/vendor_boot{slot_suffix}")).exists(); + + // if specific partition is specified, use it + if let Some(part) = partition { + return match part.as_str() { + "boot" | "init_boot" | "vendor_boot" => part.clone(), + _ => "boot".to_string(), + }; + } + + // if init_boot exists and not skipping it, use it + if !is_replace_kernel && init_boot_exist && !skip_init_boot { + return "init_boot".to_string(); + } + + // if vendor_boot exists and not skipping it, use it + if !is_replace_kernel && vendor_boot_exist && !skip_init_boot { + return "vendor_boot".to_string(); + } + + "boot".to_string() +} + +#[cfg(not(target_os = "android"))] +pub fn choose_boot_partition( + _kmi: &str, + _is_replace_kernel: bool, + _partition: &Option, +) -> String { + "boot".to_string() +} + +#[cfg(target_os = "android")] +pub fn get_slot_suffix(ota: bool) -> String { + let mut slot_suffix = utils::getprop("ro.boot.slot_suffix").unwrap_or_else(|| String::from("")); + if !slot_suffix.is_empty() && ota { + if slot_suffix == "_a" { + slot_suffix = "_b".to_string() + } else { + slot_suffix = "_a".to_string() + } + } + slot_suffix +} + +#[cfg(not(target_os = "android"))] +pub fn get_slot_suffix(_ota: bool) -> String { + String::new() +} + +#[cfg(target_os = "android")] +pub fn list_available_partitions() -> Vec { + let slot_suffix = get_slot_suffix(false); + let candidates = vec!["boot", "init_boot", "vendor_boot"]; + candidates + .into_iter() + .filter(|name| Path::new(&format!("/dev/block/by-name/{}{}", name, slot_suffix)).exists()) + .map(|s| s.to_string()) + .collect() +} + +#[cfg(not(target_os = "android"))] +pub fn list_available_partitions() -> Vec { + Vec::new() +} + fn post_ota() -> Result<()> { use crate::defs::ADB_DIR; use assets::BOOTCTL_PATH; diff --git a/userspace/ksud/src/cli.rs b/userspace/ksud/src/cli.rs index e58bf3ab..53b21f65 100644 --- a/userspace/ksud/src/cli.rs +++ b/userspace/ksud/src/cli.rs @@ -107,6 +107,10 @@ enum Commands { /// KMI version, if specified, will use the specified KMI #[arg(long, default_value = None)] kmi: Option, + + /// target partition override (init_boot | boot | vendor_boot) + #[arg(long, default_value = None)] + partition: Option, }, /// Restore boot or init_boot images patched by KernelSU @@ -156,7 +160,23 @@ enum BootInfo { CurrentKmi, /// show supported kmi versions - SupportedKmi, + SupportedKmis, + + /// check if device is A/B capable + IsAbDevice, + + /// show auto-selected boot partition name + DefaultPartition, + + /// list available partitions for current or OTA toggled slot + AvailablePartitions, + + /// show slot suffix for current or OTA toggled slot + SlotSuffix { + /// toggle to another slot + #[arg(short = 'u', long, default_value = "false")] + ota: bool, + }, } #[derive(clap::Subcommand, Debug)] @@ -524,7 +544,10 @@ pub fn run() -> Result<()> { out, magiskboot, kmi, - } => crate::boot_patch::patch(boot, kernel, module, init, ota, flash, out, magiskboot, kmi), + partition, + } => crate::boot_patch::patch( + boot, kernel, module, init, ota, flash, out, magiskboot, kmi, partition, + ), Commands::BootInfo { command } => match command { BootInfo::CurrentKmi => { @@ -533,11 +556,34 @@ pub fn run() -> Result<()> { // return here to avoid printing the error message return Ok(()); } - BootInfo::SupportedKmi => { + BootInfo::SupportedKmis => { let kmi = crate::assets::list_supported_kmi()?; kmi.iter().for_each(|kmi| println!("{kmi}")); return Ok(()); } + BootInfo::IsAbDevice => { + let val = crate::utils::getprop("ro.build.ab_update") + .unwrap_or_else(|| String::from("false")); + let is_ab = val.trim().to_lowercase() == "true"; + println!("{}", if is_ab { "true" } else { "false" }); + return Ok(()); + } + BootInfo::DefaultPartition => { + let kmi = crate::boot_patch::get_current_kmi().unwrap_or_else(|_| String::from("")); + let name = crate::boot_patch::choose_boot_partition(&kmi, false, &None); + println!("{name}"); + return Ok(()); + } + BootInfo::SlotSuffix { ota } => { + let suffix = crate::boot_patch::get_slot_suffix(ota); + println!("{suffix}"); + return Ok(()); + } + BootInfo::AvailablePartitions => { + let parts = crate::boot_patch::list_available_partitions(); + parts.iter().for_each(|p| println!("{p}")); + return Ok(()); + } }, Commands::BootRestore { boot,