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