From 00ae4f9328e81c6fd85a4acd236c5aeb8434e058 Mon Sep 17 00:00:00 2001
From: ShirkNeko <2773800761@qq.com>
Date: Wed, 19 Mar 2025 20:08:23 +0800
Subject: [PATCH] The flasher interface implements anykernel3 functionality
flasher, adapted from the original project
github.com/libxzr/HorizonKernelFlasher, which implements the direct flasher
functionality
---
manager/app/build.gradle.kts | 1 +
manager/app/src/main/AndroidManifest.xml | 4 +
manager/app/src/main/assets/mkbootfs | Bin 0 -> 12112 bytes
.../zako/sukisu/ui/screen/Install.kt | 410 +++++++++++++-----
.../shirkneko/zako/sukisu/utils/AssetsUtil.kt | 26 ++
.../src/main/res/values-zh-rCN/strings.xml | 10 +
manager/app/src/main/res/values/strings.xml | 10 +
manager/gradle/libs.versions.toml | 4 +-
8 files changed, 363 insertions(+), 102 deletions(-)
create mode 100644 manager/app/src/main/assets/mkbootfs
create mode 100644 manager/app/src/main/java/shirkneko/zako/sukisu/utils/AssetsUtil.kt
diff --git a/manager/app/build.gradle.kts b/manager/app/build.gradle.kts
index 5961b20c..820d3c41 100644
--- a/manager/app/build.gradle.kts
+++ b/manager/app/build.gradle.kts
@@ -105,6 +105,7 @@ dependencies {
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.foundation)
+ implementation(libs.androidx.documentfile)
debugImplementation(libs.androidx.compose.ui.test.manifest)
debugImplementation(libs.androidx.compose.ui.tooling)
diff --git a/manager/app/src/main/AndroidManifest.xml b/manager/app/src/main/AndroidManifest.xml
index 11cda5f2..5bbdbd49 100644
--- a/manager/app/src/main/AndroidManifest.xml
+++ b/manager/app/src/main/AndroidManifest.xml
@@ -3,6 +3,10 @@
xmlns:tools="http://schemas.android.com/tools">
+
+
?jkt5I3Te1ao4z24gyw$O7>>ko9Lh0Q(^8B(W#0
zRk{V7sw6!Dl`c^^f=`a5+yrtX>+h!!YPy5L3X|*l6X&gBFJsWDVLKSwUKG*
z14%1>cGF5~2ok5Bc9qeRe9XO+w?XTMyW{c$FUq*UlX88D>Lg0tNO8t%-GC_N4?-nyx>MJc%B!$
z+zY+$
z#89$v5~j-~KwuNV^F+l5k%{)_Ag6kLUT$WVKDQv(!t{Dmp|LY&V#_)MzQzmlWig
zvX&VQESsFM>_T&Hfh8NX#FE9b%|;{3e#V?@F|s^3wy^w+Jm|B0V?KNmDK=UdSyJti
zEt%$Im|0P=$!uX1Ithu=c1i`Yj$(_!Xf`ttQ;7w{oRveIIV-=Ah@nIzXbo`|(=s>7
zV^8xKsi)$KO)Para*Ft&pTh_%vE=3zlYp3q9+FD=5;f#e0V!$DywMm()rar1)s+<$
zO#)L9bx6lE(?z)lrBI?uZ2q!gWTi4(z8)%M_z*s>pp`OQx^jrptuowSroTgm)85Zh
ztqc$2AZVQom(Q!iGF&UuKPJNm%J361JW_^#D8u{7@Fp3qmf>H>aK1iB?2zG7QM784
z;XP&bT$ka})j^bY%5Z+?69v-!iN;I1qKU9dh6l>(50T+gQItl=aJpmhq?O?ha1b;`
zh6l*-co|N+15b%ETz(2^IT5JWzEQ`sUYb@4UOxi-TDa%+~JSmdJu`E+bo$lVKm$wBpMDeXfw+!#$pb3H@RDol=4g3D-;8S
zpXoqpyYK9_4O6=SemZxeoHfBu@_<6knyR_SvP{
zAFh5d$!Tj-T~XyFW#9tX)vpYu9Qze*+tesg|*Z
zeQWid6bp+OV?E_^z09XA+Y*>sm$sS`q`U>{`TEer^_|g%@45uit7A<^8@YMNetL%A{X=$}
zVdv_>iZ-cO`VBwktV_`!v!T5WYX^7Qp?kx(A}#S`%%%;=<2JPkeeK_`)NSkM$E-2F
z%z6vk3<20CS=*oET$ulAv=^-7UgEdpqbe@Im#~qTXgfZEqC@#AYg00W%b=
zx-CrS)Clb1y-&p?>Ymqi7DBh72m6-%I+QG3hq^joQvf^+dQ|sq^m(x#>-{X^qrM_P
zPeNY5_Q}q@XiLBySGprQ$wqizh1+HUnfjY!<@3FDw&$rk7`Jgvl(L^*)-1DB1FXEj_84p
z1>Voj6gp=L>ZN(v8}uIAh_O~B<_qaekaSd_UFE-nPX&A{5rcqOd@yf)F>jgG6{b5w
zYx&WppPg+B*E#$6u(=u)D@xQdYaPZgNWs$E1s2QDZ#OWv#Ta$ttV-02FFs?B__Lzd
zLs&Y=McLK_>urq7k`Oj7%unZJ5fy3Iz~fHZr2ACibOh+EOvkKi0@tLWuVU4xN3`3O
zfO#BdC!W?#1=fwYW_~@|`uz{Cm$}_)*tI~f7xdcE*C+g0{TXvwGj)INXeVXJf$Glew6=@l(VrsD0R#RNv{sxY>ehGh-g6`q@
z(ABJFMHT1+)e!VcZ&ox6WgZL7#p|&DBXaFi4|h7?kLH9J_vZ1|lN^`YO!^mqT|#b9
z_S;nJ&l<^w@UMV1lYXze*q7jGoza{kd2J|UQeC_s&S7nS4B09nd|BJ)d;6mu^&1fT
z!&%I_q0=>v@XxU3@(?$*JqPo?C)%!9F|1}g>^$4P6K(GZ)-_ItF6nJp*|UbmMUy+M
zliCxJ!m!tc$70^8bNqGA(oAOE-iyt}{@)byk-|yy!G6t^Uebe&Jx=>V50>76F~OSb
zeeT8a)}gSY@m&?p64h9*ygf>`PMi;0;llwRoZn2m)MhuPw!drJB7wl_;$i4twkI5BaNcxl?YQ|D
z(Rtb|&^ao^b_mMY@6lg%IL91nIs&WMhP^em8D~igba7{_*04TTpqnn%x67^D23^DpJ2mve
zVq2kqLFf}Z9(6lV{#vBrvGK6SdyLup9_c$@50r16Hwpl-#tI@oe2J^FDR~X#1$6%Pt=2Jv3uXh!#xG^W58%^=q#l<
zpuik(rG#SaCu1B_F!mVt0G+d|`~z{^zd^mO_vnt|Yr8#$Id3iXgN_QiJu&9~wyqR~
z72n&NVro(vZwNlI?Sf$c9^>4BvDf%AGxaxv4?Y)TYJ|qKsFUtMx(l|#taLyeYWu4)J
z{cMBKScm-o#v9!{9zFvOZhWC@hy%Un0A^Uq;wMxx7
zjaE3zvnZPY*(tY7cK!e6oyx1bn89PXoV3#xDTBU&b#2
ze@ezb3BE9eH&utbzBWl;A@SUw;KAcO7t?h~fu5EqO~)5QN)llUque~c--?Oe$SCo*-*`!p
zza8^<_?z+Lk^%pXAXjQIz8i_{*GY1%SSvoHLZrlF-xm|^0AdQ!x+#CV<#OIHT1h_8
zt=9b;*LA7CPe_6PAn}2cf6Rw_#zQjiCiU_kExJ+fg+eL7|4?@y@qTbr;Uy@p&}^h1
z3-#iYGmDd#v%zrOVl1_=xTSayj>EIJF)qXX4-9d+24jI+5LcLog4vjt;Vxws7qd7+
zSpgobS)4IPpKZp|wm!!IZfQYD+|x#LajvOA4;DWd8qI~Gt(h#OTEvn})m>XzzS}-0
zqd4boy;4-9omrBbXBdJ1t)Qwh@^iCTTyYNl0+^A>;w**?O9tvUxheGZ24iN)Qa3Ap
zc8y=Ya1s7fOn-By8M={ewrpJ#N&~|9z`Z{MH*g
zQ?$p8hc-WV<<#!Hk^fkCVDOW}zAc)R9JjA2EdKWiuWs%PsJits8+2p9oNblM-nw!o
z?vcJJ@7X??Zd}=ja7ZrEz=0=AQrJXNNCcv~P$%uhVRs)wE{fg@)se+qd7@_jdh<
zXBK4~e&&;d0m~HY9~`VNv$AHxi0VJ(4zF2w@blo`rSHo+et3J`-e0PN|M2?N+3)!5
z+4j^qdtvEg7iL9&rM`NnUfsX{H@^)F)rPydAn=Z
zSm)IprG3{H)*OgY4^~}zVr>5hE^l7+$ntmJ$O@l1^o^}4%E-lEmp_`9`t{czRPC~)
zHtfzDXZW)JC%@ibaIwhtkHh<~MU=-po`3$&&ZnoI4qw^$;M$NOzZ|~C)$oF2$fRqj
z>rUxM1w|KW1sb2IksO6>S9KCm_Q*cN^A`~-*Zrihkzw{Cy)`WtaUXIN*%)U>R5
zjY8ZPkJY~&@zLts&mNo~zsT=+nrXA*XkFR16+0g(dFGGL{H3ue=uZt2|7=Ke
z{HeWPy&7ZqB;EJLgD2lPJM7Kn`BQ5evTUm7Ug&u0=DQpJ>Z&*No!H~R>9nIC`VWm<
zJbdkh(b*ck28XFX~udRv5AP^t903hrWGQasI%-EsoZL
zqU^enP2r0!x8JGSw`RDj;p)Lb{xQKTw5NNQywp@0+VFZs>-(jV8x`LSo%Oe{&(98;
z^88!xWqr3KZ}>^yOVRUBei@|DzY(;(Yt)>`>YKAZIglUz%A193>A?@yeLA_22|t^mzuwD<)1o?*pVO
zPh-H)(Z%gO|4)pc!`ueD?mg)vaB}+)=^W=&dylsCzj`w45XyAsbN~Dv!0A@0KDu5|
z?ED_WTZpY*2_IebkiQGTG=WxpJIb7d!{9ba~F5&k4p2sP_&v^Rh_0Vs8=)msy`Mr-*
z&v-e{=^U6--P}LF2Xe~qfuuusMQ+E(uLBsxG8&muqV(e-r!72Cw^-DrB8QhsW$xeG
z|5ZxnKN`Q|9`;M5f3o0oICmsfz~krj#Yp!29@~9UlrEHo{6Z@k@NwYw^F8eO{f5&y
zykU|7zo&A0yw!<)A@GW|l7v%<gmt3{UoO@
@Composable
fun InstallScreen(navigator: DestinationsNavigator) {
- var installMethod by remember {
- mutableStateOf(null)
+ var installMethod by remember { mutableStateOf(null) }
+ var lkmSelection by remember { mutableStateOf(LkmSelection.KmiNone) }
+ val context = LocalContext.current
+
+ var showRebootDialog by remember { mutableStateOf(false) }
+
+ val onFlashComplete = {
+ showRebootDialog = true
}
- var lkmSelection by remember {
- mutableStateOf(LkmSelection.KmiNone)
+ if (showRebootDialog) {
+ 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 = {
installMethod?.let { method ->
- 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))
+ when (method) {
+ is InstallMethod.HorizonKernel -> {
+ method.uri?.let { uri ->
+ val worker = HorizonKernelWorker(context)
+ worker.uri = uri
+ 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 ->
kmi?.let {
@@ -107,21 +124,22 @@ fun InstallScreen(navigator: DestinationsNavigator) {
val onClickNext = {
if (lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) {
- // no lkm file selected and cannot get current kmi
selectKmiDialog.show()
} else {
onInstall()
}
+ Unit
}
- val selectLkmLauncher =
- rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {
- if (it.resultCode == Activity.RESULT_OK) {
- it.data?.data?.let { uri ->
- lkmSelection = LkmSelection.LkmUri(uri)
- }
+ val selectLkmLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartActivityForResult()
+ ) {
+ if (it.resultCode == Activity.RESULT_OK) {
+ it.data?.data?.let { uri ->
+ lkmSelection = LkmSelection.LkmUri(uri)
}
}
+ }
val onLkmUpload = {
selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
@@ -134,12 +152,14 @@ fun InstallScreen(navigator: DestinationsNavigator) {
Scaffold(
topBar = {
TopBar(
- onBack = dropUnlessResumed { navigator.popBackStack() },
+ onBack = { navigator.popBackStack() },
onLkmUpload = onLkmUpload,
scrollBehavior = scrollBehavior
)
},
- contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
+ contentWindowInsets = WindowInsets.safeDrawing.only(
+ WindowInsetsSides.Top + WindowInsetsSides.Horizontal
+ )
) { innerPadding ->
Column(
modifier = Modifier
@@ -164,11 +184,11 @@ fun InstallScreen(navigator: DestinationsNavigator) {
)
)
}
- Button(modifier = Modifier.fillMaxWidth(),
+ Button(
+ modifier = Modifier.fillMaxWidth(),
enabled = installMethod != null,
- onClick = {
- onClickNext()
- }) {
+ onClick = onClickNext
+ ) {
Text(
stringResource(id = R.string.install_next),
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 {
data class SelectFile(
val uri: Uri? = null,
@@ -196,6 +376,12 @@ sealed class InstallMethod {
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
open val summary: String? = null
}
@@ -205,52 +391,66 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
val rootAvailable = rootAvailable()
val isAbDevice = isAbDevice()
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.SelectFile(summary = selectFileTip))
+
+ val radioOptions = mutableListOf(
+ InstallMethod.SelectFile(summary = selectFileTip)
+ )
+
if (rootAvailable) {
radioOptions.add(InstallMethod.DirectInstall)
-
if (isAbDevice) {
radioOptions.add(InstallMethod.DirectInstallToInactiveSlot)
}
+ radioOptions.add(InstallMethod.HorizonKernel(summary = "Flashing the Anykernel3 Kernel"))
}
var selectedOption by remember { mutableStateOf(null) }
+ var currentSelectingMethod by remember { mutableStateOf(null) }
+
val selectImageLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
it.data?.data?.let { uri ->
- val option = InstallMethod.SelectFile(uri, summary = selectFileTip)
- selectedOption = option
- onSelected(option)
+ val option = when (currentSelectingMethod) {
+ is InstallMethod.SelectFile -> InstallMethod.SelectFile(uri, summary = selectFileTip)
+ is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel(uri, summary = " Flashing the Anykernel3 Kernel")
+ else -> null
+ }
+ option?.let {
+ selectedOption = it
+ onSelected(it)
+ }
}
}
}
- val confirmDialog = rememberConfirmDialog(onConfirm = {
- selectedOption = InstallMethod.DirectInstallToInactiveSlot
- onSelected(InstallMethod.DirectInstallToInactiveSlot)
- }, onDismiss = null)
+ val confirmDialog = rememberConfirmDialog(
+ onConfirm = {
+ selectedOption = InstallMethod.DirectInstallToInactiveSlot
+ onSelected(InstallMethod.DirectInstallToInactiveSlot)
+ },
+ onDismiss = null
+ )
+
val dialogTitle = stringResource(id = android.R.string.dialog_alert_title)
val dialogContent = stringResource(id = R.string.install_inactive_slot_warning)
val onClick = { option: InstallMethod ->
-
+ currentSelectingMethod = option
when (option) {
- is InstallMethod.SelectFile -> {
+ is InstallMethod.SelectFile, is InstallMethod.HorizonKernel -> {
selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
- type = "application/octet-stream"
+ type = "application/*"
})
}
-
is InstallMethod.DirectInstall -> {
selectedOption = option
onSelected(option)
}
-
is InstallMethod.DirectInstallToInactiveSlot -> {
confirmDialog.showConfirm(dialogTitle, dialogContent)
}
@@ -266,9 +466,7 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
.fillMaxWidth()
.toggleable(
value = option.javaClass == selectedOption?.javaClass,
- onValueChange = {
- onClick(option)
- },
+ onValueChange = { onClick(option) },
role = Role.RadioButton,
indication = LocalIndication.current,
interactionSource = interactionSource
@@ -276,9 +474,7 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
) {
RadioButton(
selected = option.javaClass == selectedOption?.javaClass,
- onClick = {
- onClick(option)
- },
+ onClick = { onClick(option) },
interactionSource = interactionSource
)
Column(
@@ -311,25 +507,33 @@ fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
val supportedKmi by produceState(initialValue = emptyList()) {
value = getSupportedKmis()
}
+
val options = supportedKmi.map { value ->
- ListOption(
- titleText = value
- )
+ ListOption(titleText = value)
}
var selection by remember { mutableStateOf(null) }
- ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = {
- onSelected(selection)
- }, onCloseRequest = {
- dismiss()
- }), header = Header.Default(
- title = stringResource(R.string.select_kmi),
- ), selection = ListSelection.Single(
- showRadioButtons = true,
- options = options,
- ) { _, option ->
- selection = option.titleText
- })
+
+ ListDialog(
+ state = rememberUseCaseState(
+ visible = true,
+ onFinishedRequest = {
+ onSelected(selection)
+ },
+ onCloseRequest = {
+ dismiss()
+ }
+ ),
+ 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
) {
TopAppBar(
- title = { Text(stringResource(R.string.install)) }, navigationIcon = {
- IconButton(
- onClick = onBack
- ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
- }, actions = {
+ title = { Text(stringResource(R.string.install)) },
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
+ }
+ },
+ actions = {
IconButton(onClick = onLkmUpload) {
Icon(Icons.Filled.FileUpload, contentDescription = null)
}
},
- windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
+ windowInsets = WindowInsets.safeDrawing.only(
+ WindowInsetsSides.Top + WindowInsetsSides.Horizontal
+ ),
scrollBehavior = scrollBehavior
)
}
-@Composable
@Preview
+@Composable
fun SelectInstallPreview() {
InstallScreen(EmptyDestinationsNavigator)
}
\ No newline at end of file
diff --git a/manager/app/src/main/java/shirkneko/zako/sukisu/utils/AssetsUtil.kt b/manager/app/src/main/java/shirkneko/zako/sukisu/utils/AssetsUtil.kt
new file mode 100644
index 00000000..080b057d
--- /dev/null
+++ b/manager/app/src/main/java/shirkneko/zako/sukisu/utils/AssetsUtil.kt
@@ -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)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml
index 2d911da1..b5386b5f 100644
--- a/manager/app/src/main/res/values-zh-rCN/strings.xml
+++ b/manager/app/src/main/res/values-zh-rCN/strings.xml
@@ -204,4 +204,14 @@
粉色
高级灰
象牙白
+ 刷入选项
+ 选择要刷入的文件
+ Anykernel3 刷写
+ 需要 root 权限
+ 文件复制失败
+ 刷写完成
+ 是否立即重启?
+ 是
+ 否
+ 重启失败
\ No newline at end of file
diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml
index 48330ace..8dadf34d 100644
--- a/manager/app/src/main/res/values/strings.xml
+++ b/manager/app/src/main/res/values/strings.xml
@@ -206,4 +206,14 @@
pink
gray
ivory
+ Brush Options
+ Select the file to be flashed
+ Anykernel3 Flush
+ Requires root privileges
+ File Copy Failure
+ Scrubbing complete
+ Whether to reboot immediately?
+ yes
+ no
+ Reboot Failed
diff --git a/manager/gradle/libs.versions.toml b/manager/gradle/libs.versions.toml
index 34aedcea..b2416367 100644
--- a/manager/gradle/libs.versions.toml
+++ b/manager/gradle/libs.versions.toml
@@ -21,6 +21,7 @@ compose-material = "1.7.8"
compose-material3 = "1.3.1"
compose-ui = "1.7.8"
compose-foundation = "1.7.8"
+documentfile = "1.0.1"
[plugins]
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" }
-lsposed-cxx = { module = "org.lsposed.libcxx:libcxx", version = "27.0.12077973" }
\ No newline at end of file
+lsposed-cxx = { module = "org.lsposed.libcxx:libcxx", version = "27.0.12077973" }
+androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "documentfile" }
\ No newline at end of file