diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Flash.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Flash.kt index 099d31af..27d49e6f 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Flash.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Flash.kt @@ -25,12 +25,14 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize import me.weishu.kernelsu.R 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.installBoot import me.weishu.kernelsu.ui.util.installModule @@ -140,7 +142,8 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { @Parcelize 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() } @@ -152,8 +155,8 @@ fun flashIt( ) { when (flashIt) { is FlashIt.FlashBoot -> installBoot( - flashIt.bootUri, - flashIt.lkmUri, + flashIt.boot, + flashIt.lkm, flashIt.ota, onFinish, onStdout, @@ -188,5 +191,5 @@ private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) { @Preview @Composable fun InstallPreview() { -// InstallScreen(DestinationsNavigator(), uri = Uri.EMPTY) + InstallScreen(EmptyDestinationsNavigator) } \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Install.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Install.kt index dcd027ad..7d07ed26 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Install.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Install.kt @@ -33,11 +33,22 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview 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.navigation.DestinationsNavigator +import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator 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.rememberCustomDialog 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.isInitBoot import me.weishu.kernelsu.ui.util.rootAvailable @@ -53,24 +64,44 @@ fun InstallScreen(navigator: DestinationsNavigator) { mutableStateOf(null) } - var lkmFileUri = null as Uri? + var lkmSelection by remember { + mutableStateOf(LkmSelection.KmiNone) + } - val onClickInstall = { + val onInstall = { installMethod?.let { method -> val flashIt = FlashIt.FlashBoot( - bootUri = if (method is InstallMethod.SelectFile) method.uri else null, - lkmUri = lkmFileUri, + boot = if (method is InstallMethod.SelectFile) method.uri else null, + lkm = lkmSelection, ota = method is InstallMethod.DirectInstallToInactiveSlot ) 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 = rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == Activity.RESULT_OK) { it.data?.data?.let { uri -> - lkmFileUri = uri + lkmSelection = LkmSelection.LkmUri(uri) } } } @@ -103,7 +134,7 @@ fun InstallScreen(navigator: DestinationsNavigator) { modifier = Modifier.fillMaxWidth(), enabled = installMethod != null, onClick = { - onClickInstall() + onClickNext() }) { Text( stringResource(id = R.string.install_next), @@ -122,12 +153,12 @@ sealed class InstallMethod { override val summary: String? ) : InstallMethod() - object DirectInstall : InstallMethod() { + data object DirectInstall : InstallMethod() { override val label: Int get() = R.string.direct_install } - object DirectInstallToInactiveSlot : InstallMethod() { + data object DirectInstallToInactiveSlot : InstallMethod() { override val label: Int 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) @Composable private fun TopBar(onBack: () -> Unit = {}, onLkmUpload: () -> Unit = {}) { @@ -249,5 +313,5 @@ private fun TopBar(onBack: () -> Unit = {}, onLkmUpload: () -> Unit = {}) { @Composable @Preview fun SelectInstall_Preview() { -// InstallScreen(DestinationsNavigator()) + InstallScreen(EmptyDestinationsNavigator) } \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt index cee3e3dc..f61ead60 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt @@ -3,12 +3,14 @@ package me.weishu.kernelsu.ui.util import android.net.Uri import android.os.Build import android.os.Environment +import android.os.Parcelable import android.os.SystemClock import android.util.Log import com.topjohnwu.superuser.CallbackList import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.ShellUtils import com.topjohnwu.superuser.io.SuFile +import kotlinx.parcelize.Parcelize import me.weishu.kernelsu.BuildConfig import me.weishu.kernelsu.Natives import me.weishu.kernelsu.ksuApp @@ -102,10 +104,7 @@ fun uninstallModule(id: String): Boolean { } fun installModule( - uri: Uri, - onFinish: (Boolean) -> Unit, - onStdout: (String) -> Unit, - onStderr: (String) -> Unit + uri: Uri, onFinish: (Boolean) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit ): Boolean { val resolver = ksuApp.contentResolver 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( bootUri: Uri?, - lkmUri: Uri?, + lkm: LkmSelection, ota: Boolean, onFinish: (Boolean) -> 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") var cmd = "boot-patch --magiskboot ${magiskboot.absolutePath}" @@ -187,8 +182,27 @@ fun installBoot( cmd += " -u" } - lkmFile?.let { - cmd += " -m ${it.absolutePath}" + var lkmFile: File? = null + 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 @@ -211,8 +225,7 @@ fun installBoot( } val result = - shell.newJob().add("${getKsuDaemonPath()} $cmd").to(stdoutCallback, stderrCallback) - .exec() + shell.newJob().add("${getKsuDaemonPath()} $cmd").to(stdoutCallback, stderrCallback).exec() Log.i("KernelSU", "install boot result: ${result.isSuccess}") bootFile?.delete() @@ -259,15 +272,14 @@ fun isInitBoot(): Boolean { fun getCurrentKmi(): String { val shell = getRootShell() - val cmd = "boot-info supported-kmi" + val cmd = "boot-info current-kmi" return ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd") } fun getSupportedKmis(): List { val shell = getRootShell() val cmd = "boot-info supported-kmi" - val out = - shell.newJob().add("${getKsuDaemonPath()} $cmd").to(ArrayList(), null).exec().out + val out = shell.newJob().add("${getKsuDaemonPath()} $cmd").to(ArrayList(), null).exec().out return out.filter { it.isNotBlank() }.map { it.trim() } } @@ -306,9 +318,8 @@ fun getSepolicy(pkg: String): String { fun setSepolicy(pkg: String, rules: String): Boolean { val shell = getRootShell() - val result = - shell.newJob().add("${getKsuDaemonPath()} profile set-sepolicy $pkg '$rules'") - .to(ArrayList(), null).exec() + val result = shell.newJob().add("${getKsuDaemonPath()} profile set-sepolicy $pkg '$rules'") + .to(ArrayList(), null).exec() Log.i(TAG, "set sepolicy result: ${result.code}") return result.isSuccess } @@ -322,22 +333,19 @@ fun listAppProfileTemplates(): List { fun getAppProfileTemplate(id: String): String { val shell = getRootShell() return shell.newJob().add("${getKsuDaemonPath()} profile get-template '${id}'") - .to(ArrayList(), null) - .exec().out.joinToString("\n") + .to(ArrayList(), null).exec().out.joinToString("\n") } fun setAppProfileTemplate(id: String, template: String): Boolean { val shell = getRootShell() return shell.newJob().add("${getKsuDaemonPath()} profile set-template '${id}' '${template}'") - .to(ArrayList(), null) - .exec().isSuccess + .to(ArrayList(), null).exec().isSuccess } fun deleteAppProfileTemplate(id: String): Boolean { val shell = getRootShell() return shell.newJob().add("${getKsuDaemonPath()} profile delete-template '${id}'") - .to(ArrayList(), null) - .exec().isSuccess + .to(ArrayList(), null).exec().isSuccess } fun forceStopApp(packageName: String) { diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index e7533f44..53c105b6 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -116,4 +116,5 @@ Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue? Next %1$s partition image is recommended + Select KMI