diff --git a/manager/app/src/main/AndroidManifest.xml b/manager/app/src/main/AndroidManifest.xml
index 025709cc..a5efbef3 100644
--- a/manager/app/src/main/AndroidManifest.xml
+++ b/manager/app/src/main/AndroidManifest.xml
@@ -25,6 +25,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
(DialogState.None) }
+ var selectedSlot by remember { mutableStateOf(null) }
+ var kpmPatchOption by remember { mutableStateOf(KpmPatchOption.FOLLOW_KERNEL) }
+
+ LaunchedEffect(intent) {
+ if (intent == null || processed) return@LaunchedEffect
+
+ val zipUris = mutableSetOf()
+
+ fun isModuleFile(uri: Uri?): Boolean {
+ if (uri == null) return false
+ val uriString = uri.toString()
+ return uriString.endsWith(".zip", ignoreCase = true) ||
+ uriString.endsWith(".apk", ignoreCase = true)
+ }
+
+ when (intent.action) {
+ Intent.ACTION_VIEW, Intent.ACTION_SEND -> {
+ val data = intent.data
+ val stream = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
+ intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
+ } else {
+ @Suppress("DEPRECATION")
+ intent.getParcelableExtra(Intent.EXTRA_STREAM)
+ }
+
+ when {
+ isModuleFile(data) -> {
+ zipUris.add(data!!)
+ }
+ isModuleFile(stream) -> {
+ zipUris.add(stream!!)
+ }
+ }
+ }
+ Intent.ACTION_SEND_MULTIPLE -> {
+ val streamList = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
+ intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java)
+ } else {
+ @Suppress("DEPRECATION")
+ intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
+ }
+ streamList?.forEach { uri ->
+ if (isModuleFile(uri)) {
+ zipUris.add(uri)
+ }
+ }
+ }
+ }
+
+ intent.clipData?.let { clipData ->
+ for (i in 0 until clipData.itemCount) {
+ clipData.getItemAt(i)?.uri?.let { uri ->
+ if (isModuleFile(uri)) {
+ zipUris.add(uri)
+ }
+ }
+ }
+ }
+
+ if (zipUris.isNotEmpty()) {
+ processed = true
+
+ val zipUrisList = zipUris.toList()
+
+ // 检测 zip 文件类型
+ val zipTypes = withContext(Dispatchers.IO) {
+ zipUrisList.map { uri -> detectZipType(context, uri) }
+ }
+
+ val moduleUris = zipUrisList.filterIndexed { index, _ -> zipTypes[index] == ZipType.MODULE }
+ val kernelUris = zipUrisList.filterIndexed { index, _ -> zipTypes[index] == ZipType.KERNEL }
+ val unknownUris = zipUrisList.filterIndexed { index, _ -> zipTypes[index] == ZipType.UNKNOWN }
+
+ val finalModuleUris = moduleUris + unknownUris
+
+ val fileNames = zipUrisList.mapIndexed { index, uri ->
+ val fileName = uri.getFileName(context) ?: context.getString(R.string.zip_file_unknown)
+ val type = when (zipTypes[index]) {
+ ZipType.MODULE -> context.getString(R.string.zip_type_module)
+ ZipType.KERNEL -> context.getString(R.string.zip_type_kernel)
+ ZipType.UNKNOWN -> context.getString(R.string.zip_type_unknown)
+ }
+ "\n${index + 1}. $fileName$type"
+ }.joinToString("")
+
+ val confirmContent = when {
+ moduleUris.isNotEmpty() && kernelUris.isNotEmpty() -> {
+ context.getString(R.string.mixed_install_prompt_with_name, fileNames)
+ }
+ kernelUris.isNotEmpty() -> {
+ context.getString(R.string.kernel_install_prompt_with_name, fileNames)
+ }
+ else -> {
+ context.getString(R.string.module_install_prompt_with_name, fileNames)
+ }
+ }
+
+ val confirmTitle = if (kernelUris.isNotEmpty() && moduleUris.isEmpty()) {
+ context.getString(R.string.horizon_kernel)
+ } else {
+ context.getString(R.string.module)
+ }
+
+ val result = confirmDialog.awaitConfirm(
+ title = confirmTitle,
+ content = confirmContent
+ )
+
+ if (result == ConfirmResult.Confirmed) {
+ if (finalModuleUris.isNotEmpty()) {
+ navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(finalModuleUris))) {
+ launchSingleTop = true
+ }
+ }
+
+ // 处理内核安装
+ if (kernelUris.isNotEmpty()) {
+ val kernelUri = kernelUris.first()
+ val isAbDeviceValue = withContext(Dispatchers.IO) { isAbDevice() }
+ dialogState = if (isAbDeviceValue) {
+ // AB设备:先选择槽位
+ DialogState.SlotSelection(kernelUri)
+ } else {
+ // 非AB设备:直接选择KPM
+ DialogState.KpmSelection(kernelUri, null)
+ }
+ }
+ }
+ }
+ }
+
+ // 槽位选择
+ when (val state = dialogState) {
+ is DialogState.SlotSelection -> {
+ SlotSelectionDialog(
+ show = true,
+ onDismiss = {
+ dialogState = DialogState.None
+ selectedSlot = null
+ kpmPatchOption = KpmPatchOption.FOLLOW_KERNEL
+ },
+ onSlotSelected = { slot ->
+ selectedSlot = slot
+ dialogState = DialogState.None
+ scope.launch {
+ delay(300)
+ dialogState = DialogState.KpmSelection(state.kernelUri, slot)
+ }
+ }
+ )
+ }
+ is DialogState.KpmSelection -> {
+ KpmPatchSelectionDialog(
+ show = true,
+ currentOption = kpmPatchOption,
+ onDismiss = {
+ dialogState = DialogState.None
+ selectedSlot = null
+ kpmPatchOption = KpmPatchOption.FOLLOW_KERNEL
+ },
+ onOptionSelected = { option ->
+ kpmPatchOption = option
+ dialogState = DialogState.None
+
+ navigator.navigate(
+ KernelFlashScreenDestination(
+ kernelUri = state.kernelUri,
+ selectedSlot = state.slot,
+ kpmPatchEnabled = option == KpmPatchOption.PATCH_KPM,
+ kpmUndoPatch = option == KpmPatchOption.UNDO_PATCH_KPM
+ )
+ ) {
+ launchSingleTop = true
+ }
+
+ selectedSlot = null
+ kpmPatchOption = KpmPatchOption.FOLLOW_KERNEL
+ }
+ )
+ }
+ is DialogState.None -> {
+ }
+ }
+}
+
+enum class ZipType {
+ MODULE,
+ KERNEL,
+ UNKNOWN
+}
+
+fun detectZipType(context: Context, uri: Uri): ZipType {
+ // 首先检查文件扩展名,APK 文件可能是模块
+ val uriString = uri.toString().lowercase()
+ val isApk = uriString.endsWith(".apk", ignoreCase = true)
+
+ return try {
+ context.contentResolver.openInputStream(uri)?.use { inputStream ->
+ java.util.zip.ZipInputStream(inputStream).use { zipStream ->
+ var hasModuleProp = false
+ var hasToolsFolder = false
+ var hasAnykernelSh = false
+
+ var entry = zipStream.nextEntry
+ while (entry != null) {
+ val entryName = entry.name.lowercase()
+
+ when {
+ entryName == "module.prop" || entryName.endsWith("/module.prop") -> {
+ hasModuleProp = true
+ }
+ entryName.startsWith("tools/") || entryName == "tools" -> {
+ hasToolsFolder = true
+ }
+ entryName == "anykernel.sh" || entryName.endsWith("/anykernel.sh") -> {
+ hasAnykernelSh = true
+ }
+ }
+
+ zipStream.closeEntry()
+ entry = zipStream.nextEntry
+ }
+
+ when {
+ hasModuleProp -> ZipType.MODULE
+ hasToolsFolder && hasAnykernelSh -> ZipType.KERNEL
+ // APK 文件如果没有检测到其他类型,默认当作模块处理
+ isApk -> ZipType.MODULE
+ else -> ZipType.UNKNOWN
+ }
+ }
+ } ?: run {
+ // 如果无法打开文件流,APK 文件默认当作模块处理
+ if (isApk) ZipType.MODULE else ZipType.UNKNOWN
+ }
+ } catch (e: java.io.IOException) {
+ e.printStackTrace()
+ // 如果是 APK 文件但读取失败,仍然当作模块处理
+ if (isApk) ZipType.MODULE else ZipType.UNKNOWN
+ }
+}
diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/kernelFlash/AnyKernel3Flow.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/kernelFlash/AnyKernel3Flow.kt
index cc55ff78..ff02de4f 100644
--- a/manager/app/src/main/java/com/sukisu/ultra/ui/kernelFlash/AnyKernel3Flow.kt
+++ b/manager/app/src/main/java/com/sukisu/ultra/ui/kernelFlash/AnyKernel3Flow.kt
@@ -51,7 +51,9 @@ data class AnyKernel3State(
val onSlotSelected: (String) -> Unit,
val onDismissSlotDialog: () -> Unit,
val onOptionSelected: (KpmPatchOption) -> Unit,
- val onDismissPatchDialog: () -> Unit
+ val onDismissPatchDialog: () -> Unit,
+ val onReopenSlotDialog: (InstallMethod.HorizonKernel) -> Unit,
+ val onReopenKpmDialog: (InstallMethod.HorizonKernel) -> Unit
)
@Composable
@@ -69,7 +71,7 @@ fun rememberAnyKernel3State(
val onHorizonKernelSelected: (InstallMethod.HorizonKernel) -> Unit = { method ->
val uri = method.uri
if (uri != null) {
- if (isAbDevice) {
+ if (isAbDevice && method.slot == null) {
tempKernelUri = uri
showSlotSelectionDialog = true
} else {
@@ -78,9 +80,22 @@ fun rememberAnyKernel3State(
}
}
}
+
+ val onReopenSlotDialog: (InstallMethod.HorizonKernel) -> Unit = { method ->
+ val uri = method.uri
+ if (uri != null && isAbDevice) {
+ tempKernelUri = uri
+ showSlotSelectionDialog = true
+ }
+ }
+
+ val onReopenKpmDialog: (InstallMethod.HorizonKernel) -> Unit = { method ->
+ installMethodState.value = method
+ showKpmPatchDialog = true
+ }
val onSlotSelected: (String) -> Unit = { slot ->
- val uri = tempKernelUri
+ val uri = tempKernelUri ?: (installMethodState.value as? InstallMethod.HorizonKernel)?.uri
if (uri != null) {
installMethodState.value = InstallMethod.HorizonKernel(
uri = uri,
@@ -95,7 +110,6 @@ fun rememberAnyKernel3State(
val onDismissSlotDialog = {
showSlotSelectionDialog = false
- tempKernelUri = null
}
val onOptionSelected: (KpmPatchOption) -> Unit = { option ->
@@ -135,7 +149,9 @@ fun rememberAnyKernel3State(
onSlotSelected = onSlotSelected,
onDismissSlotDialog = onDismissSlotDialog,
onOptionSelected = onOptionSelected,
- onDismissPatchDialog = onDismissPatchDialog
+ onDismissPatchDialog = onDismissPatchDialog,
+ onReopenSlotDialog = onReopenSlotDialog,
+ onReopenKpmDialog = onReopenKpmDialog
)
}
diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/kernelFlash/KernelFlash.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/kernelFlash/KernelFlash.kt
index d93f2fb7..aaa94e02 100644
--- a/manager/app/src/main/java/com/sukisu/ultra/ui/kernelFlash/KernelFlash.kt
+++ b/manager/app/src/main/java/com/sukisu/ultra/ui/kernelFlash/KernelFlash.kt
@@ -1,15 +1,14 @@
package com.sukisu.ultra.ui.kernelFlash
-import android.content.Context
+import android.content.Intent
import android.net.Uri
import android.os.Environment
-import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
+import androidx.activity.compose.LocalActivity
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
@@ -19,7 +18,6 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.platform.LocalContext
@@ -27,7 +25,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
-import androidx.core.content.edit
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -43,12 +40,14 @@ import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.FloatingActionButton
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.IconButton
+import top.yukonga.miuix.kmp.basic.LinearProgressIndicator
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.SmallTopAppBar
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.icons.useful.Back
import top.yukonga.miuix.kmp.icon.icons.useful.Save
+import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
import top.yukonga.miuix.kmp.utils.scrollEndHaptic
import java.io.File
@@ -66,6 +65,15 @@ private object KernelFlashStateHolder {
var currentKpmPatchEnabled: Boolean = false
var currentKpmUndoPatch: Boolean = false
var isFlashing = false
+
+ fun clear() {
+ currentState = null
+ currentUri = null
+ currentSlot = null
+ currentKpmPatchEnabled = false
+ currentKpmUndoPatch = false
+ isFlashing = false
+ }
}
@Destination
@@ -78,12 +86,6 @@ fun KernelFlashScreen(
kpmUndoPatch: Boolean = false
) {
val context = LocalContext.current
-
- val shouldAutoExit = remember {
- val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE)
- sharedPref.getBoolean("auto_exit_after_flash", false)
- }
-
val scrollState = rememberScrollState()
val scope = rememberCoroutineScope()
var logText by rememberSaveable { mutableStateOf("") }
@@ -109,17 +111,27 @@ fun KernelFlashScreen(
}
val flashState by horizonKernelState.state.collectAsState()
+ val activity = LocalActivity.current
val onFlashComplete = {
showFloatAction = true
KernelFlashStateHolder.isFlashing = false
+ }
+
+ // 如果是从外部打开的内核刷写,延迟1.5秒后自动退出
+ LaunchedEffect(flashState.isCompleted, flashState.error) {
+ if (flashState.isCompleted && flashState.error.isEmpty()) {
+ val intent = activity?.intent
+ val isFromExternalIntent = intent?.action?.let { action ->
+ action == Intent.ACTION_VIEW ||
+ action == Intent.ACTION_SEND ||
+ action == Intent.ACTION_SEND_MULTIPLE
+ } ?: false
- if (shouldAutoExit) {
- scope.launch {
+ if (isFromExternalIntent) {
delay(1500)
- val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE)
- sharedPref.edit { remove("auto_exit_after_flash") }
- (context as? ComponentActivity)?.finish()
+ KernelFlashStateHolder.clear()
+ activity.finish()
}
}
}
@@ -170,26 +182,17 @@ fun KernelFlashScreen(
val onBack: () -> Unit = {
if (!flashState.isFlashing || flashState.isCompleted || flashState.error.isNotEmpty()) {
if (flashState.isCompleted || flashState.error.isNotEmpty()) {
- KernelFlashStateHolder.currentState = null
- KernelFlashStateHolder.currentUri = null
- KernelFlashStateHolder.currentSlot = null
- KernelFlashStateHolder.currentKpmPatchEnabled = false
- KernelFlashStateHolder.currentKpmUndoPatch = false
- KernelFlashStateHolder.isFlashing = false
+ KernelFlashStateHolder.clear()
}
navigator.popBackStack()
}
}
- DisposableEffect(shouldAutoExit) {
+ // 清理状态
+ DisposableEffect(Unit) {
onDispose {
- if (shouldAutoExit) {
- KernelFlashStateHolder.currentState = null
- KernelFlashStateHolder.currentUri = null
- KernelFlashStateHolder.currentSlot = null
- KernelFlashStateHolder.currentKpmPatchEnabled = false
- KernelFlashStateHolder.currentKpmUndoPatch = false
- KernelFlashStateHolder.isFlashing = false
+ if (flashState.isCompleted || flashState.error.isNotEmpty()) {
+ KernelFlashStateHolder.clear()
}
}
}
@@ -274,14 +277,14 @@ private fun FlashProgressIndicator(
kpmPatchEnabled: Boolean = false,
kpmUndoPatch: Boolean = false
) {
- val progressColor = when {
+ val statusColor = when {
flashState.error.isNotEmpty() -> colorScheme.error
- flashState.isCompleted -> colorScheme.secondary
+ flashState.isCompleted -> colorScheme.primary
else -> colorScheme.primary
}
val progress = animateFloatAsState(
- targetValue = flashState.progress,
+ targetValue = flashState.progress.coerceIn(0f, 1f),
label = "FlashProgress"
)
@@ -306,8 +309,9 @@ private fun FlashProgressIndicator(
flashState.isCompleted -> stringResource(R.string.flash_success)
else -> stringResource(R.string.flashing)
},
- fontWeight = FontWeight.Bold,
- color = progressColor
+ fontSize = MiuixTheme.textStyles.title4.fontSize,
+ fontWeight = FontWeight.Medium,
+ color = statusColor
)
when {
@@ -322,7 +326,7 @@ private fun FlashProgressIndicator(
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = null,
- tint = colorScheme.secondary
+ tint = colorScheme.primary
)
}
}
@@ -330,67 +334,44 @@ private fun FlashProgressIndicator(
// KPM状态显示
if (kpmPatchEnabled || kpmUndoPatch) {
- Spacer(modifier = Modifier.height(4.dp))
+ Spacer(modifier = Modifier.height(8.dp))
Text(
text = if (kpmUndoPatch) stringResource(R.string.kpm_undo_patch_mode)
else stringResource(R.string.kpm_patch_mode),
- color = colorScheme.secondary
- )
- }
-
- Spacer(modifier = Modifier.height(8.dp))
-
- if (flashState.currentStep.isNotEmpty()) {
- Text(
- text = flashState.currentStep,
+ fontSize = MiuixTheme.textStyles.body2.fontSize,
color = colorScheme.onSurfaceVariantSummary
)
-
- Spacer(modifier = Modifier.height(8.dp))
}
- val progressFraction = progress.value.coerceIn(0f, 1f)
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .height(8.dp)
- .clip(RoundedCornerShape(999.dp))
- .background(colorScheme.surfaceVariant)
- ) {
- Box(
- modifier = Modifier
- .fillMaxHeight()
- .fillMaxWidth(progressFraction)
- .clip(RoundedCornerShape(999.dp))
- .background(progressColor)
+ if (flashState.currentStep.isNotEmpty()) {
+ Spacer(modifier = Modifier.height(12.dp))
+ Text(
+ text = flashState.currentStep,
+ fontSize = MiuixTheme.textStyles.body2.fontSize,
+ color = colorScheme.onSurfaceVariantSummary
)
}
+ Spacer(modifier = Modifier.height(12.dp))
+
+ LinearProgressIndicator(
+ progress = progress.value,
+ modifier = Modifier.fillMaxWidth()
+ )
+
if (flashState.error.isNotEmpty()) {
- Spacer(modifier = Modifier.height(8.dp))
-
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- Icon(
- imageVector = Icons.Default.Error,
- contentDescription = null,
- tint = colorScheme.error,
- modifier = Modifier.size(16.dp)
- )
- }
-
- Spacer(modifier = Modifier.height(4.dp))
-
+ Spacer(modifier = Modifier.height(12.dp))
Text(
text = flashState.error,
+ fontSize = MiuixTheme.textStyles.body2.fontSize,
color = colorScheme.onErrorContainer,
modifier = Modifier
.fillMaxWidth()
+ .padding(12.dp)
.background(
- colorScheme.errorContainer.copy(alpha = 0.8f)
+ colorScheme.errorContainer
)
- .padding(8.dp)
+ .padding(12.dp)
)
}
}
diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/kernelFlash/component/SlotSelectionDialog.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/kernelFlash/component/SlotSelectionDialog.kt
index 78bc7131..d4c33c49 100644
--- a/manager/app/src/main/java/com/sukisu/ultra/ui/kernelFlash/component/SlotSelectionDialog.kt
+++ b/manager/app/src/main/java/com/sukisu/ultra/ui/kernelFlash/component/SlotSelectionDialog.kt
@@ -6,12 +6,17 @@ import androidx.compose.material.icons.filled.SdStorage
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.sukisu.ultra.R
+import com.sukisu.ultra.ui.util.getRootShell
+import com.topjohnwu.superuser.ShellUtils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextButton
@@ -35,11 +40,13 @@ fun SlotSelectionDialog(
var selectedSlot by remember { mutableStateOf(null) }
val showDialog = remember { mutableStateOf(show) }
+ val context = LocalContext.current
+
LaunchedEffect(show) {
showDialog.value = show
if (show) {
try {
- currentSlot = getCurrentSlot()
+ currentSlot = withContext(Dispatchers.IO) { getCurrentSlot() }
// 设置默认选择为当前槽位
selectedSlot = when (currentSlot) {
"a" -> "a"
@@ -48,7 +55,7 @@ fun SlotSelectionDialog(
}
errorMessage = null
} catch (e: Exception) {
- errorMessage = e.message
+ errorMessage = context.getString(R.string.operation_failed)
currentSlot = null
}
}
@@ -85,9 +92,9 @@ fun SlotSelectionDialog(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 8.dp),
- text = "Error: $errorMessage",
+ text = errorMessage ?: context.getString(R.string.operation_failed),
fontSize = MiuixTheme.textStyles.body2.fontSize,
- color = colorScheme.primary,
+ color = colorScheme.error,
textAlign = TextAlign.Center
)
} else {
@@ -97,7 +104,7 @@ fun SlotSelectionDialog(
.padding(horizontal = 24.dp, vertical = 8.dp),
text = stringResource(
id = R.string.current_slot,
- currentSlot ?: "Unknown"
+ currentSlot?.uppercase() ?: context.getString(R.string.not_supported)
),
fontSize = MiuixTheme.textStyles.body2.fontSize,
color = colorScheme.onSurfaceVariantSummary,
@@ -194,25 +201,16 @@ data class SlotOption(
)
// Utility function to get current slot
-private fun getCurrentSlot(): String? {
- return runCommandGetOutput()?.let {
- if (it.startsWith("_")) it.substring(1) else it
- }
-}
-
-private fun runCommandGetOutput(): String? {
- val cmd = "getprop ro.boot.slot_suffix"
+private suspend fun getCurrentSlot(): String? {
return try {
- val process = ProcessBuilder("su").start()
- process.outputStream.bufferedWriter().use { writer ->
- writer.write("$cmd\n")
- writer.write("exit\n")
- writer.flush()
- }
- process.inputStream.bufferedReader().use { reader ->
- reader.readText().trim()
- }
- } catch (_: Exception) {
+ val shell = getRootShell()
+ val result = ShellUtils.fastCmd(shell, "getprop ro.boot.slot_suffix").trim()
+ if (result.startsWith("_")) {
+ result.substring(1)
+ } else {
+ result
+ }.takeIf { it.isNotEmpty() }
+ } catch (e: Exception) {
null
}
}
diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/kernelFlash/state/KernelFlashState.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/kernelFlash/state/KernelFlashState.kt
index bf73ed23..9890d849 100644
--- a/manager/app/src/main/java/com/sukisu/ultra/ui/kernelFlash/state/KernelFlashState.kt
+++ b/manager/app/src/main/java/com/sukisu/ultra/ui/kernelFlash/state/KernelFlashState.kt
@@ -8,9 +8,10 @@ import androidx.documentfile.provider.DocumentFile
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.kernelFlash.util.AssetsUtil
import com.sukisu.ultra.ui.kernelFlash.util.RemoteToolsDownloader
+import com.sukisu.ultra.ui.util.getRootShell
import com.sukisu.ultra.ui.util.install
import com.sukisu.ultra.ui.util.rootAvailable
-import com.topjohnwu.superuser.Shell
+import com.topjohnwu.superuser.ShellUtils
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -74,10 +75,6 @@ class HorizonKernelState {
fun completeFlashing() {
_state.update { it.copy(isCompleted = true, progress = 1f) }
}
-
- fun reset() {
- _state.value = FlashState()
- }
}
class HorizonKernelWorker(
@@ -157,7 +154,12 @@ class HorizonKernelWorker(
if (isAbDevice && slot != null) {
state.updateStep(context.getString(R.string.horizon_getting_original_slot))
state.updateProgress(0.72f)
- originalSlot = runCommandGetOutput("getprop ro.boot.slot_suffix")
+ originalSlot = try {
+ val shell = getRootShell()
+ ShellUtils.fastCmd(shell, "getprop ro.boot.slot_suffix").trim()
+ } catch (_: Exception) {
+ null
+ }
state.updateStep(context.getString(R.string.horizon_setting_target_slot))
state.updateProgress(0.74f)
@@ -308,7 +310,12 @@ class HorizonKernelWorker(
}
// 查找Image文件
- val findImageResult = runCommandGetOutput("find $extractDir -name '*Image*' -type f")
+ val findImageResult = try {
+ val shell = getRootShell()
+ ShellUtils.fastCmd(shell, "find $extractDir -name '*Image*' -type f").trim()
+ } catch (_: Exception) {
+ throw IOException(context.getString(R.string.kpm_image_file_not_found))
+ }
if (findImageResult.isBlank()) {
throw IOException(context.getString(R.string.kpm_image_file_not_found))
}
@@ -398,11 +405,16 @@ class HorizonKernelWorker(
// 检查设备是否为AB分区设备
private fun isAbDevice(): Boolean {
- val abUpdate = runCommandGetOutput("getprop ro.build.ab_update")
- if (!abUpdate.toBoolean()) return false
+ return try {
+ val shell = getRootShell()
+ val abUpdate = ShellUtils.fastCmd(shell, "getprop ro.build.ab_update").trim()
+ if (!abUpdate.toBoolean()) return false
- val slotSuffix = runCommandGetOutput("getprop ro.boot.slot_suffix")
- return slotSuffix.isNotEmpty()
+ val slotSuffix = ShellUtils.fastCmd(shell, "getprop ro.boot.slot_suffix").trim()
+ slotSuffix.isNotEmpty()
+ } catch (_: Exception) {
+ false
+ }
}
private fun cleanup() {
@@ -429,7 +441,12 @@ class HorizonKernelWorker(
@SuppressLint("StringFormatInvalid")
private fun patch() {
- val kernelVersion = runCommandGetOutput("cat /proc/version")
+ val kernelVersion = try {
+ val shell = getRootShell()
+ ShellUtils.fastCmd(shell, "cat /proc/version")
+ } catch (_: Exception) {
+ ""
+ }
val versionRegex = """\d+\.\d+\.\d+""".toRegex()
val version = kernelVersion.let { versionRegex.find(it) }?.value ?: ""
val toolName = if (version.isNotEmpty()) {
@@ -447,7 +464,9 @@ class HorizonKernelWorker(
val toolPath = "${context.filesDir.absolutePath}/mkbootfs"
AssetsUtil.exportFiles(context, "$toolName-mkbootfs", toolPath)
state.addLog("${context.getString(R.string.kernel_version_log, version)} ${context.getString(R.string.tool_version_log, toolName)}")
- runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $toolPath \$AKHOME/tools;' $binaryPath")
+ runCommand(false,
+ $$"sed -i '/chmod -R 755 tools bin;/i cp -f $$toolPath $AKHOME/tools;' $$binaryPath"
+ )
}
private fun flash() {
@@ -517,8 +536,4 @@ class HorizonKernelWorker(
process.destroy()
}
}
-
- private fun runCommandGetOutput(cmd: String): String {
- return Shell.cmd(cmd).exec().out.joinToString("\n").trim()
- }
}
\ No newline at end of file
diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/kernelFlash/util/RemoteToolsDownloader.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/kernelFlash/util/RemoteToolsDownloader.kt
index b92656b4..17a17a22 100644
--- a/manager/app/src/main/java/com/sukisu/ultra/ui/kernelFlash/util/RemoteToolsDownloader.kt
+++ b/manager/app/src/main/java/com/sukisu/ultra/ui/kernelFlash/util/RemoteToolsDownloader.kt
@@ -2,6 +2,8 @@ package com.sukisu.ultra.ui.kernelFlash.util
import android.content.Context
import android.util.Log
+import com.sukisu.ultra.ui.util.getRootShell
+import com.topjohnwu.superuser.ShellUtils
import kotlinx.coroutines.*
import java.io.File
import java.io.FileOutputStream
@@ -23,8 +25,8 @@ class RemoteToolsDownloader(
private const val KPIMG_REMOTE_URL = "https://raw.githubusercontent.com/ShirkNeko/SukiSU_patch/refs/heads/main/kpm/kpimg"
// 网络超时配置(毫秒)
- private const val CONNECTION_TIMEOUT = 15000 // 15秒连接超时
- private const val READ_TIMEOUT = 30000 // 30秒读取超时
+ private const val CONNECTION_TIMEOUT = 10000
+ private const val READ_TIMEOUT = 20000
// 最大重试次数
private const val MAX_RETRY_COUNT = 3
@@ -48,47 +50,26 @@ class RemoteToolsDownloader(
suspend fun downloadToolsAsync(listener: DownloadProgressListener?): Map = withContext(Dispatchers.IO) {
- val results = mutableMapOf()
-
listener?.onLog("Starting to prepare KPM tool files...")
+ File(workDir).mkdirs()
- try {
- // 确保工作目录存在
- File(workDir).mkdirs()
+ // 并行下载两个工具文件
+ val results = mapOf(
+ "kptools" to async { downloadSingleTool("kptools", KPTOOLS_REMOTE_URL, listener) },
+ "kpimg" to async { downloadSingleTool("kpimg", KPIMG_REMOTE_URL, listener) }
+ ).mapValues { it.value.await() }
- // 并行下载两个工具文件
- val kptoolsDeferred = async { downloadSingleTool("kptools", KPTOOLS_REMOTE_URL, listener) }
- val kpimgDeferred = async { downloadSingleTool("kpimg", KPIMG_REMOTE_URL, listener) }
-
- // 等待所有下载完成
- results["kptools"] = kptoolsDeferred.await()
- results["kpimg"] = kpimgDeferred.await()
-
- // 检查kptools执行权限
- val kptoolsFile = File(workDir, "kptools")
- if (kptoolsFile.exists()) {
- setExecutablePermission(kptoolsFile.absolutePath)
- listener?.onLog("Set kptools execution permission")
- }
-
- val successCount = results.values.count { it.success }
- val remoteCount = results.values.count { it.success && it.isRemoteSource }
-
- listener?.onLog("KPM tools preparation completed: Success $successCount/2, Remote downloaded $remoteCount")
-
- } catch (e: Exception) {
- Log.e(TAG, "Exception occurred while downloading tools", e)
- listener?.onLog("Exception occurred during tool download: ${e.message}")
-
- if (!results.containsKey("kptools")) {
- results["kptools"] = downloadSingleTool("kptools", null, listener)
- }
- if (!results.containsKey("kpimg")) {
- results["kpimg"] = downloadSingleTool("kpimg", null, listener)
- }
+ // 设置 kptools 执行权限
+ File(workDir, "kptools").takeIf { it.exists() }?.let { file ->
+ setExecutablePermission(file.absolutePath)
+ listener?.onLog("Set kptools execution permission")
}
- results.toMap()
+ val successCount = results.values.count { it.success }
+ val remoteCount = results.values.count { it.success && it.isRemoteSource }
+ listener?.onLog("KPM tools preparation completed: Success $successCount/2, Remote downloaded $remoteCount")
+
+ results
}
private suspend fun downloadSingleTool(
@@ -96,43 +77,38 @@ class RemoteToolsDownloader(
remoteUrl: String?,
listener: DownloadProgressListener?
): DownloadResult = withContext(Dispatchers.IO) {
-
val targetFile = File(workDir, fileName)
if (remoteUrl == null) {
return@withContext useLocalVersion(fileName, targetFile, listener)
}
- // 尝试从远程下载
listener?.onLog("Downloading $fileName from remote repository...")
- var lastError = ""
-
// 重试机制
+ var lastError = ""
repeat(MAX_RETRY_COUNT) { attempt ->
try {
val result = downloadFromRemote(fileName, remoteUrl, targetFile, listener)
if (result.success) {
listener?.onSuccess(fileName, true)
return@withContext result
+ } else {
+ lastError = result.errorMessage ?: "Unknown error"
}
- lastError = result.errorMessage ?: "Unknown error"
-
} catch (e: Exception) {
- lastError = e.message ?: "Network exception"
+ lastError = "Network exception"
Log.w(TAG, "$fileName download attempt ${attempt + 1} failed", e)
+ }
- if (attempt < MAX_RETRY_COUNT - 1) {
- listener?.onLog("$fileName download failed, retrying in ${(attempt + 1) * 2} seconds...")
- delay(TimeUnit.SECONDS.toMillis((attempt + 1) * 2L))
- }
+ if (attempt < MAX_RETRY_COUNT - 1) {
+ listener?.onLog("$fileName download failed, retrying in ${(attempt + 1) * 2} seconds...")
+ delay(TimeUnit.SECONDS.toMillis((attempt + 1) * 2L))
}
}
- // 所有重试都失败,回退到本地版本
listener?.onError(fileName, "Remote download failed: $lastError")
listener?.onLog("$fileName remote download failed, falling back to local version...")
-
useLocalVersion(fileName, targetFile, listener)
}
@@ -142,15 +118,10 @@ class RemoteToolsDownloader(
targetFile: File,
listener: DownloadProgressListener?
): DownloadResult = withContext(Dispatchers.IO) {
-
var connection: HttpURLConnection? = null
try {
- val url = URL(remoteUrl)
- connection = url.openConnection() as HttpURLConnection
-
- // 设置连接参数
- connection.apply {
+ connection = (URL(remoteUrl).openConnection() as HttpURLConnection).apply {
connectTimeout = CONNECTION_TIMEOUT
readTimeout = READ_TIMEOUT
requestMethod = "GET"
@@ -159,22 +130,17 @@ class RemoteToolsDownloader(
setRequestProperty("Connection", "close")
}
- // 建立连接
connection.connect()
- val responseCode = connection.responseCode
- if (responseCode != HttpURLConnection.HTTP_OK) {
+ if (connection.responseCode != HttpURLConnection.HTTP_OK) {
return@withContext DownloadResult(
false,
isRemoteSource = false,
- errorMessage = "HTTP error code: $responseCode"
+ errorMessage = "HTTP error code: ${connection.responseCode}"
)
}
val fileLength = connection.contentLength
- Log.d(TAG, "$fileName remote file size: $fileLength bytes")
-
- // 创建临时文件
val tempFile = File(targetFile.absolutePath + ".tmp")
// 下载文件
@@ -182,40 +148,34 @@ class RemoteToolsDownloader(
FileOutputStream(tempFile).use { output ->
val buffer = ByteArray(8192)
var totalBytes = 0
- var bytesRead: Int
- while (input.read(buffer).also { bytesRead = it } != -1) {
- // 检查协程是否被取消
+ while (true) {
ensureActive()
+ val bytesRead = input.read(buffer)
+ if (bytesRead == -1) break
output.write(buffer, 0, bytesRead)
totalBytes += bytesRead
- // 更新下载进度
if (fileLength > 0) {
listener?.onProgress(fileName, totalBytes, fileLength)
}
}
-
output.flush()
}
}
- // 验证下载的文件
+ // 验证并移动文件
if (!validateDownloadedFile(tempFile, fileName)) {
tempFile.delete()
return@withContext DownloadResult(
- success = false,
+ false,
isRemoteSource = false,
errorMessage = "File verification failed"
)
}
- // 移动临时文件到目标位置
- if (targetFile.exists()) {
- targetFile.delete()
- }
-
+ targetFile.delete()
if (!tempFile.renameTo(targetFile)) {
tempFile.delete()
return@withContext DownloadResult(
@@ -227,7 +187,6 @@ class RemoteToolsDownloader(
Log.i(TAG, "$fileName remote download successful, file size: ${targetFile.length()} bytes")
listener?.onLog("$fileName remote download successful")
-
DownloadResult(true, isRemoteSource = true)
} catch (e: SocketTimeoutException) {
@@ -235,16 +194,10 @@ class RemoteToolsDownloader(
DownloadResult(false, isRemoteSource = false, errorMessage = "Connection timeout")
} catch (e: IOException) {
Log.w(TAG, "$fileName network IO exception", e)
- DownloadResult(false,
- isRemoteSource = false,
- errorMessage = "Network connection exception: ${e.message}"
- )
+ DownloadResult(false, isRemoteSource = false, errorMessage = "Network exception: ${e.message}")
} catch (e: Exception) {
Log.e(TAG, "$fileName exception occurred during download", e)
- DownloadResult(false,
- isRemoteSource = false,
- errorMessage = "Download exception: ${e.message}"
- )
+ DownloadResult(false, isRemoteSource = false, errorMessage = "Download exception: ${e.message}")
} finally {
connection?.disconnect()
}
@@ -255,61 +208,42 @@ class RemoteToolsDownloader(
targetFile: File,
listener: DownloadProgressListener?
): DownloadResult = withContext(Dispatchers.IO) {
-
try {
AssetsUtil.exportFiles(context, fileName, targetFile.absolutePath)
- if (!targetFile.exists()) {
- val errorMsg = "Local $fileName file extraction failed"
+ if (!targetFile.exists() || !validateDownloadedFile(targetFile, fileName)) {
+ val errorMsg = if (!targetFile.exists()) {
+ "Local $fileName file extraction failed"
+ } else {
+ "Local $fileName file verification failed"
+ }
listener?.onError(fileName, errorMsg)
- return@withContext DownloadResult(false,
- isRemoteSource = false,
- errorMessage = errorMsg
- )
- }
-
- if (!validateDownloadedFile(targetFile, fileName)) {
- val errorMsg = "Local $fileName file verification failed"
- listener?.onError(fileName, errorMsg)
- return@withContext DownloadResult(
- success = false,
- isRemoteSource = false,
- errorMessage = errorMsg
- )
+ return@withContext DownloadResult(false, isRemoteSource = false, errorMessage = errorMsg)
}
Log.i(TAG, "$fileName local version loaded successfully, file size: ${targetFile.length()} bytes")
listener?.onLog("$fileName local version loaded successfully")
listener?.onSuccess(fileName, false)
-
DownloadResult(true, isRemoteSource = false)
} catch (e: Exception) {
Log.e(TAG, "$fileName local version loading failed", e)
val errorMsg = "Local version loading failed: ${e.message}"
listener?.onError(fileName, errorMsg)
- DownloadResult(success = false, isRemoteSource = false, errorMessage = errorMsg)
+ DownloadResult(false, isRemoteSource = false, errorMessage = errorMsg)
}
}
private fun validateDownloadedFile(file: File, fileName: String): Boolean {
- if (!file.exists()) {
- Log.w(TAG, "$fileName file does not exist")
+ if (!file.exists() || file.length() < MIN_FILE_SIZE) {
+ Log.w(TAG, "$fileName file validation failed: exists=${file.exists()}, size=${file.length()}")
return false
}
- val fileSize = file.length()
- if (fileSize < MIN_FILE_SIZE) {
- Log.w(TAG, "$fileName file is too small: $fileSize bytes")
- return false
- }
-
- try {
+ return try {
file.inputStream().use { input ->
val header = ByteArray(4)
- val bytesRead = input.read(header)
-
- if (bytesRead < 4) {
+ if (input.read(header) < 4) {
Log.w(TAG, "$fileName file header read incomplete")
return false
}
@@ -324,20 +258,24 @@ class RemoteToolsDownloader(
return false
}
- Log.d(TAG, "$fileName file verification passed, size: $fileSize bytes, ELF: $isELF")
- return true
+ Log.d(TAG, "$fileName file verification passed, size: ${file.length()} bytes, ELF: $isELF")
+ true
}
} catch (e: Exception) {
Log.w(TAG, "$fileName file verification exception", e)
- return false
+ false
}
}
private fun setExecutablePermission(filePath: String) {
try {
- val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "chmod a+rx $filePath"))
- process.waitFor()
- Log.d(TAG, "Set execution permission for $filePath")
+ val shell = getRootShell()
+ if (ShellUtils.fastCmdResult(shell, "chmod a+rx $filePath")) {
+ Log.d(TAG, "Set execution permission for $filePath")
+ } else {
+ File(filePath).setExecutable(true, false)
+ Log.d(TAG, "Set execution permission using Java method for $filePath")
+ }
} catch (e: Exception) {
Log.w(TAG, "Failed to set execution permission: $filePath", e)
try {
@@ -351,11 +289,9 @@ class RemoteToolsDownloader(
fun cleanup() {
try {
- File(workDir).listFiles()?.forEach { file ->
- if (file.name.endsWith(".tmp")) {
- file.delete()
- Log.d(TAG, "Cleaned temporary file: ${file.name}")
- }
+ File(workDir).listFiles()?.filter { it.name.endsWith(".tmp") }?.forEach { file ->
+ file.delete()
+ Log.d(TAG, "Cleaned temporary file: ${file.name}")
}
} catch (e: Exception) {
Log.w(TAG, "Failed to clean temporary files", e)
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 ed7e5026..a500ddad 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
@@ -1,5 +1,6 @@
package com.sukisu.ultra.ui.screen
+import android.content.Intent
import android.net.Uri
import android.os.Environment
import android.os.Parcelable
@@ -37,6 +38,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
+import androidx.activity.compose.LocalActivity
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
@@ -44,6 +46,7 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.dropUnlessResumed
+import kotlinx.coroutines.delay
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -115,6 +118,7 @@ fun FlashScreen(
var showFloatAction by rememberSaveable { mutableStateOf(false) }
val context = LocalContext.current
+ val activity = LocalActivity.current
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
var flashing by rememberSaveable {
@@ -149,6 +153,23 @@ fun FlashScreen(
}
}
+ // 如果是从外部打开的模块安装,延迟1秒后自动退出
+ LaunchedEffect(flashing, flashIt) {
+ if (flashing == FlashingStatus.SUCCESS && flashIt is FlashIt.FlashModules) {
+ val intent = activity?.intent
+ val isFromExternalIntent = intent?.action?.let { action ->
+ action == Intent.ACTION_VIEW ||
+ action == Intent.ACTION_SEND ||
+ action == Intent.ACTION_SEND_MULTIPLE
+ } ?: false
+
+ if (isFromExternalIntent) {
+ delay(1000)
+ activity.finish()
+ }
+ }
+ }
+
Scaffold(
topBar = {
TopBar(
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 74e74916..2b233bab 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
@@ -362,7 +362,7 @@ fun InstallScreen(
// AnyKernel3 刷写
(installMethod as? InstallMethod.HorizonKernel)?.let { method ->
- if (method.slot != null) {
+ if (isAbDevice && method.slot != null) {
Card(
modifier = Modifier
.fillMaxWidth()
@@ -374,11 +374,13 @@ fun InstallScreen(
if (method.slot == "a") stringResource(id = R.string.slot_a)
else stringResource(id = R.string.slot_b)
),
- onClick = {},
+ onClick = {
+ anyKernel3State.onReopenSlotDialog(method)
+ },
leftAction = {
Icon(
Icons.Filled.SdStorage,
- tint = colorScheme.onSurface,
+ tint = colorScheme.primary,
modifier = Modifier.padding(end = 16.dp),
contentDescription = null
)
@@ -388,32 +390,33 @@ fun InstallScreen(
}
// KPM 状态显示
- if (kpmPatchOption != KpmPatchOption.FOLLOW_KERNEL) {
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 12.dp),
- ) {
- SuperArrow(
- title = when (kpmPatchOption) {
- KpmPatchOption.PATCH_KPM -> stringResource(R.string.kpm_patch_enabled)
- KpmPatchOption.UNDO_PATCH_KPM -> stringResource(R.string.kpm_undo_patch_enabled)
- else -> ""
- },
- onClick = {},
- leftAction = {
- Icon(
- Icons.Filled.Security,
- tint = if (kpmPatchOption == KpmPatchOption.PATCH_KPM)
- colorScheme.primary
- else
- colorScheme.secondary,
- modifier = Modifier.padding(end = 16.dp),
- contentDescription = null
- )
- }
- )
- }
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 12.dp),
+ ) {
+ SuperArrow(
+ title = when (kpmPatchOption) {
+ KpmPatchOption.PATCH_KPM -> stringResource(R.string.kpm_patch_enabled)
+ KpmPatchOption.UNDO_PATCH_KPM -> stringResource(R.string.kpm_undo_patch_enabled)
+ KpmPatchOption.FOLLOW_KERNEL -> stringResource(R.string.kpm_follow_kernel_file)
+ },
+ onClick = {
+ anyKernel3State.onReopenKpmDialog(method)
+ },
+ leftAction = {
+ Icon(
+ Icons.Filled.Security,
+ tint = when (kpmPatchOption) {
+ KpmPatchOption.PATCH_KPM -> colorScheme.primary
+ KpmPatchOption.UNDO_PATCH_KPM -> colorScheme.secondary
+ KpmPatchOption.FOLLOW_KERNEL -> colorScheme.onSurfaceVariantSummary
+ },
+ modifier = Modifier.padding(end = 16.dp),
+ contentDescription = null
+ )
+ }
+ )
}
}
Button(
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 0b8fe3a0..3497f323 100644
--- a/manager/app/src/main/res/values-zh-rCN/strings.xml
+++ b/manager/app/src/main/res/values-zh-rCN/strings.xml
@@ -368,4 +368,12 @@
选择备份文件进行导入
还原成功,重启生效
还原失败
+
+ (模块)
+ (内核)
+ (未知)
+ unknown.zip
+
+ 将安装以下内核:%1$s
+ 将安装以下文件:%1$s
diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml
index 793eaedd..63150345 100644
--- a/manager/app/src/main/res/values/strings.xml
+++ b/manager/app/src/main/res/values/strings.xml
@@ -376,4 +376,12 @@
Choose a backup file to import
Restore succeeded
Restore failed
+
+ (Module)
+ (Kernel)
+ (Unknown)
+ unknown.zip
+
+ The following kernels will be installed: %1$s
+ The following files will be installed: %1$s