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:
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
BIN
manager/app/src/main/assets/mkbootfs
Normal file
BIN
manager/app/src/main/assets/mkbootfs
Normal file
Binary file not shown.
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" }
|
||||||
Reference in New Issue
Block a user