manager: Expand the option to directly open the file and flash the anykernel3 kernel package

This commit is contained in:
ShirkNeko
2025-10-08 18:23:53 +08:00
parent 2f43ad4f76
commit 230ca54d63
5 changed files with 263 additions and 16 deletions

View File

@@ -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<Uri>,
navigator: com.ramcosta.composedestinations.navigation.DestinationsNavigator
) {
withContext(Dispatchers.IO) {
try {
val moduleUris = mutableListOf<Uri>()
val kernelUris = mutableListOf<Uri>()
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()

View File

@@ -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) {

View File

@@ -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))
}
)

View File

@@ -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<RootGraph>
@Composable
fun InstallScreen(navigator: DestinationsNavigator) {
fun InstallScreen(
navigator: DestinationsNavigator,
preselectedKernelUri: String? = null
) {
val context = LocalContext.current
var installMethod by remember { mutableStateOf<InstallMethod?>(null) }
var lkmSelection by remember { mutableStateOf<LkmSelection>(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<Uri?>(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<InstallMethod?>(null) }
var currentSelectingMethod by remember { mutableStateOf<InstallMethod?>(null) }
LaunchedEffect(selectedMethod) {
selectedOption = selectedMethod
}
val selectImageLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {

View File

@@ -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()
}