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.NavGraphs
import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination
import com.ramcosta.composedestinations.spec.NavHostGraphSpec import com.ramcosta.composedestinations.spec.NavHostGraphSpec
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
import com.sukisu.ultra.Natives import com.sukisu.ultra.Natives
@@ -40,8 +41,14 @@ import com.sukisu.ultra.ui.webui.initPlatform
import com.sukisu.ultra.ui.screen.FlashIt import com.sukisu.ultra.ui.screen.FlashIt
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch 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.component.BottomBar
import zako.zako.zako.zakoui.activity.util.* 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() { class MainActivity : ComponentActivity() {
private lateinit var superUserViewModel: SuperUserViewModel private lateinit var superUserViewModel: SuperUserViewModel
@@ -113,11 +120,8 @@ class MainActivity : ComponentActivity() {
LaunchedEffect(zipUri) { LaunchedEffect(zipUri) {
if (!zipUri.isNullOrEmpty()) { if (!zipUri.isNullOrEmpty()) {
navigator.navigate( // 检测 ZIP 文件类型并导航到相应界面
FlashScreenDestination( detectZipTypeAndNavigate(zipUri, navigator)
FlashIt.FlashModules(zipUri)
)
)
} }
} }
@@ -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() { private fun initializeViewModels() {
superUserViewModel = SuperUserViewModel() superUserViewModel = SuperUserViewModel()
homeViewModel = HomeViewModel() homeViewModel = HomeViewModel()

View File

@@ -1,5 +1,6 @@
package com.sukisu.ultra.ui.screen package com.sukisu.ultra.ui.screen
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Environment import android.os.Environment
@@ -50,6 +51,7 @@ import kotlinx.parcelize.Parcelize
import java.io.File import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import androidx.core.content.edit
/** /**
* @author ShirkNeko * @author ShirkNeko
@@ -122,6 +124,11 @@ fun setModuleVerificationStatus(uri: Uri, isVerified: Boolean) {
fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
val context = LocalContext.current 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 { val isExternalInstall = remember {
when (flashIt) { when (flashIt) {
@@ -231,10 +238,14 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
} }
hasUpdateCompleted = true hasUpdateCompleted = true
// 如果是外部安装的模块更新且不需要重启,延迟后自动返回 // 如果是外部安装或需要自动退出的模块更新且不需要重启,延迟后自动返回
if (isExternalInstall) { if (isExternalInstall || shouldAutoExit) {
scope.launch { scope.launch {
kotlinx.coroutines.delay(2000) 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() (context as? ComponentActivity)?.finish()
} }
} }
@@ -330,16 +341,24 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
kotlinx.coroutines.delay(500) kotlinx.coroutines.delay(500)
navigator.navigate(FlashScreenDestination(nextFlashIt)) 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 { scope.launch {
kotlinx.coroutines.delay(2000) 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() (context as? ComponentActivity)?.finish()
} }
} else if (isExternalInstall && flashIt is FlashIt.FlashModule) { } else if ((isExternalInstall || shouldAutoExit) && flashIt is FlashIt.FlashModule) {
// 如果是外部安装单个模块,安装完成后自动返回 // 如果是外部安装或需要自动退出的单个模块,安装完成后自动返回
scope.launch { scope.launch {
kotlinx.coroutines.delay(2000) 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() (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) { return withContext(Dispatchers.IO) {
try { try {
if (uri == Uri.EMPTY) { 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.Block
import androidx.compose.material.icons.outlined.TaskAlt import androidx.compose.material.icons.outlined.TaskAlt
import androidx.compose.material.icons.outlined.Warning import androidx.compose.material.icons.outlined.Warning
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.* import androidx.compose.material3.*
@@ -139,7 +138,7 @@ fun HomeScreen(navigator: DestinationsNavigator) {
StatusCard( StatusCard(
systemStatus = viewModel.systemStatus, systemStatus = viewModel.systemStatus,
onClickInstall = { 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.semantics.Role
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp 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.Header
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
import com.maxkeppeler.sheets.list.ListDialog import com.maxkeppeler.sheets.list.ListDialog
@@ -71,19 +72,45 @@ enum class KpmPatchOption {
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph> @Destination<RootGraph>
@Composable @Composable
fun InstallScreen(navigator: DestinationsNavigator) { fun InstallScreen(
navigator: DestinationsNavigator,
preselectedKernelUri: String? = null
) {
val context = LocalContext.current
var installMethod by remember { mutableStateOf<InstallMethod?>(null) } var installMethod by remember { mutableStateOf<InstallMethod?>(null) }
var lkmSelection by remember { mutableStateOf<LkmSelection>(LkmSelection.KmiNone) } var lkmSelection by remember { mutableStateOf<LkmSelection>(LkmSelection.KmiNone) }
var kpmPatchOption by remember { mutableStateOf(KpmPatchOption.FOLLOW_KERNEL) } var kpmPatchOption by remember { mutableStateOf(KpmPatchOption.FOLLOW_KERNEL) }
val context = LocalContext.current
var showRebootDialog by remember { mutableStateOf(false) } var showRebootDialog by remember { mutableStateOf(false) }
var showSlotSelectionDialog by remember { mutableStateOf(false) } var showSlotSelectionDialog by remember { mutableStateOf(false) }
var showKpmPatchDialog by remember { mutableStateOf(false) }
var tempKernelUri by remember { mutableStateOf<Uri?>(null) } var tempKernelUri by remember { mutableStateOf<Uri?>(null) }
val kernelVersion = getKernelVersion() val kernelVersion = getKernelVersion()
val isGKI = kernelVersion.isGKI() val isGKI = kernelVersion.isGKI()
val isAbDevice = isAbDevice() val isAbDevice = isAbDevice()
val summary = stringResource(R.string.horizon_kernel_summary) 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) { if (showRebootDialog) {
RebootDialog( RebootDialog(
show = true, show = true,
@@ -143,6 +170,19 @@ fun InstallScreen(navigator: DestinationsNavigator) {
summary = summary summary = summary
) )
installMethod = horizonMethod 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 showSlotSelectionDialog = true
} else { } else {
installMethod = method installMethod = method
showKpmPatchDialog = true
} }
} else { } else {
installMethod = method 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 @Composable
private fun RebootDialog( private fun RebootDialog(
show: Boolean, show: Boolean,
@@ -404,6 +486,10 @@ private fun SelectInstallMethod(
var selectedOption by remember { mutableStateOf<InstallMethod?>(null) } var selectedOption by remember { mutableStateOf<InstallMethod?>(null) }
var currentSelectingMethod by remember { mutableStateOf<InstallMethod?>(null) } var currentSelectingMethod by remember { mutableStateOf<InstallMethod?>(null) }
LaunchedEffect(selectedMethod) {
selectedOption = selectedMethod
}
val selectImageLauncher = rememberLauncherForActivityResult( val selectImageLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult() contract = ActivityResultContracts.StartActivityForResult()
) { ) {

View File

@@ -1,7 +1,9 @@
package zako.zako.zako.zakoui.screen package zako.zako.zako.zakoui.screen
import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Environment import android.os.Environment
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background 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.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -73,6 +76,12 @@ fun KernelFlashScreen(
kpmUndoPatch: Boolean = false kpmUndoPatch: Boolean = false
) { ) {
val context = LocalContext.current 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 scrollState = rememberScrollState()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val snackBarHost = LocalSnackbarHost.current val snackBarHost = LocalSnackbarHost.current
@@ -105,6 +114,16 @@ fun KernelFlashScreen(
val onFlashComplete = { val onFlashComplete = {
showFloatAction = true showFloatAction = true
KernelFlashStateHolder.isFlashing = false 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) { BackHandler(enabled = true) {
onBack() onBack()
} }