Improve the ui and function of the anykernel3 flashing interface.
- Add self-selected brushwrite A/B slot (not perfect) Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
This commit is contained in:
372
manager/app/src/main/java/com/sukisu/ultra/flash/KernelFlash.kt
Normal file
372
manager/app/src/main/java/com/sukisu/ultra/flash/KernelFlash.kt
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
package com.sukisu.ultra.flash
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
|
import androidx.compose.material.icons.filled.Error
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.utils.AssetsUtil
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
data class FlashState(
|
||||||
|
val isFlashing: Boolean = false,
|
||||||
|
val isCompleted: Boolean = false,
|
||||||
|
val progress: Float = 0f,
|
||||||
|
val currentStep: String = "",
|
||||||
|
val logs: List<String> = emptyList(),
|
||||||
|
val error: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
class HorizonKernelState {
|
||||||
|
private val _state = MutableStateFlow(FlashState())
|
||||||
|
val state: StateFlow<FlashState> = _state.asStateFlow()
|
||||||
|
|
||||||
|
fun updateProgress(progress: Float) {
|
||||||
|
_state.update { it.copy(progress = progress) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateStep(step: String) {
|
||||||
|
_state.update { it.copy(currentStep = step) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addLog(log: String) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(logs = it.logs + log)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setError(error: String) {
|
||||||
|
_state.update { it.copy(error = error) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startFlashing() {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isFlashing = true,
|
||||||
|
isCompleted = false,
|
||||||
|
progress = 0f,
|
||||||
|
currentStep = "under preparation...",
|
||||||
|
logs = emptyList(),
|
||||||
|
error = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun completeFlashing() {
|
||||||
|
_state.update { it.copy(isCompleted = true, progress = 1f) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
_state.value = FlashState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HorizonKernelWorker(
|
||||||
|
private val context: Context,
|
||||||
|
private val state: HorizonKernelState,
|
||||||
|
private val slot: String? = null
|
||||||
|
) : Thread() {
|
||||||
|
var uri: Uri? = null
|
||||||
|
private lateinit var filePath: String
|
||||||
|
private lateinit var binaryPath: String
|
||||||
|
|
||||||
|
private var onFlashComplete: (() -> Unit)? = null
|
||||||
|
|
||||||
|
fun setOnFlashCompleteListener(listener: () -> Unit) {
|
||||||
|
onFlashComplete = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
state.startFlashing()
|
||||||
|
state.updateStep(context.getString(R.string.horizon_preparing))
|
||||||
|
|
||||||
|
filePath = "${context.filesDir.absolutePath}/${DocumentFile.fromSingleUri(context, uri!!)?.name}"
|
||||||
|
binaryPath = "${context.filesDir.absolutePath}/META-INF/com/google/android/update-binary"
|
||||||
|
|
||||||
|
try {
|
||||||
|
state.updateStep(context.getString(R.string.horizon_cleaning_files))
|
||||||
|
state.updateProgress(0.1f)
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
if (!rootAvailable()) {
|
||||||
|
state.setError(context.getString(R.string.root_required))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.updateStep(context.getString(R.string.horizon_copying_files))
|
||||||
|
state.updateProgress(0.2f)
|
||||||
|
copy()
|
||||||
|
|
||||||
|
if (!File(filePath).exists()) {
|
||||||
|
state.setError(context.getString(R.string.horizon_copy_failed))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.updateStep(context.getString(R.string.horizon_extracting_tool))
|
||||||
|
state.updateProgress(0.4f)
|
||||||
|
getBinary()
|
||||||
|
|
||||||
|
state.updateStep(context.getString(R.string.horizon_patching_script))
|
||||||
|
state.updateProgress(0.6f)
|
||||||
|
patch()
|
||||||
|
|
||||||
|
state.updateStep(context.getString(R.string.horizon_flashing))
|
||||||
|
state.updateProgress(0.7f)
|
||||||
|
flash()
|
||||||
|
|
||||||
|
state.updateStep(context.getString(R.string.horizon_flash_complete_status))
|
||||||
|
state.completeFlashing()
|
||||||
|
|
||||||
|
(context as? Activity)?.runOnUiThread {
|
||||||
|
onFlashComplete?.invoke()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
state.setError(e.message ?: context.getString(R.string.horizon_unknown_error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanup() {
|
||||||
|
runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copy() {
|
||||||
|
uri?.let { safeUri ->
|
||||||
|
context.contentResolver.openInputStream(safeUri)?.use { input ->
|
||||||
|
FileOutputStream(File(filePath)).use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBinary() {
|
||||||
|
runCommand(false, "unzip \"$filePath\" \"*/update-binary\" -d ${context.filesDir.absolutePath}")
|
||||||
|
if (!File(binaryPath).exists()) {
|
||||||
|
throw IOException("Failed to extract update-binary")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun patch() {
|
||||||
|
val mkbootfsPath = "${context.filesDir.absolutePath}/mkbootfs"
|
||||||
|
AssetsUtil.exportFiles(context, "mkbootfs", mkbootfsPath)
|
||||||
|
runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $mkbootfsPath \$AKHOME/tools;' $binaryPath")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun flash() {
|
||||||
|
val process = ProcessBuilder("su")
|
||||||
|
.redirectErrorStream(true)
|
||||||
|
.start()
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.outputStream.bufferedWriter().use { writer ->
|
||||||
|
writer.write("export POSTINSTALL=${context.filesDir.absolutePath}\n")
|
||||||
|
|
||||||
|
// 写入槽位信息到临时文件
|
||||||
|
slot?.let { selectedSlot ->
|
||||||
|
writer.write("echo \"$selectedSlot\" > ${context.filesDir.absolutePath}/bootslot\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建刷写命令
|
||||||
|
val flashCommand = buildString {
|
||||||
|
append("sh $binaryPath 3 1 \"$filePath\"")
|
||||||
|
if (slot != null) {
|
||||||
|
append(" \"$(cat ${context.filesDir.absolutePath}/bootslot)\"")
|
||||||
|
}
|
||||||
|
append(" && touch ${context.filesDir.absolutePath}/done\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.write(flashCommand)
|
||||||
|
writer.write("exit\n")
|
||||||
|
writer.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
process.inputStream.bufferedReader().use { reader ->
|
||||||
|
reader.lineSequence().forEach { line ->
|
||||||
|
if (line.startsWith("ui_print")) {
|
||||||
|
val logMessage = line.removePrefix("ui_print").trim()
|
||||||
|
state.addLog(logMessage)
|
||||||
|
|
||||||
|
when {
|
||||||
|
logMessage.contains("extracting", ignoreCase = true) -> {
|
||||||
|
state.updateProgress(0.75f)
|
||||||
|
}
|
||||||
|
logMessage.contains("installing", ignoreCase = true) -> {
|
||||||
|
state.updateProgress(0.85f)
|
||||||
|
}
|
||||||
|
logMessage.contains("complete", ignoreCase = true) -> {
|
||||||
|
state.updateProgress(0.95f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
process.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File("${context.filesDir.absolutePath}/done").exists()) {
|
||||||
|
throw IOException("Flash failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runCommand(su: Boolean, cmd: String): Int {
|
||||||
|
val process = ProcessBuilder(if (su) "su" else "sh")
|
||||||
|
.redirectErrorStream(true)
|
||||||
|
.start()
|
||||||
|
|
||||||
|
return try {
|
||||||
|
process.outputStream.bufferedWriter().use { writer ->
|
||||||
|
writer.write("$cmd\n")
|
||||||
|
writer.write("exit\n")
|
||||||
|
writer.flush()
|
||||||
|
}
|
||||||
|
process.waitFor()
|
||||||
|
} finally {
|
||||||
|
process.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rootAvailable(): Boolean {
|
||||||
|
return try {
|
||||||
|
val process = Runtime.getRuntime().exec("su -c id")
|
||||||
|
val exitValue = process.waitFor()
|
||||||
|
exitValue == 0
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HorizonKernelFlashProgress(state: FlashState) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.horizon_flash_title),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
progress = { state.progress },
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = state.currentStep,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(vertical = 4.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (state.logs.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.horizon_logs_label),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Start)
|
||||||
|
.padding(top = 8.dp, bottom = 4.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(max = 150.dp)
|
||||||
|
.padding(vertical = 4.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
tonalElevation = 1.dp,
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
state.logs.forEach { log ->
|
||||||
|
Text(
|
||||||
|
text = log,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.padding(vertical = 2.dp),
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.error.isNotEmpty()) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Error,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.padding(end = 8.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = state.error,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (state.isCompleted) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.CheckCircle,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(end = 8.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.horizon_flash_complete),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.ui.theme.ThemeConfig
|
||||||
|
import com.sukisu.ultra.ui.theme.getCardColors
|
||||||
|
import com.sukisu.ultra.ui.theme.getCardElevation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 槽位选择对话框组件
|
||||||
|
* 用于HorizonKernel刷写时选择目标槽位
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SlotSelectionDialog(
|
||||||
|
show: Boolean,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onSlotSelected: (String) -> Unit
|
||||||
|
) {
|
||||||
|
if (show) {
|
||||||
|
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||||
|
ThemeConfig.currentTheme.ButtonContrast
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.select_slot_title),
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.select_slot_description),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = { onSlotSelected("a") },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.slot_a),
|
||||||
|
style = MaterialTheme.typography.labelLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { onSlotSelected("b") },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.slot_b),
|
||||||
|
style = MaterialTheme.typography.labelLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(text = stringResource(id = android.R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = getCardColors(cardColor.copy(alpha = 0.9f)).containerColor.copy(alpha = 0.9f),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
tonalElevation = getCardElevation()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
package com.sukisu.ultra.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.LocalIndication
|
import androidx.compose.foundation.LocalIndication
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
@@ -28,26 +32,24 @@ 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.documentfile.provider.DocumentFile
|
|
||||||
import com.maxkeppeler.sheets.list.models.ListOption
|
import com.maxkeppeler.sheets.list.models.ListOption
|
||||||
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.generated.destinations.FlashScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||||
|
import com.sukisu.ultra.R
|
||||||
import com.sukisu.ultra.ui.component.DialogHandle
|
import com.sukisu.ultra.ui.component.DialogHandle
|
||||||
|
import com.sukisu.ultra.ui.component.SlotSelectionDialog
|
||||||
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
||||||
import com.sukisu.ultra.ui.component.rememberCustomDialog
|
import com.sukisu.ultra.ui.component.rememberCustomDialog
|
||||||
|
import com.sukisu.ultra.flash.HorizonKernelFlashProgress
|
||||||
|
import com.sukisu.ultra.flash.HorizonKernelState
|
||||||
|
import com.sukisu.ultra.flash.HorizonKernelWorker
|
||||||
import com.sukisu.ultra.ui.theme.ThemeConfig
|
import com.sukisu.ultra.ui.theme.ThemeConfig
|
||||||
import com.sukisu.ultra.ui.theme.getCardColors
|
import com.sukisu.ultra.ui.theme.getCardColors
|
||||||
import com.sukisu.ultra.ui.theme.getCardElevation
|
import com.sukisu.ultra.ui.theme.getCardElevation
|
||||||
import com.sukisu.ultra.ui.util.*
|
import com.sukisu.ultra.ui.util.*
|
||||||
import com.sukisu.ultra.R
|
|
||||||
import com.sukisu.ultra.utils.AssetsUtil
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author weishu
|
* @author weishu
|
||||||
@@ -60,8 +62,12 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
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) }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
var showRebootDialog by remember { mutableStateOf(false) }
|
var showRebootDialog by remember { mutableStateOf(false) }
|
||||||
|
var showSlotSelectionDialog by remember { mutableStateOf(false) }
|
||||||
|
var tempKernelUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
|
val horizonKernelState = remember { HorizonKernelState() }
|
||||||
|
val flashState by horizonKernelState.state.collectAsState()
|
||||||
|
val summary = stringResource(R.string.horizon_kernel_summary)
|
||||||
|
|
||||||
val onFlashComplete = {
|
val onFlashComplete = {
|
||||||
showRebootDialog = true
|
showRebootDialog = true
|
||||||
@@ -79,7 +85,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
writer.write("svc power reboot\n")
|
writer.write("svc power reboot\n")
|
||||||
writer.write("exit\n")
|
writer.write("exit\n")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
Toast.makeText(context, R.string.failed_reboot, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, R.string.failed_reboot, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +97,11 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
when (method) {
|
when (method) {
|
||||||
is InstallMethod.HorizonKernel -> {
|
is InstallMethod.HorizonKernel -> {
|
||||||
method.uri?.let { uri ->
|
method.uri?.let { uri ->
|
||||||
val worker = HorizonKernelWorker(context)
|
val worker = HorizonKernelWorker(
|
||||||
|
context = context,
|
||||||
|
state = horizonKernelState,
|
||||||
|
slot = method.slot
|
||||||
|
)
|
||||||
worker.uri = uri
|
worker.uri = uri
|
||||||
worker.setOnFlashCompleteListener(onFlashComplete)
|
worker.setOnFlashCompleteListener(onFlashComplete)
|
||||||
worker.start()
|
worker.start()
|
||||||
@@ -110,6 +120,22 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 槽位选择
|
||||||
|
SlotSelectionDialog(
|
||||||
|
show = showSlotSelectionDialog,
|
||||||
|
onDismiss = { showSlotSelectionDialog = false },
|
||||||
|
onSlotSelected = { slot ->
|
||||||
|
showSlotSelectionDialog = false
|
||||||
|
val horizonMethod = InstallMethod.HorizonKernel(
|
||||||
|
uri = tempKernelUri,
|
||||||
|
slot = slot,
|
||||||
|
summary = summary
|
||||||
|
)
|
||||||
|
installMethod = horizonMethod
|
||||||
|
onInstall()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
val currentKmi by produceState(initialValue = "") {
|
val currentKmi by produceState(initialValue = "") {
|
||||||
value = getCurrentKmi()
|
value = getCurrentKmi()
|
||||||
}
|
}
|
||||||
@@ -165,9 +191,25 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
SelectInstallMethod { method ->
|
SelectInstallMethod(
|
||||||
|
onSelected = { method ->
|
||||||
|
if (method is InstallMethod.HorizonKernel && method.uri != null && method.slot == null) {
|
||||||
|
tempKernelUri = method.uri
|
||||||
|
showSlotSelectionDialog = true
|
||||||
|
} else {
|
||||||
installMethod = method
|
installMethod = method
|
||||||
}
|
}
|
||||||
|
horizonKernelState.reset()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = flashState.isFlashing && installMethod is InstallMethod.HorizonKernel,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically()
|
||||||
|
) {
|
||||||
|
HorizonKernelFlashProgress(flashState)
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -182,9 +224,20 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
(installMethod as? InstallMethod.HorizonKernel)?.let { method ->
|
||||||
|
if (method.slot != null) {
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
id = R.string.selected_slot,
|
||||||
|
if (method.slot == "a") stringResource(id = R.string.slot_a)
|
||||||
|
else stringResource(id = R.string.slot_b)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
enabled = installMethod != null,
|
enabled = installMethod != null && !flashState.isFlashing,
|
||||||
onClick = onClickNext
|
onClick = onClickNext
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -197,7 +250,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RebootDialog(
|
private fun RebootDialog(
|
||||||
show: Boolean,
|
show: Boolean,
|
||||||
@@ -223,133 +275,6 @@ private fun RebootDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private class HorizonKernelWorker(private val context: Context) : Thread() {
|
|
||||||
var uri: Uri? = null
|
|
||||||
private lateinit var filePath: String
|
|
||||||
private lateinit var binaryPath: String
|
|
||||||
|
|
||||||
|
|
||||||
private var onFlashComplete: (() -> Unit)? = null
|
|
||||||
|
|
||||||
fun setOnFlashCompleteListener(listener: () -> Unit) {
|
|
||||||
onFlashComplete = listener
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
filePath = "${context.filesDir.absolutePath}/${DocumentFile.fromSingleUri(context, uri!!)?.name}"
|
|
||||||
binaryPath = "${context.filesDir.absolutePath}/META-INF/com/google/android/update-binary"
|
|
||||||
|
|
||||||
try {
|
|
||||||
cleanup()
|
|
||||||
if (!rootAvailable()) {
|
|
||||||
showError(context.getString(R.string.root_required))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
copy()
|
|
||||||
if (!File(filePath).exists()) {
|
|
||||||
showError(context.getString(R.string.copy_failed))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
getBinary()
|
|
||||||
patch()
|
|
||||||
flash()
|
|
||||||
|
|
||||||
(context as? Activity)?.runOnUiThread {
|
|
||||||
onFlashComplete?.invoke()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError(e.message ?: context.getString(R.string.unknown_error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cleanup() {
|
|
||||||
runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copy() {
|
|
||||||
uri?.let { safeUri ->
|
|
||||||
context.contentResolver.openInputStream(safeUri)?.use { input ->
|
|
||||||
FileOutputStream(File(filePath)).use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBinary() {
|
|
||||||
runCommand(false, "unzip \"$filePath\" \"*/update-binary\" -d ${context.filesDir.absolutePath}")
|
|
||||||
if (!File(binaryPath).exists()) {
|
|
||||||
throw IOException("Failed to extract update-binary")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun patch() {
|
|
||||||
val mkbootfsPath = "${context.filesDir.absolutePath}/mkbootfs"
|
|
||||||
AssetsUtil.exportFiles(context, "mkbootfs", mkbootfsPath)
|
|
||||||
runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $mkbootfsPath \$AKHOME/tools;' $binaryPath")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun flash() {
|
|
||||||
val process = ProcessBuilder("su")
|
|
||||||
.redirectErrorStream(true)
|
|
||||||
.start()
|
|
||||||
|
|
||||||
try {
|
|
||||||
process.outputStream.bufferedWriter().use { writer ->
|
|
||||||
writer.write("export POSTINSTALL=${context.filesDir.absolutePath}\n")
|
|
||||||
writer.write("sh $binaryPath 3 1 \"$filePath\" && touch ${context.filesDir.absolutePath}/done\nexit\n")
|
|
||||||
writer.flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
process.inputStream.bufferedReader().use { reader ->
|
|
||||||
reader.lineSequence().forEach { line ->
|
|
||||||
if (line.startsWith("ui_print")) {
|
|
||||||
showLog(line.removePrefix("ui_print"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
process.destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!File("${context.filesDir.absolutePath}/done").exists()) {
|
|
||||||
throw IOException("Flash failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runCommand(su: Boolean, cmd: String): Int {
|
|
||||||
val process = ProcessBuilder(if (su) "su" else "sh")
|
|
||||||
.redirectErrorStream(true)
|
|
||||||
.start()
|
|
||||||
|
|
||||||
return try {
|
|
||||||
process.outputStream.bufferedWriter().use { writer ->
|
|
||||||
writer.write("$cmd\n")
|
|
||||||
writer.write("exit\n")
|
|
||||||
writer.flush()
|
|
||||||
}
|
|
||||||
process.waitFor()
|
|
||||||
} finally {
|
|
||||||
process.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showError(message: String) {
|
|
||||||
(context as? Activity)?.runOnUiThread {
|
|
||||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showLog(message: String) {
|
|
||||||
(context as? Activity)?.runOnUiThread {
|
|
||||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class InstallMethod {
|
sealed class InstallMethod {
|
||||||
data class SelectFile(
|
data class SelectFile(
|
||||||
val uri: Uri? = null,
|
val uri: Uri? = null,
|
||||||
@@ -369,6 +294,7 @@ sealed class InstallMethod {
|
|||||||
|
|
||||||
data class HorizonKernel(
|
data class HorizonKernel(
|
||||||
val uri: Uri? = null,
|
val uri: Uri? = null,
|
||||||
|
val slot: String? = null,
|
||||||
@StringRes override val label: Int = R.string.horizon_kernel,
|
@StringRes override val label: Int = R.string.horizon_kernel,
|
||||||
override val summary: String? = null
|
override val summary: String? = null
|
||||||
) : InstallMethod()
|
) : InstallMethod()
|
||||||
@@ -402,7 +328,6 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
|
|||||||
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) }
|
||||||
|
|
||||||
|
|
||||||
val selectImageLauncher = rememberLauncherForActivityResult(
|
val selectImageLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.StartActivityForResult()
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -266,4 +266,25 @@
|
|||||||
<string name="image_editor_hint">使用双指缩放图片,单指拖动调整位置</string>
|
<string name="image_editor_hint">使用双指缩放图片,单指拖动调整位置</string>
|
||||||
<string name="background_image_error">无法加载图片</string>
|
<string name="background_image_error">无法加载图片</string>
|
||||||
<string name="reprovision">重置</string>
|
<string name="reprovision">重置</string>
|
||||||
|
<!-- Anykernel3 Kernel刷写进度相关 -->
|
||||||
|
<string name="horizon_flash_title">刷写Anykernel3</string>
|
||||||
|
<string name="horizon_logs_label">日志:</string>
|
||||||
|
<string name="horizon_flash_complete">刷写完成</string>
|
||||||
|
<!-- 刷写状态相关 -->
|
||||||
|
<string name="horizon_preparing">准备中…</string>
|
||||||
|
<string name="horizon_cleaning_files">清理文件…</string>
|
||||||
|
<string name="horizon_copying_files">复制文件…</string>
|
||||||
|
<string name="horizon_extracting_tool">提取刷写工具…</string>
|
||||||
|
<string name="horizon_patching_script">修补刷写脚本…</string>
|
||||||
|
<string name="horizon_flashing">刷写内核中…</string>
|
||||||
|
<string name="horizon_flash_complete_status">刷写完成</string>
|
||||||
|
<!-- 槽位选择相关字符串 -->
|
||||||
|
<string name="select_slot_title">选择刷写槽位</string>
|
||||||
|
<string name="select_slot_description">请选择要刷写HorizonKernel的目标槽位</string>
|
||||||
|
<string name="slot_a">A槽位</string>
|
||||||
|
<string name="slot_b">B槽位</string>
|
||||||
|
<string name="selected_slot">已选择槽位: %1$s</string>
|
||||||
|
<!-- 错误信息 -->
|
||||||
|
<string name="horizon_copy_failed">复制失败</string>
|
||||||
|
<string name="horizon_unknown_error">未知错误</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -270,4 +270,25 @@
|
|||||||
<string name="image_editor_hint">Use two fingers to zoom the image, and one finger to drag it to adjust the position</string>
|
<string name="image_editor_hint">Use two fingers to zoom the image, and one finger to drag it to adjust the position</string>
|
||||||
<string name="background_image_error">Could not load image</string>
|
<string name="background_image_error">Could not load image</string>
|
||||||
<string name="reprovision">Reprovision</string>
|
<string name="reprovision">Reprovision</string>
|
||||||
|
<!-- Anykernel3 Kernel Flash Progress Related -->
|
||||||
|
<string name="horizon_flash_title">Anykernel3 Flashing</string>
|
||||||
|
<string name="horizon_logs_label">Logs:</string>
|
||||||
|
<string name="horizon_flash_complete">Flash Complete</string>
|
||||||
|
<!-- Flash Status Related -->
|
||||||
|
<string name="horizon_preparing">Preparing…</string>
|
||||||
|
<string name="horizon_cleaning_files">Cleaning files…</string>
|
||||||
|
<string name="horizon_copying_files">Copying files…</string>
|
||||||
|
<string name="horizon_extracting_tool">Extracting flash tool…</string>
|
||||||
|
<string name="horizon_patching_script">Patching flash script…</string>
|
||||||
|
<string name="horizon_flashing">Flashing kernel…</string>
|
||||||
|
<string name="horizon_flash_complete_status">Flash completed</string>
|
||||||
|
<!-- Slot selection related strings -->
|
||||||
|
<string name="select_slot_title">Select Flash Slot</string>
|
||||||
|
<string name="select_slot_description">Please select the target slot for flashing HorizonKernel</string>
|
||||||
|
<string name="slot_a">Slot A</string>
|
||||||
|
<string name="slot_b">Slot B</string>
|
||||||
|
<string name="selected_slot">Selected slot: %1$s</string>
|
||||||
|
<!-- Error Messages -->
|
||||||
|
<string name="horizon_copy_failed">Copy failed</string>
|
||||||
|
<string name="horizon_unknown_error">Unknown error</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user