manager: support choose kmi manually. close #1496

This commit is contained in:
weishu
2024-03-23 12:30:27 +08:00
parent e124aab76a
commit 98030ee1ae
4 changed files with 121 additions and 45 deletions

View File

@@ -25,12 +25,14 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import me.weishu.kernelsu.R import me.weishu.kernelsu.R
import me.weishu.kernelsu.ui.component.KeyEventBlocker import me.weishu.kernelsu.ui.component.KeyEventBlocker
import me.weishu.kernelsu.ui.util.LkmSelection
import me.weishu.kernelsu.ui.util.LocalSnackbarHost import me.weishu.kernelsu.ui.util.LocalSnackbarHost
import me.weishu.kernelsu.ui.util.installBoot import me.weishu.kernelsu.ui.util.installBoot
import me.weishu.kernelsu.ui.util.installModule import me.weishu.kernelsu.ui.util.installModule
@@ -140,7 +142,8 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
@Parcelize @Parcelize
sealed class FlashIt : Parcelable { sealed class FlashIt : Parcelable {
data class FlashBoot(val bootUri: Uri? = null, val lkmUri: Uri? = null, val ota: Boolean) : FlashIt() data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean) :
FlashIt()
data class FlashModule(val uri: Uri) : FlashIt() data class FlashModule(val uri: Uri) : FlashIt()
} }
@@ -152,8 +155,8 @@ fun flashIt(
) { ) {
when (flashIt) { when (flashIt) {
is FlashIt.FlashBoot -> installBoot( is FlashIt.FlashBoot -> installBoot(
flashIt.bootUri, flashIt.boot,
flashIt.lkmUri, flashIt.lkm,
flashIt.ota, flashIt.ota,
onFinish, onFinish,
onStdout, onStdout,
@@ -188,5 +191,5 @@ private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) {
@Preview @Preview
@Composable @Composable
fun InstallPreview() { fun InstallPreview() {
// InstallScreen(DestinationsNavigator(), uri = Uri.EMPTY) InstallScreen(EmptyDestinationsNavigator)
} }

View File

@@ -33,11 +33,22 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
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 com.maxkeppeker.sheets.core.models.base.Header
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
import com.maxkeppeler.sheets.list.ListDialog
import com.maxkeppeler.sheets.list.models.ListOption
import com.maxkeppeler.sheets.list.models.ListSelection
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
import me.weishu.kernelsu.R import me.weishu.kernelsu.R
import me.weishu.kernelsu.ui.component.DialogHandle
import me.weishu.kernelsu.ui.component.rememberConfirmDialog import me.weishu.kernelsu.ui.component.rememberConfirmDialog
import me.weishu.kernelsu.ui.component.rememberCustomDialog
import me.weishu.kernelsu.ui.screen.destinations.FlashScreenDestination import me.weishu.kernelsu.ui.screen.destinations.FlashScreenDestination
import me.weishu.kernelsu.ui.util.LkmSelection
import me.weishu.kernelsu.ui.util.getCurrentKmi
import me.weishu.kernelsu.ui.util.getSupportedKmis
import me.weishu.kernelsu.ui.util.isAbDevice import me.weishu.kernelsu.ui.util.isAbDevice
import me.weishu.kernelsu.ui.util.isInitBoot import me.weishu.kernelsu.ui.util.isInitBoot
import me.weishu.kernelsu.ui.util.rootAvailable import me.weishu.kernelsu.ui.util.rootAvailable
@@ -53,24 +64,44 @@ fun InstallScreen(navigator: DestinationsNavigator) {
mutableStateOf<InstallMethod?>(null) mutableStateOf<InstallMethod?>(null)
} }
var lkmFileUri = null as Uri? var lkmSelection by remember {
mutableStateOf<LkmSelection>(LkmSelection.KmiNone)
}
val onClickInstall = { val onInstall = {
installMethod?.let { method -> installMethod?.let { method ->
val flashIt = FlashIt.FlashBoot( val flashIt = FlashIt.FlashBoot(
bootUri = if (method is InstallMethod.SelectFile) method.uri else null, boot = if (method is InstallMethod.SelectFile) method.uri else null,
lkmUri = lkmFileUri, lkm = lkmSelection,
ota = method is InstallMethod.DirectInstallToInactiveSlot ota = method is InstallMethod.DirectInstallToInactiveSlot
) )
navigator.navigate(FlashScreenDestination(flashIt)) navigator.navigate(FlashScreenDestination(flashIt))
} }
} }
val currentKmi = remember { getCurrentKmi() }
val selectKmiDialog = rememberSelectKmiDialog { kmi ->
kmi?.let {
lkmSelection = LkmSelection.KmiString(it)
onInstall()
}
}
val onClickNext = {
if (lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) {
// no lkm file selected and cannot get current kmi
selectKmiDialog.show()
} else {
onInstall()
}
}
val selectLkmLauncher = val selectLkmLauncher =
rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) { rememberLauncherForActivityResult(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 ->
lkmFileUri = uri lkmSelection = LkmSelection.LkmUri(uri)
} }
} }
} }
@@ -103,7 +134,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
enabled = installMethod != null, enabled = installMethod != null,
onClick = { onClick = {
onClickInstall() onClickNext()
}) { }) {
Text( Text(
stringResource(id = R.string.install_next), stringResource(id = R.string.install_next),
@@ -122,12 +153,12 @@ sealed class InstallMethod {
override val summary: String? override val summary: String?
) : InstallMethod() ) : InstallMethod()
object DirectInstall : InstallMethod() { data object DirectInstall : InstallMethod() {
override val label: Int override val label: Int
get() = R.string.direct_install get() = R.string.direct_install
} }
object DirectInstallToInactiveSlot : InstallMethod() { data object DirectInstallToInactiveSlot : InstallMethod() {
override val label: Int override val label: Int
get() = R.string.install_inactive_slot get() = R.string.install_inactive_slot
} }
@@ -228,6 +259,39 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
return rememberCustomDialog { dismiss ->
val kmis = remember {
getSupportedKmis()
}
val options = kmis.map { value ->
ListOption(
titleText = value
)
}
var selection: String? = 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
}
)
}
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun TopBar(onBack: () -> Unit = {}, onLkmUpload: () -> Unit = {}) { private fun TopBar(onBack: () -> Unit = {}, onLkmUpload: () -> Unit = {}) {
@@ -249,5 +313,5 @@ private fun TopBar(onBack: () -> Unit = {}, onLkmUpload: () -> Unit = {}) {
@Composable @Composable
@Preview @Preview
fun SelectInstall_Preview() { fun SelectInstall_Preview() {
// InstallScreen(DestinationsNavigator()) InstallScreen(EmptyDestinationsNavigator)
} }

View File

@@ -3,12 +3,14 @@ package me.weishu.kernelsu.ui.util
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.os.Parcelable
import android.os.SystemClock import android.os.SystemClock
import android.util.Log import android.util.Log
import com.topjohnwu.superuser.CallbackList import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.io.SuFile import com.topjohnwu.superuser.io.SuFile
import kotlinx.parcelize.Parcelize
import me.weishu.kernelsu.BuildConfig import me.weishu.kernelsu.BuildConfig
import me.weishu.kernelsu.Natives import me.weishu.kernelsu.Natives
import me.weishu.kernelsu.ksuApp import me.weishu.kernelsu.ksuApp
@@ -102,10 +104,7 @@ fun uninstallModule(id: String): Boolean {
} }
fun installModule( fun installModule(
uri: Uri, uri: Uri, onFinish: (Boolean) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit
onFinish: (Boolean) -> Unit,
onStdout: (String) -> Unit,
onStderr: (String) -> Unit
): Boolean { ): Boolean {
val resolver = ksuApp.contentResolver val resolver = ksuApp.contentResolver
with(resolver.openInputStream(uri)) { with(resolver.openInputStream(uri)) {
@@ -141,9 +140,16 @@ fun installModule(
} }
} }
@Parcelize
sealed class LkmSelection: Parcelable {
data class LkmUri(val uri: Uri) : LkmSelection()
data class KmiString(val value: String) : LkmSelection()
data object KmiNone : LkmSelection()
}
fun installBoot( fun installBoot(
bootUri: Uri?, bootUri: Uri?,
lkmUri: Uri?, lkm: LkmSelection,
ota: Boolean, ota: Boolean,
onFinish: (Boolean) -> Unit, onFinish: (Boolean) -> Unit,
onStdout: (String) -> Unit, onStdout: (String) -> Unit,
@@ -162,17 +168,6 @@ fun installBoot(
} }
} }
val lkmFile = lkmUri?.let { uri ->
with(resolver.openInputStream(uri)) {
val lkmFile = File(ksuApp.cacheDir, "kernelsu-tmp-lkm.ko")
lkmFile.outputStream().use { output ->
this?.copyTo(output)
}
lkmFile
}
}
val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so") val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so")
var cmd = "boot-patch --magiskboot ${magiskboot.absolutePath}" var cmd = "boot-patch --magiskboot ${magiskboot.absolutePath}"
@@ -187,8 +182,27 @@ fun installBoot(
cmd += " -u" cmd += " -u"
} }
lkmFile?.let { var lkmFile: File? = null
cmd += " -m ${it.absolutePath}" when (lkm) {
is LkmSelection.LkmUri -> {
lkmFile = with(resolver.openInputStream(lkm.uri)) {
val file = File(ksuApp.cacheDir, "kernelsu-tmp-lkm.ko")
file.outputStream().use { output ->
this?.copyTo(output)
}
file
}
cmd += " -m ${lkmFile.absolutePath}"
}
is LkmSelection.KmiString -> {
cmd += " --kmi ${lkm.value}"
}
LkmSelection.KmiNone -> {
// do nothing
}
} }
// output dir // output dir
@@ -211,8 +225,7 @@ fun installBoot(
} }
val result = val result =
shell.newJob().add("${getKsuDaemonPath()} $cmd").to(stdoutCallback, stderrCallback) shell.newJob().add("${getKsuDaemonPath()} $cmd").to(stdoutCallback, stderrCallback).exec()
.exec()
Log.i("KernelSU", "install boot result: ${result.isSuccess}") Log.i("KernelSU", "install boot result: ${result.isSuccess}")
bootFile?.delete() bootFile?.delete()
@@ -259,15 +272,14 @@ fun isInitBoot(): Boolean {
fun getCurrentKmi(): String { fun getCurrentKmi(): String {
val shell = getRootShell() val shell = getRootShell()
val cmd = "boot-info supported-kmi" val cmd = "boot-info current-kmi"
return ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd") return ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd")
} }
fun getSupportedKmis(): List<String> { fun getSupportedKmis(): List<String> {
val shell = getRootShell() val shell = getRootShell()
val cmd = "boot-info supported-kmi" val cmd = "boot-info supported-kmi"
val out = val out = shell.newJob().add("${getKsuDaemonPath()} $cmd").to(ArrayList(), null).exec().out
shell.newJob().add("${getKsuDaemonPath()} $cmd").to(ArrayList(), null).exec().out
return out.filter { it.isNotBlank() }.map { it.trim() } return out.filter { it.isNotBlank() }.map { it.trim() }
} }
@@ -306,9 +318,8 @@ fun getSepolicy(pkg: String): String {
fun setSepolicy(pkg: String, rules: String): Boolean { fun setSepolicy(pkg: String, rules: String): Boolean {
val shell = getRootShell() val shell = getRootShell()
val result = val result = shell.newJob().add("${getKsuDaemonPath()} profile set-sepolicy $pkg '$rules'")
shell.newJob().add("${getKsuDaemonPath()} profile set-sepolicy $pkg '$rules'") .to(ArrayList(), null).exec()
.to(ArrayList(), null).exec()
Log.i(TAG, "set sepolicy result: ${result.code}") Log.i(TAG, "set sepolicy result: ${result.code}")
return result.isSuccess return result.isSuccess
} }
@@ -322,22 +333,19 @@ fun listAppProfileTemplates(): List<String> {
fun getAppProfileTemplate(id: String): String { fun getAppProfileTemplate(id: String): String {
val shell = getRootShell() val shell = getRootShell()
return shell.newJob().add("${getKsuDaemonPath()} profile get-template '${id}'") return shell.newJob().add("${getKsuDaemonPath()} profile get-template '${id}'")
.to(ArrayList(), null) .to(ArrayList(), null).exec().out.joinToString("\n")
.exec().out.joinToString("\n")
} }
fun setAppProfileTemplate(id: String, template: String): Boolean { fun setAppProfileTemplate(id: String, template: String): Boolean {
val shell = getRootShell() val shell = getRootShell()
return shell.newJob().add("${getKsuDaemonPath()} profile set-template '${id}' '${template}'") return shell.newJob().add("${getKsuDaemonPath()} profile set-template '${id}' '${template}'")
.to(ArrayList(), null) .to(ArrayList(), null).exec().isSuccess
.exec().isSuccess
} }
fun deleteAppProfileTemplate(id: String): Boolean { fun deleteAppProfileTemplate(id: String): Boolean {
val shell = getRootShell() val shell = getRootShell()
return shell.newJob().add("${getKsuDaemonPath()} profile delete-template '${id}'") return shell.newJob().add("${getKsuDaemonPath()} profile delete-template '${id}'")
.to(ArrayList(), null) .to(ArrayList(), null).exec().isSuccess
.exec().isSuccess
} }
fun forceStopApp(packageName: String) { fun forceStopApp(packageName: String) {

View File

@@ -116,4 +116,5 @@
<string name="install_inactive_slot_warning">Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue?</string> <string name="install_inactive_slot_warning">Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue?</string>
<string name="install_next">Next</string> <string name="install_next">Next</string>
<string name="select_file_tip">%1$s partition image is recommended</string> <string name="select_file_tip">%1$s partition image is recommended</string>
<string name="select_kmi">Select KMI</string>
</resources> </resources>