The flasher interface implements anykernel3 functionality flasher, adapted from the original project github.com/libxzr/HorizonKernelFlasher, which implements the direct flasher functionality

This commit is contained in:
ShirkNeko
2025-03-19 20:08:23 +08:00
parent 21bb254100
commit 00ae4f9328
8 changed files with 363 additions and 102 deletions

View File

@@ -105,6 +105,7 @@ dependencies {
implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.foundation) implementation(libs.androidx.foundation)
implementation(libs.androidx.documentfile)
debugImplementation(libs.androidx.compose.ui.test.manifest) debugImplementation(libs.androidx.compose.ui.test.manifest)
debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.tooling)

View File

@@ -3,6 +3,10 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<application <application
android:name=".KernelSUApplication" android:name=".KernelSUApplication"

Binary file not shown.

View File

@@ -1,52 +1,34 @@
package shirkneko.zako.sukisu.ui.screen package shirkneko.zako.sukisu.ui.screen
import android.app.Activity import android.app.Activity
import android.app.AlertDialog
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 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.foundation.LocalIndication import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.selection.toggleable
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.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.FileUpload import androidx.compose.material.icons.filled.FileUpload
import androidx.compose.material3.Button import androidx.compose.material3.*
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.*
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource 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.maxkeppeker.sheets.core.models.base.Header import com.maxkeppeker.sheets.core.models.base.Header
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
import com.maxkeppeler.sheets.list.ListDialog import com.maxkeppeler.sheets.list.ListDialog
@@ -61,13 +43,12 @@ import shirkneko.zako.sukisu.R
import shirkneko.zako.sukisu.ui.component.DialogHandle import shirkneko.zako.sukisu.ui.component.DialogHandle
import shirkneko.zako.sukisu.ui.component.rememberConfirmDialog import shirkneko.zako.sukisu.ui.component.rememberConfirmDialog
import shirkneko.zako.sukisu.ui.component.rememberCustomDialog import shirkneko.zako.sukisu.ui.component.rememberCustomDialog
import shirkneko.zako.sukisu.ui.util.LkmSelection import shirkneko.zako.sukisu.ui.util.*
import shirkneko.zako.sukisu.ui.util.getCurrentKmi import shirkneko.zako.sukisu.utils.AssetsUtil
import shirkneko.zako.sukisu.ui.util.getSupportedKmis import java.io.File
import shirkneko.zako.sukisu.ui.util.isAbDevice import java.io.FileOutputStream
import shirkneko.zako.sukisu.ui.util.isInitBoot import java.io.IOException
import shirkneko.zako.sukisu.ui.util.rootAvailable
import androidx.lifecycle.compose.dropUnlessResumed
/** /**
* @author weishu * @author weishu
@@ -77,26 +58,62 @@ import androidx.lifecycle.compose.dropUnlessResumed
@Destination<RootGraph> @Destination<RootGraph>
@Composable @Composable
fun InstallScreen(navigator: DestinationsNavigator) { fun InstallScreen(navigator: DestinationsNavigator) {
var installMethod by remember { var installMethod by remember { mutableStateOf<InstallMethod?>(null) }
mutableStateOf<InstallMethod?>(null) var lkmSelection by remember { mutableStateOf<LkmSelection>(LkmSelection.KmiNone) }
val context = LocalContext.current
var showRebootDialog by remember { mutableStateOf(false) }
val onFlashComplete = {
showRebootDialog = true
} }
var lkmSelection by remember { if (showRebootDialog) {
mutableStateOf<LkmSelection>(LkmSelection.KmiNone) RebootDialog(
show = true,
onDismiss = { showRebootDialog = false },
onConfirm = {
showRebootDialog = false
try {
val process = Runtime.getRuntime().exec("su")
process.outputStream.bufferedWriter().use { writer ->
writer.write("svc power reboot\n")
writer.write("exit\n")
}
} catch (e: Exception) {
Toast.makeText(context, R.string.failed_reboot, Toast.LENGTH_SHORT).show()
}
}
)
} }
val onInstall = { val onInstall = {
installMethod?.let { method -> installMethod?.let { method ->
val flashIt = FlashIt.FlashBoot( when (method) {
boot = if (method is InstallMethod.SelectFile) method.uri else null, is InstallMethod.HorizonKernel -> {
lkm = lkmSelection, method.uri?.let { uri ->
ota = method is InstallMethod.DirectInstallToInactiveSlot val worker = HorizonKernelWorker(context)
) worker.uri = uri
navigator.navigate(FlashScreenDestination(flashIt)) worker.setOnFlashCompleteListener(onFlashComplete)
worker.start()
}
}
else -> {
val flashIt = FlashIt.FlashBoot(
boot = if (method is InstallMethod.SelectFile) method.uri else null,
lkm = lkmSelection,
ota = method is InstallMethod.DirectInstallToInactiveSlot
)
navigator.navigate(FlashScreenDestination(flashIt))
}
}
} }
Unit
} }
val currentKmi by produceState(initialValue = "") { value = getCurrentKmi() } val currentKmi by produceState(initialValue = "") {
value = getCurrentKmi()
}
val selectKmiDialog = rememberSelectKmiDialog { kmi -> val selectKmiDialog = rememberSelectKmiDialog { kmi ->
kmi?.let { kmi?.let {
@@ -107,21 +124,22 @@ fun InstallScreen(navigator: DestinationsNavigator) {
val onClickNext = { val onClickNext = {
if (lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) { if (lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) {
// no lkm file selected and cannot get current kmi
selectKmiDialog.show() selectKmiDialog.show()
} else { } else {
onInstall() onInstall()
} }
Unit
} }
val selectLkmLauncher = val selectLkmLauncher = rememberLauncherForActivityResult(
rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) { contract = ActivityResultContracts.StartActivityForResult()
if (it.resultCode == Activity.RESULT_OK) { ) {
it.data?.data?.let { uri -> if (it.resultCode == Activity.RESULT_OK) {
lkmSelection = LkmSelection.LkmUri(uri) it.data?.data?.let { uri ->
} lkmSelection = LkmSelection.LkmUri(uri)
} }
} }
}
val onLkmUpload = { val onLkmUpload = {
selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply { selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
@@ -134,12 +152,14 @@ fun InstallScreen(navigator: DestinationsNavigator) {
Scaffold( Scaffold(
topBar = { topBar = {
TopBar( TopBar(
onBack = dropUnlessResumed { navigator.popBackStack() }, onBack = { navigator.popBackStack() },
onLkmUpload = onLkmUpload, onLkmUpload = onLkmUpload,
scrollBehavior = scrollBehavior scrollBehavior = scrollBehavior
) )
}, },
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) contentWindowInsets = WindowInsets.safeDrawing.only(
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
)
) { innerPadding -> ) { innerPadding ->
Column( Column(
modifier = Modifier modifier = Modifier
@@ -164,11 +184,11 @@ fun InstallScreen(navigator: DestinationsNavigator) {
) )
) )
} }
Button(modifier = Modifier.fillMaxWidth(), Button(
modifier = Modifier.fillMaxWidth(),
enabled = installMethod != null, enabled = installMethod != null,
onClick = { onClick = onClickNext
onClickNext() ) {
}) {
Text( Text(
stringResource(id = R.string.install_next), stringResource(id = R.string.install_next),
fontSize = MaterialTheme.typography.bodyMedium.fontSize fontSize = MaterialTheme.typography.bodyMedium.fontSize
@@ -179,6 +199,166 @@ fun InstallScreen(navigator: DestinationsNavigator) {
} }
} }
private fun launchHorizonKernelFlash(context: Context, uri: Uri) {
val worker = HorizonKernelWorker(context)
worker.uri = uri
worker.setOnFlashCompleteListener {
}
worker.start()
}
@Composable
private fun RebootDialog(
show: Boolean,
onDismiss: () -> Unit,
onConfirm: () -> Unit
) {
if (show) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(id = R.string.reboot_complete_title)) },
text = { Text(stringResource(id = R.string.reboot_complete_msg)) },
confirmButton = {
TextButton(onClick = onConfirm) {
Text(stringResource(id = R.string.yes))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(id = R.string.no))
}
}
)
}
}
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, "rm -rf ${context.filesDir.absolutePath}/*")
}
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,
@@ -196,6 +376,12 @@ sealed class InstallMethod {
get() = R.string.install_inactive_slot get() = R.string.install_inactive_slot
} }
data class HorizonKernel(
val uri: Uri? = null,
@StringRes override val label: Int = R.string.horizon_kernel,
override val summary: String? = null
) : InstallMethod()
abstract val label: Int abstract val label: Int
open val summary: String? = null open val summary: String? = null
} }
@@ -205,52 +391,66 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
val rootAvailable = rootAvailable() val rootAvailable = rootAvailable()
val isAbDevice = isAbDevice() val isAbDevice = isAbDevice()
val selectFileTip = stringResource( val selectFileTip = stringResource(
id = R.string.select_file_tip, if (isInitBoot()) "init_boot" else "boot" id = R.string.select_file_tip,
if (isInitBoot()) "init_boot" else "boot"
) )
val radioOptions =
mutableListOf<InstallMethod>(InstallMethod.SelectFile(summary = selectFileTip)) val radioOptions = mutableListOf<InstallMethod>(
InstallMethod.SelectFile(summary = selectFileTip)
)
if (rootAvailable) { if (rootAvailable) {
radioOptions.add(InstallMethod.DirectInstall) radioOptions.add(InstallMethod.DirectInstall)
if (isAbDevice) { if (isAbDevice) {
radioOptions.add(InstallMethod.DirectInstallToInactiveSlot) radioOptions.add(InstallMethod.DirectInstallToInactiveSlot)
} }
radioOptions.add(InstallMethod.HorizonKernel(summary = "Flashing the Anykernel3 Kernel"))
} }
var selectedOption by remember { mutableStateOf<InstallMethod?>(null) } var selectedOption by remember { mutableStateOf<InstallMethod?>(null) }
var currentSelectingMethod by remember { mutableStateOf<InstallMethod?>(null) }
val selectImageLauncher = rememberLauncherForActivityResult( val selectImageLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult() contract = ActivityResultContracts.StartActivityForResult()
) { ) {
if (it.resultCode == Activity.RESULT_OK) { if (it.resultCode == Activity.RESULT_OK) {
it.data?.data?.let { uri -> it.data?.data?.let { uri ->
val option = InstallMethod.SelectFile(uri, summary = selectFileTip) val option = when (currentSelectingMethod) {
selectedOption = option is InstallMethod.SelectFile -> InstallMethod.SelectFile(uri, summary = selectFileTip)
onSelected(option) is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel(uri, summary = " Flashing the Anykernel3 Kernel")
else -> null
}
option?.let {
selectedOption = it
onSelected(it)
}
} }
} }
} }
val confirmDialog = rememberConfirmDialog(onConfirm = { val confirmDialog = rememberConfirmDialog(
selectedOption = InstallMethod.DirectInstallToInactiveSlot onConfirm = {
onSelected(InstallMethod.DirectInstallToInactiveSlot) selectedOption = InstallMethod.DirectInstallToInactiveSlot
}, onDismiss = null) onSelected(InstallMethod.DirectInstallToInactiveSlot)
},
onDismiss = null
)
val dialogTitle = stringResource(id = android.R.string.dialog_alert_title) val dialogTitle = stringResource(id = android.R.string.dialog_alert_title)
val dialogContent = stringResource(id = R.string.install_inactive_slot_warning) val dialogContent = stringResource(id = R.string.install_inactive_slot_warning)
val onClick = { option: InstallMethod -> val onClick = { option: InstallMethod ->
currentSelectingMethod = option
when (option) { when (option) {
is InstallMethod.SelectFile -> { is InstallMethod.SelectFile, is InstallMethod.HorizonKernel -> {
selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply { selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
type = "application/octet-stream" type = "application/*"
}) })
} }
is InstallMethod.DirectInstall -> { is InstallMethod.DirectInstall -> {
selectedOption = option selectedOption = option
onSelected(option) onSelected(option)
} }
is InstallMethod.DirectInstallToInactiveSlot -> { is InstallMethod.DirectInstallToInactiveSlot -> {
confirmDialog.showConfirm(dialogTitle, dialogContent) confirmDialog.showConfirm(dialogTitle, dialogContent)
} }
@@ -266,9 +466,7 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
.fillMaxWidth() .fillMaxWidth()
.toggleable( .toggleable(
value = option.javaClass == selectedOption?.javaClass, value = option.javaClass == selectedOption?.javaClass,
onValueChange = { onValueChange = { onClick(option) },
onClick(option)
},
role = Role.RadioButton, role = Role.RadioButton,
indication = LocalIndication.current, indication = LocalIndication.current,
interactionSource = interactionSource interactionSource = interactionSource
@@ -276,9 +474,7 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
) { ) {
RadioButton( RadioButton(
selected = option.javaClass == selectedOption?.javaClass, selected = option.javaClass == selectedOption?.javaClass,
onClick = { onClick = { onClick(option) },
onClick(option)
},
interactionSource = interactionSource interactionSource = interactionSource
) )
Column( Column(
@@ -311,25 +507,33 @@ fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
val supportedKmi by produceState(initialValue = emptyList<String>()) { val supportedKmi by produceState(initialValue = emptyList<String>()) {
value = getSupportedKmis() value = getSupportedKmis()
} }
val options = supportedKmi.map { value -> val options = supportedKmi.map { value ->
ListOption( ListOption(titleText = value)
titleText = value
)
} }
var selection by remember { mutableStateOf<String?>(null) } var selection by remember { mutableStateOf<String?>(null) }
ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = {
onSelected(selection) ListDialog(
}, onCloseRequest = { state = rememberUseCaseState(
dismiss() visible = true,
}), header = Header.Default( onFinishedRequest = {
title = stringResource(R.string.select_kmi), onSelected(selection)
), selection = ListSelection.Single( },
showRadioButtons = true, onCloseRequest = {
options = options, dismiss()
) { _, option -> }
selection = option.titleText ),
}) header = Header.Default(
title = stringResource(R.string.select_kmi),
),
selection = ListSelection.Single(
showRadioButtons = true,
options = options,
) { _, option ->
selection = option.titleText
}
)
} }
} }
@@ -341,22 +545,26 @@ private fun TopBar(
scrollBehavior: TopAppBarScrollBehavior? = null scrollBehavior: TopAppBarScrollBehavior? = null
) { ) {
TopAppBar( TopAppBar(
title = { Text(stringResource(R.string.install)) }, navigationIcon = { title = { Text(stringResource(R.string.install)) },
IconButton( navigationIcon = {
onClick = onBack IconButton(onClick = onBack) {
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
}, actions = { }
},
actions = {
IconButton(onClick = onLkmUpload) { IconButton(onClick = onLkmUpload) {
Icon(Icons.Filled.FileUpload, contentDescription = null) Icon(Icons.Filled.FileUpload, contentDescription = null)
} }
}, },
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), windowInsets = WindowInsets.safeDrawing.only(
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
),
scrollBehavior = scrollBehavior scrollBehavior = scrollBehavior
) )
} }
@Composable
@Preview @Preview
@Composable
fun SelectInstallPreview() { fun SelectInstallPreview() {
InstallScreen(EmptyDestinationsNavigator) InstallScreen(EmptyDestinationsNavigator)
} }

View File

@@ -0,0 +1,26 @@
package shirkneko.zako.sukisu.utils
import android.content.Context
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
object AssetsUtil {
@Throws(IOException::class)
fun exportFiles(context: Context, src: String, out: String) {
val fileNames = context.assets.list(src)
if (fileNames?.isNotEmpty() == true) {
val file = File(out)
file.mkdirs()
fileNames.forEach { fileName ->
exportFiles(context, "$src/$fileName", "$out/$fileName")
}
} else {
context.assets.open(src).use { inputStream ->
FileOutputStream(File(out)).use { outputStream ->
inputStream.copyTo(outputStream)
}
}
}
}
}

View File

@@ -204,4 +204,14 @@
<string name="color_pink">粉色</string> <string name="color_pink">粉色</string>
<string name="color_gray">高级灰</string> <string name="color_gray">高级灰</string>
<string name="color_ivory">象牙白</string> <string name="color_ivory">象牙白</string>
<string name="flash_option">刷入选项</string>
<string name="flash_option_tip">选择要刷入的文件</string>
<string name="horizon_kernel">Anykernel3 刷写</string>
<string name="root_required">需要 root 权限</string>
<string name="copy_failed">文件复制失败</string>
<string name="reboot_complete_title">刷写完成</string>
<string name="reboot_complete_msg">是否立即重启?</string>
<string name="yes"></string>
<string name="no"></string>
<string name="failed_reboot">重启失败</string>
</resources> </resources>

View File

@@ -206,4 +206,14 @@
<string name="color_pink">pink</string> <string name="color_pink">pink</string>
<string name="color_gray">gray</string> <string name="color_gray">gray</string>
<string name="color_ivory">ivory</string> <string name="color_ivory">ivory</string>
<string name="flash_option">Brush Options</string>
<string name="flash_option_tip">Select the file to be flashed</string>
<string name="horizon_kernel">Anykernel3 Flush</string>
<string name="root_required">Requires root privileges</string>
<string name="copy_failed">File Copy Failure</string>
<string name="reboot_complete_title">Scrubbing complete</string>
<string name="reboot_complete_msg">Whether to reboot immediately</string>
<string name="yes">yes</string>
<string name="no">no</string>
<string name="failed_reboot">Reboot Failed</string>
</resources> </resources>

View File

@@ -21,6 +21,7 @@ compose-material = "1.7.8"
compose-material3 = "1.3.1" compose-material3 = "1.3.1"
compose-ui = "1.7.8" compose-ui = "1.7.8"
compose-foundation = "1.7.8" compose-foundation = "1.7.8"
documentfile = "1.0.1"
[plugins] [plugins]
agp-app = { id = "com.android.application", version.ref = "agp" } agp-app = { id = "com.android.application", version.ref = "agp" }
@@ -79,4 +80,5 @@ sheet-compose-dialogs-input = { group = "com.maxkeppeler.sheets-compose-dialogs"
markdown = { group = "io.noties.markwon", name = "core", version.ref = "markdown" } markdown = { group = "io.noties.markwon", name = "core", version.ref = "markdown" }
lsposed-cxx = { module = "org.lsposed.libcxx:libcxx", version = "27.0.12077973" } lsposed-cxx = { module = "org.lsposed.libcxx:libcxx", version = "27.0.12077973" }
androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "documentfile" }