From 230ca54d63a517812f9e96761784825f313d2ebf Mon Sep 17 00:00:00 2001 From: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com> Date: Wed, 8 Oct 2025 18:23:53 +0800 Subject: [PATCH] manager: Expand the option to directly open the file and flash the anykernel3 kernel package --- .../java/com/sukisu/ultra/ui/MainActivity.kt | 123 +++++++++++++++++- .../java/com/sukisu/ultra/ui/screen/Flash.kt | 33 ++++- .../java/com/sukisu/ultra/ui/screen/Home.kt | 3 +- .../com/sukisu/ultra/ui/screen/Install.kt | 90 ++++++++++++- .../zako/zako/zakoui/screen/KernelFlash.kt | 30 +++++ 5 files changed, 263 insertions(+), 16 deletions(-) diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt index f01fef8b..2e16d864 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt @@ -27,6 +27,7 @@ import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationSty import com.ramcosta.composedestinations.generated.NavGraphs import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination +import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination import com.ramcosta.composedestinations.spec.NavHostGraphSpec import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator import com.sukisu.ultra.Natives @@ -40,8 +41,14 @@ import com.sukisu.ultra.ui.webui.initPlatform import com.sukisu.ultra.ui.screen.FlashIt import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import zako.zako.zako.zakoui.activity.component.BottomBar import zako.zako.zako.zakoui.activity.util.* +import java.util.zip.ZipInputStream +import java.io.IOException +import androidx.core.content.edit +import com.sukisu.ultra.ui.util.rootAvailable class MainActivity : ComponentActivity() { private lateinit var superUserViewModel: SuperUserViewModel @@ -113,11 +120,8 @@ class MainActivity : ComponentActivity() { LaunchedEffect(zipUri) { if (!zipUri.isNullOrEmpty()) { - navigator.navigate( - FlashScreenDestination( - FlashIt.FlashModules(zipUri) - ) - ) + // 检测 ZIP 文件类型并导航到相应界面 + detectZipTypeAndNavigate(zipUri, navigator) } } @@ -197,6 +201,115 @@ class MainActivity : ComponentActivity() { } } + private enum class ZipType { + MODULE, + KERNEL, + UNKNOWN + } + + private fun detectZipType(uri: Uri): ZipType { + return try { + contentResolver.openInputStream(uri)?.use { inputStream -> + 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 + else -> ZipType.UNKNOWN + } + } + } ?: ZipType.UNKNOWN + } catch (e: IOException) { + e.printStackTrace() + ZipType.UNKNOWN + } + } + + private suspend fun detectZipTypeAndNavigate( + zipUris: ArrayList, + navigator: com.ramcosta.composedestinations.navigation.DestinationsNavigator + ) { + withContext(Dispatchers.IO) { + try { + val moduleUris = mutableListOf() + val kernelUris = mutableListOf() + + for (uri in zipUris) { + val zipType = detectZipType(uri) + when (zipType) { + ZipType.MODULE -> moduleUris.add(uri) + ZipType.KERNEL -> kernelUris.add(uri) + ZipType.UNKNOWN -> { + } + } + } + + // 根据检测结果导航 + withContext(Dispatchers.Main) { + when { + // 内核文件 + kernelUris.isNotEmpty() && moduleUris.isEmpty() -> { + if (kernelUris.size == 1 && rootAvailable()) { + navigator.navigate( + InstallScreenDestination( + preselectedKernelUri = kernelUris.first().toString() + ) + ) + } + setAutoExitAfterFlash() + } + // 模块文件 + moduleUris.isNotEmpty() -> { + navigator.navigate( + FlashScreenDestination( + FlashIt.FlashModules(ArrayList(moduleUris)) + ) + ) + setAutoExitAfterFlash() + } + // 如果没有识别出任何类型的文件,则直接退出 + else -> { + (this@MainActivity as? ComponentActivity)?.finish() + } + } + } + } catch (e: Exception) { + (this@MainActivity as? ComponentActivity)?.finish() + e.printStackTrace() + } + } + } + + private fun setAutoExitAfterFlash() { + val sharedPref = getSharedPreferences("kernel_flash_prefs", MODE_PRIVATE) + sharedPref.edit { + putBoolean("auto_exit_after_flash", true) + } + } + private fun initializeViewModels() { superUserViewModel = SuperUserViewModel() homeViewModel = HomeViewModel() 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 3285234b..62060d25 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.Context import android.content.Intent import android.net.Uri import android.os.Environment @@ -50,6 +51,7 @@ import kotlinx.parcelize.Parcelize import java.io.File import java.text.SimpleDateFormat import java.util.* +import androidx.core.content.edit /** * @author ShirkNeko @@ -122,6 +124,11 @@ fun setModuleVerificationStatus(uri: Uri, isVerified: Boolean) { fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { 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 isExternalInstall = remember { when (flashIt) { @@ -231,10 +238,14 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { } hasUpdateCompleted = true - // 如果是外部安装的模块更新且不需要重启,延迟后自动返回 - if (isExternalInstall) { + // 如果是外部安装或需要自动退出的模块更新且不需要重启,延迟后自动返回 + if (isExternalInstall || shouldAutoExit) { scope.launch { kotlinx.coroutines.delay(2000) + if (shouldAutoExit) { + val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) + sharedPref.edit { remove("auto_exit_after_flash") } + } (context as? ComponentActivity)?.finish() } } @@ -330,16 +341,24 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { kotlinx.coroutines.delay(500) navigator.navigate(FlashScreenDestination(nextFlashIt)) } - } else if (isExternalInstall && flashIt is FlashIt.FlashModules && flashIt.currentIndex >= flashIt.uris.size - 1) { - // 如果是外部安装且是最后一个模块,安装完成后自动返回 + } else if ((isExternalInstall || shouldAutoExit) && flashIt is FlashIt.FlashModules && flashIt.currentIndex >= flashIt.uris.size - 1) { + // 如果是外部安装或需要自动退出且是最后一个模块,安装完成后自动返回 scope.launch { kotlinx.coroutines.delay(2000) + if (shouldAutoExit) { + val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) + sharedPref.edit { remove("auto_exit_after_flash") } + } (context as? ComponentActivity)?.finish() } - } else if (isExternalInstall && flashIt is FlashIt.FlashModule) { - // 如果是外部安装单个模块,安装完成后自动返回 + } else if ((isExternalInstall || shouldAutoExit) && flashIt is FlashIt.FlashModule) { + // 如果是外部安装或需要自动退出的单个模块,安装完成后自动返回 scope.launch { kotlinx.coroutines.delay(2000) + if (shouldAutoExit) { + val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) + sharedPref.edit { remove("auto_exit_after_flash") } + } (context as? ComponentActivity)?.finish() } } @@ -668,7 +687,7 @@ private fun TopBar( ) } -suspend fun getModuleNameFromUri(context: android.content.Context, uri: Uri): String { +suspend fun getModuleNameFromUri(context: Context, uri: Uri): String { return withContext(Dispatchers.IO) { try { if (uri == Uri.EMPTY) { diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt index dcec2877..b003040b 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt @@ -20,7 +20,6 @@ import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.outlined.Block import androidx.compose.material.icons.outlined.TaskAlt import androidx.compose.material.icons.outlined.Warning -import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.* @@ -139,7 +138,7 @@ fun HomeScreen(navigator: DestinationsNavigator) { StatusCard( systemStatus = viewModel.systemStatus, onClickInstall = { - navigator.navigate(InstallScreenDestination) + navigator.navigate(InstallScreenDestination(preselectedKernelUri = null)) } ) 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 9f4fb7ce..2bdf085b 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 @@ -33,6 +33,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import com.maxkeppeker.sheets.core.models.base.Header import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState import com.maxkeppeler.sheets.list.ListDialog @@ -71,19 +72,45 @@ enum class KpmPatchOption { @OptIn(ExperimentalMaterial3Api::class) @Destination @Composable -fun InstallScreen(navigator: DestinationsNavigator) { +fun InstallScreen( + navigator: DestinationsNavigator, + preselectedKernelUri: String? = null +) { + val context = LocalContext.current var installMethod by remember { mutableStateOf(null) } var lkmSelection by remember { mutableStateOf(LkmSelection.KmiNone) } var kpmPatchOption by remember { mutableStateOf(KpmPatchOption.FOLLOW_KERNEL) } - val context = LocalContext.current var showRebootDialog by remember { mutableStateOf(false) } 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 summary = stringResource(R.string.horizon_kernel_summary) + // 处理预选的内核文件 + LaunchedEffect(preselectedKernelUri) { + preselectedKernelUri?.let { uriString -> + try { + val preselectedUri = uriString.toUri() + val horizonMethod = InstallMethod.HorizonKernel( + uri = preselectedUri, + summary = summary + ) + installMethod = horizonMethod + tempKernelUri = preselectedUri + if (isAbDevice) { + showSlotSelectionDialog = true + } else { + showKpmPatchDialog = true + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + if (showRebootDialog) { RebootDialog( show = true, @@ -143,6 +170,19 @@ fun InstallScreen(navigator: DestinationsNavigator) { summary = summary ) installMethod = horizonMethod + if (preselectedKernelUri != null) { + showKpmPatchDialog = true + } + } + ) + + KpmPatchSelectionDialog( + show = showKpmPatchDialog, + currentOption = kpmPatchOption, + onDismiss = { showKpmPatchDialog = false }, + onOptionSelected = { option -> + kpmPatchOption = option + showKpmPatchDialog = false } ) @@ -194,6 +234,7 @@ fun InstallScreen(navigator: DestinationsNavigator) { showSlotSelectionDialog = true } else { installMethod = method + showKpmPatchDialog = true } } else { installMethod = method @@ -316,6 +357,47 @@ fun InstallScreen(navigator: DestinationsNavigator) { } } +@Composable +private fun KpmPatchSelectionDialog( + show: Boolean, + currentOption: KpmPatchOption, + onDismiss: () -> Unit, + onOptionSelected: (KpmPatchOption) -> Unit +) { + if (show) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.kpm_patch_options)) }, + text = { + Column { + Text( + text = stringResource(R.string.kpm_patch_description), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 16.dp) + ) + + KpmPatchOptionGroup( + selectedOption = currentOption, + onOptionChanged = onOptionSelected + ) + } + }, + confirmButton = { + TextButton( + onClick = { onOptionSelected(currentOption) } + ) { + Text(stringResource(android.R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(android.R.string.cancel)) + } + } + ) + } +} + @Composable private fun RebootDialog( show: Boolean, @@ -404,6 +486,10 @@ private fun SelectInstallMethod( var selectedOption by remember { mutableStateOf(null) } var currentSelectingMethod by remember { mutableStateOf(null) } + LaunchedEffect(selectedMethod) { + selectedOption = selectedMethod + } + val selectImageLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/KernelFlash.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/KernelFlash.kt index 75ff72ff..2bf28fe9 100644 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/KernelFlash.kt +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/KernelFlash.kt @@ -1,7 +1,9 @@ package zako.zako.zako.zakoui.screen +import android.content.Context import android.net.Uri import android.os.Environment +import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background @@ -27,6 +29,7 @@ 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 @@ -73,6 +76,12 @@ 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 scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) val snackBarHost = LocalSnackbarHost.current @@ -105,6 +114,16 @@ fun KernelFlashScreen( val onFlashComplete = { showFloatAction = true KernelFlashStateHolder.isFlashing = false + + // 如果需要自动退出,延迟3秒后退出 + if (shouldAutoExit) { + scope.launch { + delay(3000) + val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) + sharedPref.edit { remove("auto_exit_after_flash") } + (context as? ComponentActivity)?.finish() + } + } } // 开始刷写 @@ -165,6 +184,17 @@ fun KernelFlashScreen( } } + DisposableEffect(Unit) { + onDispose { + KernelFlashStateHolder.currentState = null + KernelFlashStateHolder.currentUri = null + KernelFlashStateHolder.currentSlot = null + KernelFlashStateHolder.currentKpmPatchEnabled = false + KernelFlashStateHolder.currentKpmUndoPatch = false + KernelFlashStateHolder.isFlashing = false + } + } + BackHandler(enabled = true) { onBack() }