Step 6: feat: add direct zip flash for AnyKernel3 and modules
- fix Chrome zip open failure - one-tap flash AnyKernel3 kernel packages - bulk install with state de-duplication - refine share UI & color scheme --------------------------------- Co-Authored-By: Der_Googler <54764558+dergoogler@users.noreply.github.com> Co-authored-by: rifsxd <rifat.44.azad.rifs@gmail.com> Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com> Co-authored-by: KOWX712 <leecc0503@gmail.com> Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
This commit is contained in:
@@ -25,6 +25,26 @@
|
|||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:mimeType="application/zip" />
|
||||||
|
<data android:mimeType="application/vnd.android.package-archive" />
|
||||||
|
<data android:scheme="content" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="application/zip" />
|
||||||
|
<data android:mimeType="application/vnd.android.package-archive" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="application/zip" />
|
||||||
|
<data android:mimeType="application/vnd.android.package-archive" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import com.ramcosta.composedestinations.annotation.Destination
|
|||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
import com.ramcosta.composedestinations.generated.NavGraphs
|
import com.ramcosta.composedestinations.generated.NavGraphs
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
import dev.chrisbanes.haze.HazeStyle
|
import dev.chrisbanes.haze.HazeStyle
|
||||||
import dev.chrisbanes.haze.HazeTint
|
import dev.chrisbanes.haze.HazeTint
|
||||||
@@ -105,6 +106,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
KernelSUTheme(colorMode = colorMode, keyColor = keyColor) {
|
KernelSUTheme(colorMode = colorMode, keyColor = keyColor) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
val navigator = navController.rememberDestinationsNavigator()
|
||||||
|
val initialIntent = remember { intent }
|
||||||
|
|
||||||
Scaffold {
|
Scaffold {
|
||||||
DestinationsNavHost(
|
DestinationsNavHost(
|
||||||
@@ -146,6 +149,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
HandleZipFileIntent(initialIntent, navigator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,290 @@
|
|||||||
|
package com.sukisu.ultra.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.KernelFlashScreenDestination
|
||||||
|
import com.sukisu.ultra.ui.component.ConfirmResult
|
||||||
|
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
||||||
|
import com.sukisu.ultra.ui.kernelFlash.KpmPatchOption
|
||||||
|
import com.sukisu.ultra.ui.kernelFlash.KpmPatchSelectionDialog
|
||||||
|
import com.sukisu.ultra.ui.kernelFlash.component.SlotSelectionDialog
|
||||||
|
import com.sukisu.ultra.ui.screen.FlashIt
|
||||||
|
import com.sukisu.ultra.ui.util.getFileName
|
||||||
|
import com.sukisu.ultra.ui.util.isAbDevice
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
private sealed class DialogState {
|
||||||
|
data object None : DialogState()
|
||||||
|
data class SlotSelection(val kernelUri: Uri) : DialogState()
|
||||||
|
data class KpmSelection(val kernelUri: Uri, val slot: String?) : DialogState()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("StringFormatInvalid")
|
||||||
|
@Composable
|
||||||
|
fun HandleZipFileIntent(
|
||||||
|
intent: Intent?,
|
||||||
|
navigator: DestinationsNavigator
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val confirmDialog = rememberConfirmDialog()
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var processed by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var dialogState by remember { mutableStateOf<DialogState>(DialogState.None) }
|
||||||
|
var selectedSlot by remember { mutableStateOf<String?>(null) }
|
||||||
|
var kpmPatchOption by remember { mutableStateOf(KpmPatchOption.FOLLOW_KERNEL) }
|
||||||
|
|
||||||
|
LaunchedEffect(intent) {
|
||||||
|
if (intent == null || processed) return@LaunchedEffect
|
||||||
|
|
||||||
|
val zipUris = mutableSetOf<Uri>()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,7 +51,9 @@ data class AnyKernel3State(
|
|||||||
val onSlotSelected: (String) -> Unit,
|
val onSlotSelected: (String) -> Unit,
|
||||||
val onDismissSlotDialog: () -> Unit,
|
val onDismissSlotDialog: () -> Unit,
|
||||||
val onOptionSelected: (KpmPatchOption) -> Unit,
|
val onOptionSelected: (KpmPatchOption) -> Unit,
|
||||||
val onDismissPatchDialog: () -> Unit
|
val onDismissPatchDialog: () -> Unit,
|
||||||
|
val onReopenSlotDialog: (InstallMethod.HorizonKernel) -> Unit,
|
||||||
|
val onReopenKpmDialog: (InstallMethod.HorizonKernel) -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -69,7 +71,7 @@ fun rememberAnyKernel3State(
|
|||||||
val onHorizonKernelSelected: (InstallMethod.HorizonKernel) -> Unit = { method ->
|
val onHorizonKernelSelected: (InstallMethod.HorizonKernel) -> Unit = { method ->
|
||||||
val uri = method.uri
|
val uri = method.uri
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
if (isAbDevice) {
|
if (isAbDevice && method.slot == null) {
|
||||||
tempKernelUri = uri
|
tempKernelUri = uri
|
||||||
showSlotSelectionDialog = true
|
showSlotSelectionDialog = true
|
||||||
} else {
|
} else {
|
||||||
@@ -79,8 +81,21 @@ 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 onSlotSelected: (String) -> Unit = { slot ->
|
||||||
val uri = tempKernelUri
|
val uri = tempKernelUri ?: (installMethodState.value as? InstallMethod.HorizonKernel)?.uri
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
installMethodState.value = InstallMethod.HorizonKernel(
|
installMethodState.value = InstallMethod.HorizonKernel(
|
||||||
uri = uri,
|
uri = uri,
|
||||||
@@ -95,7 +110,6 @@ fun rememberAnyKernel3State(
|
|||||||
|
|
||||||
val onDismissSlotDialog = {
|
val onDismissSlotDialog = {
|
||||||
showSlotSelectionDialog = false
|
showSlotSelectionDialog = false
|
||||||
tempKernelUri = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val onOptionSelected: (KpmPatchOption) -> Unit = { option ->
|
val onOptionSelected: (KpmPatchOption) -> Unit = { option ->
|
||||||
@@ -135,7 +149,9 @@ fun rememberAnyKernel3State(
|
|||||||
onSlotSelected = onSlotSelected,
|
onSlotSelected = onSlotSelected,
|
||||||
onDismissSlotDialog = onDismissSlotDialog,
|
onDismissSlotDialog = onDismissSlotDialog,
|
||||||
onOptionSelected = onOptionSelected,
|
onOptionSelected = onOptionSelected,
|
||||||
onDismissPatchDialog = onDismissPatchDialog
|
onDismissPatchDialog = onDismissPatchDialog,
|
||||||
|
onReopenSlotDialog = onReopenSlotDialog,
|
||||||
|
onReopenKpmDialog = onReopenKpmDialog
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
package com.sukisu.ultra.ui.kernelFlash
|
package com.sukisu.ultra.ui.kernelFlash
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Intent
|
||||||
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.activity.compose.LocalActivity
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
@@ -19,7 +18,6 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.input.key.key
|
import androidx.compose.ui.input.key.key
|
||||||
import androidx.compose.ui.platform.LocalContext
|
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.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
|
||||||
@@ -43,12 +40,14 @@ import top.yukonga.miuix.kmp.basic.Card
|
|||||||
import top.yukonga.miuix.kmp.basic.FloatingActionButton
|
import top.yukonga.miuix.kmp.basic.FloatingActionButton
|
||||||
import top.yukonga.miuix.kmp.basic.Icon
|
import top.yukonga.miuix.kmp.basic.Icon
|
||||||
import top.yukonga.miuix.kmp.basic.IconButton
|
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.Scaffold
|
||||||
import top.yukonga.miuix.kmp.basic.SmallTopAppBar
|
import top.yukonga.miuix.kmp.basic.SmallTopAppBar
|
||||||
import top.yukonga.miuix.kmp.basic.Text
|
import top.yukonga.miuix.kmp.basic.Text
|
||||||
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
||||||
import top.yukonga.miuix.kmp.icon.icons.useful.Back
|
import top.yukonga.miuix.kmp.icon.icons.useful.Back
|
||||||
import top.yukonga.miuix.kmp.icon.icons.useful.Save
|
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.theme.MiuixTheme.colorScheme
|
||||||
import top.yukonga.miuix.kmp.utils.scrollEndHaptic
|
import top.yukonga.miuix.kmp.utils.scrollEndHaptic
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -66,6 +65,15 @@ private object KernelFlashStateHolder {
|
|||||||
var currentKpmPatchEnabled: Boolean = false
|
var currentKpmPatchEnabled: Boolean = false
|
||||||
var currentKpmUndoPatch: Boolean = false
|
var currentKpmUndoPatch: Boolean = false
|
||||||
var isFlashing = false
|
var isFlashing = false
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
currentState = null
|
||||||
|
currentUri = null
|
||||||
|
currentSlot = null
|
||||||
|
currentKpmPatchEnabled = false
|
||||||
|
currentKpmUndoPatch = false
|
||||||
|
isFlashing = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@@ -78,12 +86,6 @@ 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 scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var logText by rememberSaveable { mutableStateOf("") }
|
var logText by rememberSaveable { mutableStateOf("") }
|
||||||
@@ -109,17 +111,27 @@ fun KernelFlashScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val flashState by horizonKernelState.state.collectAsState()
|
val flashState by horizonKernelState.state.collectAsState()
|
||||||
|
val activity = LocalActivity.current
|
||||||
|
|
||||||
val onFlashComplete = {
|
val onFlashComplete = {
|
||||||
showFloatAction = true
|
showFloatAction = true
|
||||||
KernelFlashStateHolder.isFlashing = false
|
KernelFlashStateHolder.isFlashing = false
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldAutoExit) {
|
// 如果是从外部打开的内核刷写,延迟1.5秒后自动退出
|
||||||
scope.launch {
|
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 (isFromExternalIntent) {
|
||||||
delay(1500)
|
delay(1500)
|
||||||
val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE)
|
KernelFlashStateHolder.clear()
|
||||||
sharedPref.edit { remove("auto_exit_after_flash") }
|
activity.finish()
|
||||||
(context as? ComponentActivity)?.finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,26 +182,17 @@ fun KernelFlashScreen(
|
|||||||
val onBack: () -> Unit = {
|
val onBack: () -> Unit = {
|
||||||
if (!flashState.isFlashing || flashState.isCompleted || flashState.error.isNotEmpty()) {
|
if (!flashState.isFlashing || flashState.isCompleted || flashState.error.isNotEmpty()) {
|
||||||
if (flashState.isCompleted || flashState.error.isNotEmpty()) {
|
if (flashState.isCompleted || flashState.error.isNotEmpty()) {
|
||||||
KernelFlashStateHolder.currentState = null
|
KernelFlashStateHolder.clear()
|
||||||
KernelFlashStateHolder.currentUri = null
|
|
||||||
KernelFlashStateHolder.currentSlot = null
|
|
||||||
KernelFlashStateHolder.currentKpmPatchEnabled = false
|
|
||||||
KernelFlashStateHolder.currentKpmUndoPatch = false
|
|
||||||
KernelFlashStateHolder.isFlashing = false
|
|
||||||
}
|
}
|
||||||
navigator.popBackStack()
|
navigator.popBackStack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(shouldAutoExit) {
|
// 清理状态
|
||||||
|
DisposableEffect(Unit) {
|
||||||
onDispose {
|
onDispose {
|
||||||
if (shouldAutoExit) {
|
if (flashState.isCompleted || flashState.error.isNotEmpty()) {
|
||||||
KernelFlashStateHolder.currentState = null
|
KernelFlashStateHolder.clear()
|
||||||
KernelFlashStateHolder.currentUri = null
|
|
||||||
KernelFlashStateHolder.currentSlot = null
|
|
||||||
KernelFlashStateHolder.currentKpmPatchEnabled = false
|
|
||||||
KernelFlashStateHolder.currentKpmUndoPatch = false
|
|
||||||
KernelFlashStateHolder.isFlashing = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,14 +277,14 @@ private fun FlashProgressIndicator(
|
|||||||
kpmPatchEnabled: Boolean = false,
|
kpmPatchEnabled: Boolean = false,
|
||||||
kpmUndoPatch: Boolean = false
|
kpmUndoPatch: Boolean = false
|
||||||
) {
|
) {
|
||||||
val progressColor = when {
|
val statusColor = when {
|
||||||
flashState.error.isNotEmpty() -> colorScheme.error
|
flashState.error.isNotEmpty() -> colorScheme.error
|
||||||
flashState.isCompleted -> colorScheme.secondary
|
flashState.isCompleted -> colorScheme.primary
|
||||||
else -> colorScheme.primary
|
else -> colorScheme.primary
|
||||||
}
|
}
|
||||||
|
|
||||||
val progress = animateFloatAsState(
|
val progress = animateFloatAsState(
|
||||||
targetValue = flashState.progress,
|
targetValue = flashState.progress.coerceIn(0f, 1f),
|
||||||
label = "FlashProgress"
|
label = "FlashProgress"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -306,8 +309,9 @@ private fun FlashProgressIndicator(
|
|||||||
flashState.isCompleted -> stringResource(R.string.flash_success)
|
flashState.isCompleted -> stringResource(R.string.flash_success)
|
||||||
else -> stringResource(R.string.flashing)
|
else -> stringResource(R.string.flashing)
|
||||||
},
|
},
|
||||||
fontWeight = FontWeight.Bold,
|
fontSize = MiuixTheme.textStyles.title4.fontSize,
|
||||||
color = progressColor
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = statusColor
|
||||||
)
|
)
|
||||||
|
|
||||||
when {
|
when {
|
||||||
@@ -322,7 +326,7 @@ private fun FlashProgressIndicator(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.CheckCircle,
|
imageVector = Icons.Default.CheckCircle,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = colorScheme.secondary
|
tint = colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -330,67 +334,44 @@ private fun FlashProgressIndicator(
|
|||||||
|
|
||||||
// KPM状态显示
|
// KPM状态显示
|
||||||
if (kpmPatchEnabled || kpmUndoPatch) {
|
if (kpmPatchEnabled || kpmUndoPatch) {
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = if (kpmUndoPatch) stringResource(R.string.kpm_undo_patch_mode)
|
text = if (kpmUndoPatch) stringResource(R.string.kpm_undo_patch_mode)
|
||||||
else stringResource(R.string.kpm_patch_mode),
|
else stringResource(R.string.kpm_patch_mode),
|
||||||
color = colorScheme.secondary
|
fontSize = MiuixTheme.textStyles.body2.fontSize,
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
if (flashState.currentStep.isNotEmpty()) {
|
|
||||||
Text(
|
|
||||||
text = flashState.currentStep,
|
|
||||||
color = colorScheme.onSurfaceVariantSummary
|
color = colorScheme.onSurfaceVariantSummary
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val progressFraction = progress.value.coerceIn(0f, 1f)
|
if (flashState.currentStep.isNotEmpty()) {
|
||||||
Box(
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
modifier = Modifier
|
Text(
|
||||||
.fillMaxWidth()
|
text = flashState.currentStep,
|
||||||
.height(8.dp)
|
fontSize = MiuixTheme.textStyles.body2.fontSize,
|
||||||
.clip(RoundedCornerShape(999.dp))
|
color = colorScheme.onSurfaceVariantSummary
|
||||||
.background(colorScheme.surfaceVariant)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
.fillMaxWidth(progressFraction)
|
|
||||||
.clip(RoundedCornerShape(999.dp))
|
|
||||||
.background(progressColor)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = progress.value,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
if (flashState.error.isNotEmpty()) {
|
if (flashState.error.isNotEmpty()) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(12.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))
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = flashState.error,
|
text = flashState.error,
|
||||||
|
fontSize = MiuixTheme.textStyles.body2.fontSize,
|
||||||
color = colorScheme.onErrorContainer,
|
color = colorScheme.onErrorContainer,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp)
|
||||||
.background(
|
.background(
|
||||||
colorScheme.errorContainer.copy(alpha = 0.8f)
|
colorScheme.errorContainer
|
||||||
)
|
)
|
||||||
.padding(8.dp)
|
.padding(12.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,17 @@ import androidx.compose.material.icons.filled.SdStorage
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.sukisu.ultra.R
|
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.Icon
|
||||||
import top.yukonga.miuix.kmp.basic.Text
|
import top.yukonga.miuix.kmp.basic.Text
|
||||||
import top.yukonga.miuix.kmp.basic.TextButton
|
import top.yukonga.miuix.kmp.basic.TextButton
|
||||||
@@ -35,11 +40,13 @@ fun SlotSelectionDialog(
|
|||||||
var selectedSlot by remember { mutableStateOf<String?>(null) }
|
var selectedSlot by remember { mutableStateOf<String?>(null) }
|
||||||
val showDialog = remember { mutableStateOf(show) }
|
val showDialog = remember { mutableStateOf(show) }
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
LaunchedEffect(show) {
|
LaunchedEffect(show) {
|
||||||
showDialog.value = show
|
showDialog.value = show
|
||||||
if (show) {
|
if (show) {
|
||||||
try {
|
try {
|
||||||
currentSlot = getCurrentSlot()
|
currentSlot = withContext(Dispatchers.IO) { getCurrentSlot() }
|
||||||
// 设置默认选择为当前槽位
|
// 设置默认选择为当前槽位
|
||||||
selectedSlot = when (currentSlot) {
|
selectedSlot = when (currentSlot) {
|
||||||
"a" -> "a"
|
"a" -> "a"
|
||||||
@@ -48,7 +55,7 @@ fun SlotSelectionDialog(
|
|||||||
}
|
}
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
errorMessage = e.message
|
errorMessage = context.getString(R.string.operation_failed)
|
||||||
currentSlot = null
|
currentSlot = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,9 +92,9 @@ fun SlotSelectionDialog(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 24.dp, vertical = 8.dp),
|
.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||||
text = "Error: $errorMessage",
|
text = errorMessage ?: context.getString(R.string.operation_failed),
|
||||||
fontSize = MiuixTheme.textStyles.body2.fontSize,
|
fontSize = MiuixTheme.textStyles.body2.fontSize,
|
||||||
color = colorScheme.primary,
|
color = colorScheme.error,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -97,7 +104,7 @@ fun SlotSelectionDialog(
|
|||||||
.padding(horizontal = 24.dp, vertical = 8.dp),
|
.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||||
text = stringResource(
|
text = stringResource(
|
||||||
id = R.string.current_slot,
|
id = R.string.current_slot,
|
||||||
currentSlot ?: "Unknown"
|
currentSlot?.uppercase() ?: context.getString(R.string.not_supported)
|
||||||
),
|
),
|
||||||
fontSize = MiuixTheme.textStyles.body2.fontSize,
|
fontSize = MiuixTheme.textStyles.body2.fontSize,
|
||||||
color = colorScheme.onSurfaceVariantSummary,
|
color = colorScheme.onSurfaceVariantSummary,
|
||||||
@@ -194,25 +201,16 @@ data class SlotOption(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Utility function to get current slot
|
// Utility function to get current slot
|
||||||
private fun getCurrentSlot(): String? {
|
private suspend 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"
|
|
||||||
return try {
|
return try {
|
||||||
val process = ProcessBuilder("su").start()
|
val shell = getRootShell()
|
||||||
process.outputStream.bufferedWriter().use { writer ->
|
val result = ShellUtils.fastCmd(shell, "getprop ro.boot.slot_suffix").trim()
|
||||||
writer.write("$cmd\n")
|
if (result.startsWith("_")) {
|
||||||
writer.write("exit\n")
|
result.substring(1)
|
||||||
writer.flush()
|
} else {
|
||||||
}
|
result
|
||||||
process.inputStream.bufferedReader().use { reader ->
|
}.takeIf { it.isNotEmpty() }
|
||||||
reader.readText().trim()
|
} catch (e: Exception) {
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import androidx.documentfile.provider.DocumentFile
|
|||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
import com.sukisu.ultra.ui.kernelFlash.util.AssetsUtil
|
import com.sukisu.ultra.ui.kernelFlash.util.AssetsUtil
|
||||||
import com.sukisu.ultra.ui.kernelFlash.util.RemoteToolsDownloader
|
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.install
|
||||||
import com.sukisu.ultra.ui.util.rootAvailable
|
import com.sukisu.ultra.ui.util.rootAvailable
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -74,10 +75,6 @@ class HorizonKernelState {
|
|||||||
fun completeFlashing() {
|
fun completeFlashing() {
|
||||||
_state.update { it.copy(isCompleted = true, progress = 1f) }
|
_state.update { it.copy(isCompleted = true, progress = 1f) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reset() {
|
|
||||||
_state.value = FlashState()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class HorizonKernelWorker(
|
class HorizonKernelWorker(
|
||||||
@@ -157,7 +154,12 @@ class HorizonKernelWorker(
|
|||||||
if (isAbDevice && slot != null) {
|
if (isAbDevice && slot != null) {
|
||||||
state.updateStep(context.getString(R.string.horizon_getting_original_slot))
|
state.updateStep(context.getString(R.string.horizon_getting_original_slot))
|
||||||
state.updateProgress(0.72f)
|
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.updateStep(context.getString(R.string.horizon_setting_target_slot))
|
||||||
state.updateProgress(0.74f)
|
state.updateProgress(0.74f)
|
||||||
@@ -308,7 +310,12 @@ class HorizonKernelWorker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 查找Image文件
|
// 查找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()) {
|
if (findImageResult.isBlank()) {
|
||||||
throw IOException(context.getString(R.string.kpm_image_file_not_found))
|
throw IOException(context.getString(R.string.kpm_image_file_not_found))
|
||||||
}
|
}
|
||||||
@@ -398,11 +405,16 @@ class HorizonKernelWorker(
|
|||||||
|
|
||||||
// 检查设备是否为AB分区设备
|
// 检查设备是否为AB分区设备
|
||||||
private fun isAbDevice(): Boolean {
|
private fun isAbDevice(): Boolean {
|
||||||
val abUpdate = runCommandGetOutput("getprop ro.build.ab_update")
|
return try {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val abUpdate = ShellUtils.fastCmd(shell, "getprop ro.build.ab_update").trim()
|
||||||
if (!abUpdate.toBoolean()) return false
|
if (!abUpdate.toBoolean()) return false
|
||||||
|
|
||||||
val slotSuffix = runCommandGetOutput("getprop ro.boot.slot_suffix")
|
val slotSuffix = ShellUtils.fastCmd(shell, "getprop ro.boot.slot_suffix").trim()
|
||||||
return slotSuffix.isNotEmpty()
|
slotSuffix.isNotEmpty()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cleanup() {
|
private fun cleanup() {
|
||||||
@@ -429,7 +441,12 @@ class HorizonKernelWorker(
|
|||||||
|
|
||||||
@SuppressLint("StringFormatInvalid")
|
@SuppressLint("StringFormatInvalid")
|
||||||
private fun patch() {
|
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 versionRegex = """\d+\.\d+\.\d+""".toRegex()
|
||||||
val version = kernelVersion.let { versionRegex.find(it) }?.value ?: ""
|
val version = kernelVersion.let { versionRegex.find(it) }?.value ?: ""
|
||||||
val toolName = if (version.isNotEmpty()) {
|
val toolName = if (version.isNotEmpty()) {
|
||||||
@@ -447,7 +464,9 @@ class HorizonKernelWorker(
|
|||||||
val toolPath = "${context.filesDir.absolutePath}/mkbootfs"
|
val toolPath = "${context.filesDir.absolutePath}/mkbootfs"
|
||||||
AssetsUtil.exportFiles(context, "$toolName-mkbootfs", toolPath)
|
AssetsUtil.exportFiles(context, "$toolName-mkbootfs", toolPath)
|
||||||
state.addLog("${context.getString(R.string.kernel_version_log, version)} ${context.getString(R.string.tool_version_log, toolName)}")
|
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() {
|
private fun flash() {
|
||||||
@@ -517,8 +536,4 @@ class HorizonKernelWorker(
|
|||||||
process.destroy()
|
process.destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun runCommandGetOutput(cmd: String): String {
|
|
||||||
return Shell.cmd(cmd).exec().out.joinToString("\n").trim()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,8 @@ package com.sukisu.ultra.ui.kernelFlash.util
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.sukisu.ultra.ui.util.getRootShell
|
||||||
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
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 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 CONNECTION_TIMEOUT = 10000
|
||||||
private const val READ_TIMEOUT = 30000 // 30秒读取超时
|
private const val READ_TIMEOUT = 20000
|
||||||
|
|
||||||
// 最大重试次数
|
// 最大重试次数
|
||||||
private const val MAX_RETRY_COUNT = 3
|
private const val MAX_RETRY_COUNT = 3
|
||||||
@@ -48,47 +50,26 @@ class RemoteToolsDownloader(
|
|||||||
|
|
||||||
|
|
||||||
suspend fun downloadToolsAsync(listener: DownloadProgressListener?): Map<String, DownloadResult> = withContext(Dispatchers.IO) {
|
suspend fun downloadToolsAsync(listener: DownloadProgressListener?): Map<String, DownloadResult> = withContext(Dispatchers.IO) {
|
||||||
val results = mutableMapOf<String, DownloadResult>()
|
|
||||||
|
|
||||||
listener?.onLog("Starting to prepare KPM tool files...")
|
listener?.onLog("Starting to prepare KPM tool files...")
|
||||||
|
|
||||||
try {
|
|
||||||
// 确保工作目录存在
|
|
||||||
File(workDir).mkdirs()
|
File(workDir).mkdirs()
|
||||||
|
|
||||||
// 并行下载两个工具文件
|
// 并行下载两个工具文件
|
||||||
val kptoolsDeferred = async { downloadSingleTool("kptools", KPTOOLS_REMOTE_URL, listener) }
|
val results = mapOf(
|
||||||
val kpimgDeferred = async { downloadSingleTool("kpimg", KPIMG_REMOTE_URL, listener) }
|
"kptools" to async { downloadSingleTool("kptools", KPTOOLS_REMOTE_URL, listener) },
|
||||||
|
"kpimg" to async { downloadSingleTool("kpimg", KPIMG_REMOTE_URL, listener) }
|
||||||
|
).mapValues { it.value.await() }
|
||||||
|
|
||||||
// 等待所有下载完成
|
// 设置 kptools 执行权限
|
||||||
results["kptools"] = kptoolsDeferred.await()
|
File(workDir, "kptools").takeIf { it.exists() }?.let { file ->
|
||||||
results["kpimg"] = kpimgDeferred.await()
|
setExecutablePermission(file.absolutePath)
|
||||||
|
|
||||||
// 检查kptools执行权限
|
|
||||||
val kptoolsFile = File(workDir, "kptools")
|
|
||||||
if (kptoolsFile.exists()) {
|
|
||||||
setExecutablePermission(kptoolsFile.absolutePath)
|
|
||||||
listener?.onLog("Set kptools execution permission")
|
listener?.onLog("Set kptools execution permission")
|
||||||
}
|
}
|
||||||
|
|
||||||
val successCount = results.values.count { it.success }
|
val successCount = results.values.count { it.success }
|
||||||
val remoteCount = results.values.count { it.success && it.isRemoteSource }
|
val remoteCount = results.values.count { it.success && it.isRemoteSource }
|
||||||
|
|
||||||
listener?.onLog("KPM tools preparation completed: Success $successCount/2, Remote downloaded $remoteCount")
|
listener?.onLog("KPM tools preparation completed: Success $successCount/2, Remote downloaded $remoteCount")
|
||||||
|
|
||||||
} catch (e: Exception) {
|
results
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
results.toMap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun downloadSingleTool(
|
private suspend fun downloadSingleTool(
|
||||||
@@ -96,43 +77,38 @@ class RemoteToolsDownloader(
|
|||||||
remoteUrl: String?,
|
remoteUrl: String?,
|
||||||
listener: DownloadProgressListener?
|
listener: DownloadProgressListener?
|
||||||
): DownloadResult = withContext(Dispatchers.IO) {
|
): DownloadResult = withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
val targetFile = File(workDir, fileName)
|
val targetFile = File(workDir, fileName)
|
||||||
|
|
||||||
if (remoteUrl == null) {
|
if (remoteUrl == null) {
|
||||||
return@withContext useLocalVersion(fileName, targetFile, listener)
|
return@withContext useLocalVersion(fileName, targetFile, listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试从远程下载
|
|
||||||
listener?.onLog("Downloading $fileName from remote repository...")
|
listener?.onLog("Downloading $fileName from remote repository...")
|
||||||
|
|
||||||
var lastError = ""
|
|
||||||
|
|
||||||
// 重试机制
|
// 重试机制
|
||||||
|
var lastError = ""
|
||||||
repeat(MAX_RETRY_COUNT) { attempt ->
|
repeat(MAX_RETRY_COUNT) { attempt ->
|
||||||
try {
|
try {
|
||||||
val result = downloadFromRemote(fileName, remoteUrl, targetFile, listener)
|
val result = downloadFromRemote(fileName, remoteUrl, targetFile, listener)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
listener?.onSuccess(fileName, true)
|
listener?.onSuccess(fileName, true)
|
||||||
return@withContext result
|
return@withContext result
|
||||||
}
|
} else {
|
||||||
lastError = result.errorMessage ?: "Unknown error"
|
lastError = result.errorMessage ?: "Unknown error"
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
lastError = e.message ?: "Network exception"
|
lastError = "Network exception"
|
||||||
Log.w(TAG, "$fileName download attempt ${attempt + 1} failed", e)
|
Log.w(TAG, "$fileName download attempt ${attempt + 1} failed", e)
|
||||||
|
}
|
||||||
|
|
||||||
if (attempt < MAX_RETRY_COUNT - 1) {
|
if (attempt < MAX_RETRY_COUNT - 1) {
|
||||||
listener?.onLog("$fileName download failed, retrying in ${(attempt + 1) * 2} seconds...")
|
listener?.onLog("$fileName download failed, retrying in ${(attempt + 1) * 2} seconds...")
|
||||||
delay(TimeUnit.SECONDS.toMillis((attempt + 1) * 2L))
|
delay(TimeUnit.SECONDS.toMillis((attempt + 1) * 2L))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 所有重试都失败,回退到本地版本
|
|
||||||
listener?.onError(fileName, "Remote download failed: $lastError")
|
listener?.onError(fileName, "Remote download failed: $lastError")
|
||||||
listener?.onLog("$fileName remote download failed, falling back to local version...")
|
listener?.onLog("$fileName remote download failed, falling back to local version...")
|
||||||
|
|
||||||
useLocalVersion(fileName, targetFile, listener)
|
useLocalVersion(fileName, targetFile, listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,15 +118,10 @@ class RemoteToolsDownloader(
|
|||||||
targetFile: File,
|
targetFile: File,
|
||||||
listener: DownloadProgressListener?
|
listener: DownloadProgressListener?
|
||||||
): DownloadResult = withContext(Dispatchers.IO) {
|
): DownloadResult = withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
var connection: HttpURLConnection? = null
|
var connection: HttpURLConnection? = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val url = URL(remoteUrl)
|
connection = (URL(remoteUrl).openConnection() as HttpURLConnection).apply {
|
||||||
connection = url.openConnection() as HttpURLConnection
|
|
||||||
|
|
||||||
// 设置连接参数
|
|
||||||
connection.apply {
|
|
||||||
connectTimeout = CONNECTION_TIMEOUT
|
connectTimeout = CONNECTION_TIMEOUT
|
||||||
readTimeout = READ_TIMEOUT
|
readTimeout = READ_TIMEOUT
|
||||||
requestMethod = "GET"
|
requestMethod = "GET"
|
||||||
@@ -159,22 +130,17 @@ class RemoteToolsDownloader(
|
|||||||
setRequestProperty("Connection", "close")
|
setRequestProperty("Connection", "close")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 建立连接
|
|
||||||
connection.connect()
|
connection.connect()
|
||||||
|
|
||||||
val responseCode = connection.responseCode
|
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||||
if (responseCode != HttpURLConnection.HTTP_OK) {
|
|
||||||
return@withContext DownloadResult(
|
return@withContext DownloadResult(
|
||||||
false,
|
false,
|
||||||
isRemoteSource = false,
|
isRemoteSource = false,
|
||||||
errorMessage = "HTTP error code: $responseCode"
|
errorMessage = "HTTP error code: ${connection.responseCode}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val fileLength = connection.contentLength
|
val fileLength = connection.contentLength
|
||||||
Log.d(TAG, "$fileName remote file size: $fileLength bytes")
|
|
||||||
|
|
||||||
// 创建临时文件
|
|
||||||
val tempFile = File(targetFile.absolutePath + ".tmp")
|
val tempFile = File(targetFile.absolutePath + ".tmp")
|
||||||
|
|
||||||
// 下载文件
|
// 下载文件
|
||||||
@@ -182,40 +148,34 @@ class RemoteToolsDownloader(
|
|||||||
FileOutputStream(tempFile).use { output ->
|
FileOutputStream(tempFile).use { output ->
|
||||||
val buffer = ByteArray(8192)
|
val buffer = ByteArray(8192)
|
||||||
var totalBytes = 0
|
var totalBytes = 0
|
||||||
var bytesRead: Int
|
|
||||||
|
|
||||||
while (input.read(buffer).also { bytesRead = it } != -1) {
|
while (true) {
|
||||||
// 检查协程是否被取消
|
|
||||||
ensureActive()
|
ensureActive()
|
||||||
|
val bytesRead = input.read(buffer)
|
||||||
|
if (bytesRead == -1) break
|
||||||
|
|
||||||
output.write(buffer, 0, bytesRead)
|
output.write(buffer, 0, bytesRead)
|
||||||
totalBytes += bytesRead
|
totalBytes += bytesRead
|
||||||
|
|
||||||
// 更新下载进度
|
|
||||||
if (fileLength > 0) {
|
if (fileLength > 0) {
|
||||||
listener?.onProgress(fileName, totalBytes, fileLength)
|
listener?.onProgress(fileName, totalBytes, fileLength)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
output.flush()
|
output.flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证下载的文件
|
// 验证并移动文件
|
||||||
if (!validateDownloadedFile(tempFile, fileName)) {
|
if (!validateDownloadedFile(tempFile, fileName)) {
|
||||||
tempFile.delete()
|
tempFile.delete()
|
||||||
return@withContext DownloadResult(
|
return@withContext DownloadResult(
|
||||||
success = false,
|
false,
|
||||||
isRemoteSource = false,
|
isRemoteSource = false,
|
||||||
errorMessage = "File verification failed"
|
errorMessage = "File verification failed"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移动临时文件到目标位置
|
|
||||||
if (targetFile.exists()) {
|
|
||||||
targetFile.delete()
|
targetFile.delete()
|
||||||
}
|
|
||||||
|
|
||||||
if (!tempFile.renameTo(targetFile)) {
|
if (!tempFile.renameTo(targetFile)) {
|
||||||
tempFile.delete()
|
tempFile.delete()
|
||||||
return@withContext DownloadResult(
|
return@withContext DownloadResult(
|
||||||
@@ -227,7 +187,6 @@ class RemoteToolsDownloader(
|
|||||||
|
|
||||||
Log.i(TAG, "$fileName remote download successful, file size: ${targetFile.length()} bytes")
|
Log.i(TAG, "$fileName remote download successful, file size: ${targetFile.length()} bytes")
|
||||||
listener?.onLog("$fileName remote download successful")
|
listener?.onLog("$fileName remote download successful")
|
||||||
|
|
||||||
DownloadResult(true, isRemoteSource = true)
|
DownloadResult(true, isRemoteSource = true)
|
||||||
|
|
||||||
} catch (e: SocketTimeoutException) {
|
} catch (e: SocketTimeoutException) {
|
||||||
@@ -235,16 +194,10 @@ class RemoteToolsDownloader(
|
|||||||
DownloadResult(false, isRemoteSource = false, errorMessage = "Connection timeout")
|
DownloadResult(false, isRemoteSource = false, errorMessage = "Connection timeout")
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.w(TAG, "$fileName network IO exception", e)
|
Log.w(TAG, "$fileName network IO exception", e)
|
||||||
DownloadResult(false,
|
DownloadResult(false, isRemoteSource = false, errorMessage = "Network exception: ${e.message}")
|
||||||
isRemoteSource = false,
|
|
||||||
errorMessage = "Network connection exception: ${e.message}"
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "$fileName exception occurred during download", e)
|
Log.e(TAG, "$fileName exception occurred during download", e)
|
||||||
DownloadResult(false,
|
DownloadResult(false, isRemoteSource = false, errorMessage = "Download exception: ${e.message}")
|
||||||
isRemoteSource = false,
|
|
||||||
errorMessage = "Download exception: ${e.message}"
|
|
||||||
)
|
|
||||||
} finally {
|
} finally {
|
||||||
connection?.disconnect()
|
connection?.disconnect()
|
||||||
}
|
}
|
||||||
@@ -255,61 +208,42 @@ class RemoteToolsDownloader(
|
|||||||
targetFile: File,
|
targetFile: File,
|
||||||
listener: DownloadProgressListener?
|
listener: DownloadProgressListener?
|
||||||
): DownloadResult = withContext(Dispatchers.IO) {
|
): DownloadResult = withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
AssetsUtil.exportFiles(context, fileName, targetFile.absolutePath)
|
AssetsUtil.exportFiles(context, fileName, targetFile.absolutePath)
|
||||||
|
|
||||||
if (!targetFile.exists()) {
|
if (!targetFile.exists() || !validateDownloadedFile(targetFile, fileName)) {
|
||||||
val errorMsg = "Local $fileName file extraction failed"
|
val errorMsg = if (!targetFile.exists()) {
|
||||||
listener?.onError(fileName, errorMsg)
|
"Local $fileName file extraction failed"
|
||||||
return@withContext DownloadResult(false,
|
} else {
|
||||||
isRemoteSource = false,
|
"Local $fileName file verification failed"
|
||||||
errorMessage = errorMsg
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateDownloadedFile(targetFile, fileName)) {
|
|
||||||
val errorMsg = "Local $fileName file verification failed"
|
|
||||||
listener?.onError(fileName, errorMsg)
|
listener?.onError(fileName, errorMsg)
|
||||||
return@withContext DownloadResult(
|
return@withContext DownloadResult(false, isRemoteSource = false, errorMessage = errorMsg)
|
||||||
success = false,
|
|
||||||
isRemoteSource = false,
|
|
||||||
errorMessage = errorMsg
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(TAG, "$fileName local version loaded successfully, file size: ${targetFile.length()} bytes")
|
Log.i(TAG, "$fileName local version loaded successfully, file size: ${targetFile.length()} bytes")
|
||||||
listener?.onLog("$fileName local version loaded successfully")
|
listener?.onLog("$fileName local version loaded successfully")
|
||||||
listener?.onSuccess(fileName, false)
|
listener?.onSuccess(fileName, false)
|
||||||
|
|
||||||
DownloadResult(true, isRemoteSource = false)
|
DownloadResult(true, isRemoteSource = false)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "$fileName local version loading failed", e)
|
Log.e(TAG, "$fileName local version loading failed", e)
|
||||||
val errorMsg = "Local version loading failed: ${e.message}"
|
val errorMsg = "Local version loading failed: ${e.message}"
|
||||||
listener?.onError(fileName, errorMsg)
|
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 {
|
private fun validateDownloadedFile(file: File, fileName: String): Boolean {
|
||||||
if (!file.exists()) {
|
if (!file.exists() || file.length() < MIN_FILE_SIZE) {
|
||||||
Log.w(TAG, "$fileName file does not exist")
|
Log.w(TAG, "$fileName file validation failed: exists=${file.exists()}, size=${file.length()}")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val fileSize = file.length()
|
return try {
|
||||||
if (fileSize < MIN_FILE_SIZE) {
|
|
||||||
Log.w(TAG, "$fileName file is too small: $fileSize bytes")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
file.inputStream().use { input ->
|
file.inputStream().use { input ->
|
||||||
val header = ByteArray(4)
|
val header = ByteArray(4)
|
||||||
val bytesRead = input.read(header)
|
if (input.read(header) < 4) {
|
||||||
|
|
||||||
if (bytesRead < 4) {
|
|
||||||
Log.w(TAG, "$fileName file header read incomplete")
|
Log.w(TAG, "$fileName file header read incomplete")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -324,20 +258,24 @@ class RemoteToolsDownloader(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "$fileName file verification passed, size: $fileSize bytes, ELF: $isELF")
|
Log.d(TAG, "$fileName file verification passed, size: ${file.length()} bytes, ELF: $isELF")
|
||||||
return true
|
true
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "$fileName file verification exception", e)
|
Log.w(TAG, "$fileName file verification exception", e)
|
||||||
return false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setExecutablePermission(filePath: String) {
|
private fun setExecutablePermission(filePath: String) {
|
||||||
try {
|
try {
|
||||||
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "chmod a+rx $filePath"))
|
val shell = getRootShell()
|
||||||
process.waitFor()
|
if (ShellUtils.fastCmdResult(shell, "chmod a+rx $filePath")) {
|
||||||
Log.d(TAG, "Set execution permission for $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) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Failed to set execution permission: $filePath", e)
|
Log.w(TAG, "Failed to set execution permission: $filePath", e)
|
||||||
try {
|
try {
|
||||||
@@ -351,12 +289,10 @@ class RemoteToolsDownloader(
|
|||||||
|
|
||||||
fun cleanup() {
|
fun cleanup() {
|
||||||
try {
|
try {
|
||||||
File(workDir).listFiles()?.forEach { file ->
|
File(workDir).listFiles()?.filter { it.name.endsWith(".tmp") }?.forEach { file ->
|
||||||
if (file.name.endsWith(".tmp")) {
|
|
||||||
file.delete()
|
file.delete()
|
||||||
Log.d(TAG, "Cleaned temporary file: ${file.name}")
|
Log.d(TAG, "Cleaned temporary file: ${file.name}")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Failed to clean temporary files", e)
|
Log.w(TAG, "Failed to clean temporary files", e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.sukisu.ultra.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
@@ -37,6 +38,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.key.Key
|
import androidx.compose.ui.input.key.Key
|
||||||
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.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.res.stringResource
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.compose.dropUnlessResumed
|
import androidx.lifecycle.compose.dropUnlessResumed
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
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
|
||||||
@@ -115,6 +118,7 @@ fun FlashScreen(
|
|||||||
var showFloatAction by rememberSaveable { mutableStateOf(false) }
|
var showFloatAction by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val activity = LocalActivity.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
var flashing by rememberSaveable {
|
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(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopBar(
|
TopBar(
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ fun InstallScreen(
|
|||||||
|
|
||||||
// AnyKernel3 刷写
|
// AnyKernel3 刷写
|
||||||
(installMethod as? InstallMethod.HorizonKernel)?.let { method ->
|
(installMethod as? InstallMethod.HorizonKernel)?.let { method ->
|
||||||
if (method.slot != null) {
|
if (isAbDevice && method.slot != null) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -374,11 +374,13 @@ fun InstallScreen(
|
|||||||
if (method.slot == "a") stringResource(id = R.string.slot_a)
|
if (method.slot == "a") stringResource(id = R.string.slot_a)
|
||||||
else stringResource(id = R.string.slot_b)
|
else stringResource(id = R.string.slot_b)
|
||||||
),
|
),
|
||||||
onClick = {},
|
onClick = {
|
||||||
|
anyKernel3State.onReopenSlotDialog(method)
|
||||||
|
},
|
||||||
leftAction = {
|
leftAction = {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.SdStorage,
|
Icons.Filled.SdStorage,
|
||||||
tint = colorScheme.onSurface,
|
tint = colorScheme.primary,
|
||||||
modifier = Modifier.padding(end = 16.dp),
|
modifier = Modifier.padding(end = 16.dp),
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
@@ -388,7 +390,6 @@ fun InstallScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// KPM 状态显示
|
// KPM 状态显示
|
||||||
if (kpmPatchOption != KpmPatchOption.FOLLOW_KERNEL) {
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -398,16 +399,19 @@ fun InstallScreen(
|
|||||||
title = when (kpmPatchOption) {
|
title = when (kpmPatchOption) {
|
||||||
KpmPatchOption.PATCH_KPM -> stringResource(R.string.kpm_patch_enabled)
|
KpmPatchOption.PATCH_KPM -> stringResource(R.string.kpm_patch_enabled)
|
||||||
KpmPatchOption.UNDO_PATCH_KPM -> stringResource(R.string.kpm_undo_patch_enabled)
|
KpmPatchOption.UNDO_PATCH_KPM -> stringResource(R.string.kpm_undo_patch_enabled)
|
||||||
else -> ""
|
KpmPatchOption.FOLLOW_KERNEL -> stringResource(R.string.kpm_follow_kernel_file)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
anyKernel3State.onReopenKpmDialog(method)
|
||||||
},
|
},
|
||||||
onClick = {},
|
|
||||||
leftAction = {
|
leftAction = {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Security,
|
Icons.Filled.Security,
|
||||||
tint = if (kpmPatchOption == KpmPatchOption.PATCH_KPM)
|
tint = when (kpmPatchOption) {
|
||||||
colorScheme.primary
|
KpmPatchOption.PATCH_KPM -> colorScheme.primary
|
||||||
else
|
KpmPatchOption.UNDO_PATCH_KPM -> colorScheme.secondary
|
||||||
colorScheme.secondary,
|
KpmPatchOption.FOLLOW_KERNEL -> colorScheme.onSurfaceVariantSummary
|
||||||
|
},
|
||||||
modifier = Modifier.padding(end = 16.dp),
|
modifier = Modifier.padding(end = 16.dp),
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
@@ -415,7 +419,6 @@ fun InstallScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|||||||
@@ -368,4 +368,12 @@
|
|||||||
<string name="allowlist_restore_summary_picker">选择备份文件进行导入</string>
|
<string name="allowlist_restore_summary_picker">选择备份文件进行导入</string>
|
||||||
<string name="allowlist_restore_success">还原成功,重启生效</string>
|
<string name="allowlist_restore_success">还原成功,重启生效</string>
|
||||||
<string name="allowlist_restore_failed">还原失败</string>
|
<string name="allowlist_restore_failed">还原失败</string>
|
||||||
|
<!-- Zip File Type Labels -->
|
||||||
|
<string name="zip_type_module"> (模块)</string>
|
||||||
|
<string name="zip_type_kernel"> (内核)</string>
|
||||||
|
<string name="zip_type_unknown"> (未知)</string>
|
||||||
|
<string name="zip_file_unknown">unknown.zip</string>
|
||||||
|
<!-- Zip Install Prompts -->
|
||||||
|
<string name="kernel_install_prompt_with_name">将安装以下内核:%1$s</string>
|
||||||
|
<string name="mixed_install_prompt_with_name">将安装以下文件:%1$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -376,4 +376,12 @@
|
|||||||
<string name="allowlist_restore_summary_picker">Choose a backup file to import</string>
|
<string name="allowlist_restore_summary_picker">Choose a backup file to import</string>
|
||||||
<string name="allowlist_restore_success">Restore succeeded</string>
|
<string name="allowlist_restore_success">Restore succeeded</string>
|
||||||
<string name="allowlist_restore_failed">Restore failed</string>
|
<string name="allowlist_restore_failed">Restore failed</string>
|
||||||
|
<!-- Zip File Type Labels -->
|
||||||
|
<string name="zip_type_module"> (Module)</string>
|
||||||
|
<string name="zip_type_kernel"> (Kernel)</string>
|
||||||
|
<string name="zip_type_unknown"> (Unknown)</string>
|
||||||
|
<string name="zip_file_unknown">unknown.zip</string>
|
||||||
|
<!-- Zip Install Prompts -->
|
||||||
|
<string name="kernel_install_prompt_with_name">The following kernels will be installed: %1$s</string>
|
||||||
|
<string name="mixed_install_prompt_with_name">The following files will be installed: %1$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user