From 76d144b36a636b5692ef095518e064835cca048a Mon Sep 17 00:00:00 2001 From: mu7220 Date: Tue, 22 Oct 2024 14:13:43 +0800 Subject: [PATCH 1/4] manager: Add action.sh for user to manually trigger modules' functionality from manager --- .../kernelsu/ui/screen/ExecuteModuleAction.kt | 135 ++++++++++++++++++ .../me/weishu/kernelsu/ui/screen/Module.kt | 39 ++++- .../java/me/weishu/kernelsu/ui/util/KsuCli.kt | 24 ++++ .../kernelsu/ui/viewmodel/ModuleViewModel.kt | 5 +- .../src/main/res/values-zh-rCN/strings.xml | 1 + manager/app/src/main/res/values/strings.xml | 1 + userspace/ksud/src/cli.rs | 7 + userspace/ksud/src/defs.rs | 1 + userspace/ksud/src/module.rs | 24 ++++ 9 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 manager/app/src/main/java/me/weishu/kernelsu/ui/screen/ExecuteModuleAction.kt diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/ExecuteModuleAction.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/ExecuteModuleAction.kt new file mode 100644 index 000000000000..0a7a05e30db0 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/ExecuteModuleAction.kt @@ -0,0 +1,135 @@ +package me.weishu.kernelsu.ui.screen + +import android.os.Environment +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Save +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.key +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import me.weishu.kernelsu.R +import me.weishu.kernelsu.ui.component.KeyEventBlocker +import me.weishu.kernelsu.ui.util.LocalSnackbarHost +import me.weishu.kernelsu.ui.util.runModuleAction +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +@Composable +@Destination +fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String) { + var text by rememberSaveable { mutableStateOf("") } + val logContent = rememberSaveable { StringBuilder() } + val snackBarHost = LocalSnackbarHost.current + val scope = rememberCoroutineScope() + val scrollState = rememberScrollState() + + LaunchedEffect(Unit) { + if (text.isNotEmpty()) { + return@LaunchedEffect + } + withContext(Dispatchers.IO) { + runModuleAction(moduleId, onStdout = { + text += "$it\n" + logContent.append(it).append("\n") + }, onStderr = { + logContent.append(it).append("\n") + }) + } + } + + Scaffold( + topBar = { + TopBar( + onBack = { + navigator.popBackStack() + }, + onSave = { + scope.launch { + val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault()) + val date = format.format(Date()) + val file = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + "KernelSU_module_action_log_${date}.log" + ) + file.writeText(logContent.toString()) + snackBarHost.showSnackbar("Log saved to ${file.absolutePath}") + } + } + ) + }, + snackbarHost = { SnackbarHost(snackBarHost) } + ) { innerPadding -> + KeyEventBlocker { + it.key == Key.VolumeDown || it.key == Key.VolumeUp + } + Column( + modifier = Modifier + .fillMaxSize(1f) + .padding(innerPadding) + .verticalScroll(scrollState), + ) { + LaunchedEffect(text) { + scrollState.animateScrollTo(scrollState.maxValue) + } + Text( + modifier = Modifier.padding(8.dp), + text = text, + fontSize = MaterialTheme.typography.bodySmall.fontSize, + fontFamily = FontFamily.Monospace, + lineHeight = MaterialTheme.typography.bodySmall.lineHeight, + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) { + TopAppBar( + title = { Text(stringResource(R.string.action)) }, + navigationIcon = { + IconButton( + onClick = onBack + ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } + }, + actions = { + IconButton(onClick = onSave) { + Icon( + imageVector = Icons.Filled.Save, + contentDescription = stringResource(id = R.string.save_log), + ) + } + } + ) +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt index f30e8f22b110..798a152ae032 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.selection.toggleable @@ -32,6 +33,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.Button import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api @@ -77,8 +79,10 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -176,6 +180,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) { } else -> { ModuleList( + navigator, viewModel = viewModel, modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), boxModifier = Modifier.padding(innerPadding), @@ -203,6 +208,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) { @OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) @Composable private fun ModuleList( + navigator: DestinationsNavigator, viewModel: ModuleViewModel, modifier: Modifier = Modifier, boxModifier: Modifier = Modifier, @@ -392,7 +398,7 @@ private fun ModuleList( } } - ModuleItem(module, isChecked, updatedModule.first, onUninstall = { + ModuleItem(navigator, module, isChecked, updatedModule.first, onUninstall = { scope.launch { onModuleUninstall(module) } }, onCheckChanged = { scope.launch { @@ -457,6 +463,7 @@ private fun TopBar( @Composable private fun ModuleItem( + navigator: DestinationsNavigator, module: ModuleViewModel.ModuleInfo, isChecked: Boolean, updateUrl: String, @@ -609,6 +616,33 @@ private fun ModuleItem( ) } } + + if (module.hasActionScript) { + Button( + onClick = { navigator.navigate(ExecuteModuleActionScreenDestination(module.id)) }, + modifier = Modifier + .padding(0.dp) + .defaultMinSize(52.dp, 32.dp), + shape = RoundedCornerShape(6.dp), + contentPadding = PaddingValues(0.dp) + ) { + Icon( + modifier = Modifier + .padding(6.dp) + .size(20.dp), + imageVector = Icons.Filled.PlayArrow, + contentDescription = null + ) + Text( + modifier = Modifier.padding(end = 6.dp), + text = stringResource(R.string.action), + maxLines = 1, + fontFamily = MaterialTheme.typography.labelMedium.fontFamily, + fontSize = MaterialTheme.typography.labelMedium.fontSize, + softWrap = false + ) + } + } } } } @@ -629,6 +663,7 @@ fun ModuleItemPreview() { remove = true, updateJson = "", hasWebUi = false, + hasActionScript = false, ) - ModuleItem(module, true, "", {}, {}, {}, {}) + ModuleItem(EmptyDestinationsNavigator, module, true, "", {}, {}, {}, {}) } \ 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 2376876589e1..32a94b7e1768 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 @@ -188,6 +188,30 @@ fun flashModule( } } +fun runModuleAction( + moduleId: String, onStdout: (String) -> Unit, onStderr: (String) -> Unit +): Boolean { + val shell = getRootShell() + + val stdoutCallback: CallbackList = object : CallbackList() { + override fun onAddElement(s: String?) { + onStdout(s ?: "") + } + } + + val stderrCallback: CallbackList = object : CallbackList() { + override fun onAddElement(s: String?) { + onStderr(s ?: "") + } + } + + val result = shell.newJob().add("${getKsuDaemonPath()} module action $moduleId") + .to(stdoutCallback, stderrCallback).exec() + Log.i("KernelSU", "Module runAction result: $result") + + return result.isSuccess +} + fun restoreBoot( onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit ): Boolean { diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt index 013453fbfeed..4533e6e5ffc4 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt @@ -36,6 +36,7 @@ class ModuleViewModel : ViewModel() { val remove: Boolean, val updateJson: String, val hasWebUi: Boolean, + val hasActionScript: Boolean, ) data class ModuleUpdateInfo( @@ -87,7 +88,6 @@ class ModuleViewModel : ViewModel() { .map { obj -> ModuleInfo( obj.getString("id"), - obj.optString("name"), obj.optString("author", "Unknown"), obj.optString("version", "Unknown"), @@ -97,7 +97,8 @@ class ModuleViewModel : ViewModel() { obj.getBoolean("update"), obj.getBoolean("remove"), obj.optString("updateJson"), - obj.optBoolean("web") + obj.optBoolean("web"), + obj.optBoolean("action") ) }.toList() isNeedRefresh = false 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 7553f80ae2df..950c69e5c257 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -105,6 +105,7 @@ 检查更新 在应用启动后自动检查是否有最新版 获取 root 失败! + 执行 打开 启用 WebView 调试 可用于调试 WebUI ,请仅在需要时启用。 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index a9be58cfc9e6..e9d058e9e0bd 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -107,6 +107,7 @@ Check update Automatically check for updates when opening the app Failed to grant root! + Action Open Enable WebView debugging Can be used to debug WebUI, please enable only when needed. diff --git a/userspace/ksud/src/cli.rs b/userspace/ksud/src/cli.rs index cd96a360f346..14717e62df78 100644 --- a/userspace/ksud/src/cli.rs +++ b/userspace/ksud/src/cli.rs @@ -223,6 +223,12 @@ enum Module { id: String, }, + /// run action for module + Action { + // module id + id: String, + }, + /// list all modules List, @@ -306,6 +312,7 @@ pub fn run() -> Result<()> { Module::Uninstall { id } => module::uninstall_module(&id), Module::Enable { id } => module::enable_module(&id), Module::Disable { id } => module::disable_module(&id), + Module::Action { id } => module::run_action(&id), Module::List => module::list_modules(), Module::Shrink => module::shrink_ksu_images(), } diff --git a/userspace/ksud/src/defs.rs b/userspace/ksud/src/defs.rs index dd71a0f63e82..d5514501d140 100644 --- a/userspace/ksud/src/defs.rs +++ b/userspace/ksud/src/defs.rs @@ -30,6 +30,7 @@ pub const SYSTEM_RW_DIR: &str = concatcp!(MODULE_DIR, ".rw/"); pub const TEMP_DIR: &str = "/debug_ramdisk"; pub const MODULE_WEB_DIR: &str = "webroot"; +pub const MODULE_ACTION_SH: &str = "action.sh"; pub const DISABLE_FILE_NAME: &str = "disable"; pub const UPDATE_FILE_NAME: &str = "update"; pub const REMOVE_FILE_NAME: &str = "remove"; diff --git a/userspace/ksud/src/module.rs b/userspace/ksud/src/module.rs index ac3ff213b62f..9aa257c255eb 100644 --- a/userspace/ksud/src/module.rs +++ b/userspace/ksud/src/module.rs @@ -570,6 +570,28 @@ pub fn uninstall_module(id: &str) -> Result<()> { }) } +pub fn run_action(id: &str) -> Result<()> { + let action_script_path = format!("/data/adb/modules/{}/action.sh", id); + let result = Command::new(assets::BUSYBOX_PATH) + .args(["sh", &action_script_path]) + .env("ASH_STANDALONE", "1") + .env( + "PATH", + format!( + "{}:{}", + env_var("PATH").unwrap(), + defs::BINARY_DIR.trim_end_matches('/') + ), + ) + .env("KSU", "true") + .env("KSU_VER", defs::VERSION_NAME) + .env("KSU_KERNEL_VER_CODE", defs::VERSION_CODE) + .env("OUTFD", "1") + .status()?; + ensure!(result.success(), "Failed to execute action script"); + Ok(()) +} + fn _enable_module(module_dir: &str, mid: &str, enable: bool) -> Result<()> { let src_module_path = format!("{module_dir}/{mid}"); let src_module = Path::new(&src_module_path); @@ -668,11 +690,13 @@ fn _list_modules(path: &str) -> Vec> { let update = path.join(defs::UPDATE_FILE_NAME).exists(); let remove = path.join(defs::REMOVE_FILE_NAME).exists(); let web = path.join(defs::MODULE_WEB_DIR).exists(); + let action = path.join(defs::MODULE_ACTION_SH).exists(); module_prop_map.insert("enabled".to_owned(), enabled.to_string()); module_prop_map.insert("update".to_owned(), update.to_string()); module_prop_map.insert("remove".to_owned(), remove.to_string()); module_prop_map.insert("web".to_owned(), web.to_string()); + module_prop_map.insert("action".to_owned(), action.to_string()); if result.is_err() { warn!("Failed to parse module.prop: {}", module_prop.display()); From ef4c8ab7d96a1ec92872a5e38194de209b14fadf Mon Sep 17 00:00:00 2001 From: Light summer <93428659+lightsummer233@users.noreply.github.com> Date: Wed, 23 Oct 2024 23:18:01 +0800 Subject: [PATCH 2/4] manager: Optimize `ModuleItem` Co-authored-by: lingqiqi5211 --- .../kernelsu/ui/screen/ExecuteModuleAction.kt | 20 ++- .../me/weishu/kernelsu/ui/screen/Module.kt | 141 +++++++++--------- 2 files changed, 87 insertions(+), 74 deletions(-) diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/ExecuteModuleAction.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/ExecuteModuleAction.kt index 0a7a05e30db0..69480997d73f 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/ExecuteModuleAction.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/ExecuteModuleAction.kt @@ -53,19 +53,27 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String val snackBarHost = LocalSnackbarHost.current val scope = rememberCoroutineScope() val scrollState = rememberScrollState() + var actionResult: Boolean LaunchedEffect(Unit) { if (text.isNotEmpty()) { return@LaunchedEffect } withContext(Dispatchers.IO) { - runModuleAction(moduleId, onStdout = { - text += "$it\n" - logContent.append(it).append("\n") - }, onStderr = { - logContent.append(it).append("\n") - }) + runModuleAction( + moduleId = moduleId, + onStdout = { + text += "$it\n" + logContent.append(it).append("\n") + }, + onStderr = { + logContent.append(it).append("\n") + } + ).let { + actionResult = it + } } + if (actionResult) navigator.popBackStack() } Scaffold( diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt index 798a152ae032..fd5a473e624c 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt @@ -29,15 +29,16 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.selection.toggleable -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.Wysiwyg import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.outlined.PlayArrow import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -51,7 +52,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable @@ -125,7 +125,11 @@ fun ModuleScreen(navigator: DestinationsNavigator) { Scaffold( topBar = { - TopBar(scrollBehavior = scrollBehavior) + TopAppBar( + scrollBehavior = scrollBehavior, + title = { Text(stringResource(R.string.module)) }, + windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + ) }, floatingActionButton = { if (hideInstallButton) { @@ -151,8 +155,9 @@ fun ModuleScreen(navigator: DestinationsNavigator) { ExtendedFloatingActionButton( onClick = { // select the zip file to install - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.type = "application/zip" + val intent = Intent(Intent.ACTION_GET_CONTENT).apply { + type = "application/zip" + } selectZipLauncher.launch(intent) }, icon = { Icon(Icons.Filled.Add, moduleInstall) }, @@ -205,7 +210,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) { } } -@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ModuleList( navigator: DestinationsNavigator, @@ -449,20 +454,8 @@ private fun ModuleList( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable -private fun TopBar( - scrollBehavior: TopAppBarScrollBehavior? = null -) { - TopAppBar( - scrollBehavior = scrollBehavior, - title = { Text(stringResource(R.string.module)) }, - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) - ) -} - -@Composable -private fun ModuleItem( +fun ModuleItem( navigator: DestinationsNavigator, module: ModuleViewModel.ModuleInfo, isChecked: Boolean, @@ -501,7 +494,7 @@ private fun ModuleItem( ) } } - .padding(24.dp, 16.dp, 24.dp, 0.dp) + .padding(22.dp, 18.dp, 22.dp, 12.dp) ) { Row( modifier = Modifier.fillMaxWidth(), @@ -510,7 +503,9 @@ private fun ModuleItem( val moduleVersion = stringResource(id = R.string.module_version) val moduleAuthor = stringResource(id = R.string.module_author) - Column(modifier = Modifier.fillMaxWidth(0.8f)) { + Column( + modifier = Modifier.fillMaxWidth(0.8f) + ) { Text( text = module.name, fontSize = MaterialTheme.typography.titleMedium.fontSize, @@ -565,83 +560,93 @@ private fun ModuleItem( textDecoration = textDecoration ) - Spacer(modifier = Modifier.height(16.dp)) HorizontalDivider(thickness = Dp.Hairline) + Spacer(modifier = Modifier.height(4.dp)) + Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Spacer(modifier = Modifier.weight(1f, true)) - if (updateUrl.isNotEmpty()) { - Button( - modifier = Modifier - .padding(0.dp) - .defaultMinSize(52.dp, 32.dp), - onClick = { onUpdate(module) }, - shape = RoundedCornerShape(6.dp), - contentPadding = PaddingValues(0.dp) + if (module.hasActionScript) { + FilledTonalButton( + modifier = Modifier.defaultMinSize(52.dp, 32.dp), + onClick = { navigator.navigate(ExecuteModuleActionScreenDestination(module.id)) }, + contentPadding = ButtonDefaults.TextButtonContentPadding ) { + Icon( + modifier = Modifier + .padding(end = 7.dp) + .size(20.dp), + imageVector = Icons.Outlined.PlayArrow, + contentDescription = null + ) Text( + text = stringResource(R.string.action), fontFamily = MaterialTheme.typography.labelMedium.fontFamily, - fontSize = MaterialTheme.typography.labelMedium.fontSize, - text = stringResource(R.string.module_update), + fontSize = MaterialTheme.typography.labelMedium.fontSize ) } - } - TextButton( - enabled = !module.remove, - onClick = { onUninstall(module) }, - ) { - Text( - fontFamily = MaterialTheme.typography.labelMedium.fontFamily, - fontSize = MaterialTheme.typography.labelMedium.fontSize, - text = stringResource(R.string.uninstall), - ) + Spacer(modifier = Modifier.weight(0.1f, true)) } if (module.hasWebUi) { - TextButton( + FilledTonalButton( + modifier = Modifier.defaultMinSize(52.dp, 32.dp), onClick = { onClick(module) }, - interactionSource = interactionSource + interactionSource = interactionSource, + contentPadding = ButtonDefaults.TextButtonContentPadding ) { + if (!module.hasActionScript) { + Icon( + modifier = Modifier + .padding(end = 7.dp) + .size(20.dp), + imageVector = Icons.AutoMirrored.Outlined.Wysiwyg, + contentDescription = null + ) + } Text( fontFamily = MaterialTheme.typography.labelMedium.fontFamily, fontSize = MaterialTheme.typography.labelMedium.fontSize, - text = stringResource(R.string.open), + text = stringResource(R.string.open) ) } } - if (module.hasActionScript) { + Spacer(modifier = Modifier.weight(1f, true)) + + if (updateUrl.isNotEmpty()) { Button( - onClick = { navigator.navigate(ExecuteModuleActionScreenDestination(module.id)) }, - modifier = Modifier - .padding(0.dp) - .defaultMinSize(52.dp, 32.dp), - shape = RoundedCornerShape(6.dp), - contentPadding = PaddingValues(0.dp) + modifier = Modifier.defaultMinSize(52.dp, 32.dp), + onClick = { onUpdate(module) }, + shape = ButtonDefaults.textShape, + contentPadding = ButtonDefaults.TextButtonContentPadding ) { - Icon( - modifier = Modifier - .padding(6.dp) - .size(20.dp), - imageVector = Icons.Filled.PlayArrow, - contentDescription = null - ) Text( - modifier = Modifier.padding(end = 6.dp), - text = stringResource(R.string.action), - maxLines = 1, fontFamily = MaterialTheme.typography.labelMedium.fontFamily, fontSize = MaterialTheme.typography.labelMedium.fontSize, - softWrap = false + text = stringResource(R.string.module_update) ) } + + Spacer(modifier = Modifier.weight(0.1f, true)) + } + + TextButton( + modifier = Modifier.defaultMinSize(52.dp, 32.dp), + enabled = !module.remove, + onClick = { onUninstall(module) } + ) { + Text( + fontFamily = MaterialTheme.typography.labelMedium.fontFamily, + fontSize = MaterialTheme.typography.labelMedium.fontSize, + text = stringResource(R.string.uninstall) + ) } } } @@ -660,10 +665,10 @@ fun ModuleItemPreview() { description = "I am a test module and i do nothing but show a very long description", enabled = true, update = true, - remove = true, + remove = false, updateJson = "", hasWebUi = false, - hasActionScript = false, + hasActionScript = false ) ModuleItem(EmptyDestinationsNavigator, module, true, "", {}, {}, {}, {}) } \ No newline at end of file From bcfcd58816432afc8ebc52b9d6c2ee57eb3efc2c Mon Sep 17 00:00:00 2001 From: Light summer <93428659+lightsummer233@users.noreply.github.com> Date: Wed, 23 Oct 2024 23:58:26 +0800 Subject: [PATCH 3/4] manager: uninstall button: `TextButton` -> `FilledTonalButton` Bump Compose Destination to `2.1.0-beta14` Co-authored-by: lingqiqi5211 --- .../me/weishu/kernelsu/ui/screen/Module.kt | 82 +++++++++++-------- manager/gradle/libs.versions.toml | 4 +- 2 files changed, 48 insertions(+), 38 deletions(-) diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt index fd5a473e624c..75bedce17fb7 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt @@ -49,7 +49,6 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Switch import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.pulltorefresh.PullToRefreshBox @@ -183,6 +182,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) { ) } } + else -> { ModuleList( navigator, @@ -403,44 +403,53 @@ private fun ModuleList( } } - ModuleItem(navigator, module, isChecked, updatedModule.first, onUninstall = { - scope.launch { onModuleUninstall(module) } - }, onCheckChanged = { - scope.launch { - val success = loadingDialog.withLoading { - withContext(Dispatchers.IO) { - toggleModule(module.id, !isChecked) + ModuleItem( + navigator = navigator, + module = module, + isChecked = isChecked, + updateUrl = updatedModule.first, + onUninstall = { + scope.launch { onModuleUninstall(module) } + }, + onCheckChanged = { + scope.launch { + val success = loadingDialog.withLoading { + withContext(Dispatchers.IO) { + toggleModule(module.id, !isChecked) + } + } + if (success) { + isChecked = it + viewModel.fetchModuleList() + + val result = snackBarHost.showSnackbar( + message = rebootToApply, + actionLabel = reboot, + duration = SnackbarDuration.Long + ) + if (result == SnackbarResult.ActionPerformed) { + reboot() + } + } else { + val message = if (isChecked) failedDisable else failedEnable + snackBarHost.showSnackbar(message.format(module.name)) } } - if (success) { - isChecked = it - viewModel.fetchModuleList() - - val result = snackBarHost.showSnackbar( - message = rebootToApply, - actionLabel = reboot, - duration = SnackbarDuration.Long + }, + onUpdate = { + scope.launch { + onModuleUpdate( + module, + updatedModule.third, + updatedModule.first, + "${module.name}-${updatedModule.second}.zip" ) - if (result == SnackbarResult.ActionPerformed) { - reboot() - } - } else { - val message = if (isChecked) failedDisable else failedEnable - snackBarHost.showSnackbar(message.format(module.name)) } + }, + onClick = { + onClickModule(it.id, it.name, it.hasWebUi) } - }, onUpdate = { - scope.launch { - onModuleUpdate( - module, - updatedModule.third, - updatedModule.first, - "${module.name}-${updatedModule.second}.zip" - ) - } - }, onClick = { - onClickModule(it.id, it.name, it.hasWebUi) - }) + ) // fix last item shadow incomplete in LazyColumn Spacer(Modifier.height(1.dp)) @@ -637,10 +646,11 @@ fun ModuleItem( Spacer(modifier = Modifier.weight(0.1f, true)) } - TextButton( + FilledTonalButton( modifier = Modifier.defaultMinSize(52.dp, 32.dp), enabled = !module.remove, - onClick = { onUninstall(module) } + onClick = { onUninstall(module) }, + contentPadding = ButtonDefaults.TextButtonContentPadding ) { Text( fontFamily = MaterialTheme.typography.labelMedium.fontFamily, diff --git a/manager/gradle/libs.versions.toml b/manager/gradle/libs.versions.toml index e739e139319d..42e9ce44741d 100644 --- a/manager/gradle/libs.versions.toml +++ b/manager/gradle/libs.versions.toml @@ -8,7 +8,7 @@ navigation = "2.8.3" activity-compose = "1.9.3" kotlinx-coroutines = "1.9.0" coil-compose = "2.7.0" -compose-destination = "2.1.0-beta13" +compose-destination = "2.1.0-beta14" sheets-compose-dialogs = "1.3.0" markdown = "4.6.2" webkit = "1.12.1" @@ -52,7 +52,7 @@ androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } com-github-topjohnwu-libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" } com-github-topjohnwu-libsu-service = { group = "com.github.topjohnwu.libsu", name = "service", version.ref = "libsu" } -com-github-topjohnwu-libsu-io= { group = "com.github.topjohnwu.libsu", name = "io", version.ref = "libsu" } +com-github-topjohnwu-libsu-io = { group = "com.github.topjohnwu.libsu", name = "io", version.ref = "libsu" } dev-rikka-rikkax-parcelablelist = { module = "dev.rikka.rikkax.parcelablelist:parcelablelist", version.ref = "parcelablelist" } From 6ba18ac19ec5e7dcb2bc673dc324b9c7b1b43f49 Mon Sep 17 00:00:00 2001 From: mu7220 Date: Sun, 27 Oct 2024 14:43:28 +0800 Subject: [PATCH 4/4] Optimize `run_action` function (cherry picked from commit 98cd3cd515410348533e594cf3c61797f236959a) --- userspace/ksud/src/module.rs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/userspace/ksud/src/module.rs b/userspace/ksud/src/module.rs index 9aa257c255eb..28c5df405795 100644 --- a/userspace/ksud/src/module.rs +++ b/userspace/ksud/src/module.rs @@ -572,24 +572,7 @@ pub fn uninstall_module(id: &str) -> Result<()> { pub fn run_action(id: &str) -> Result<()> { let action_script_path = format!("/data/adb/modules/{}/action.sh", id); - let result = Command::new(assets::BUSYBOX_PATH) - .args(["sh", &action_script_path]) - .env("ASH_STANDALONE", "1") - .env( - "PATH", - format!( - "{}:{}", - env_var("PATH").unwrap(), - defs::BINARY_DIR.trim_end_matches('/') - ), - ) - .env("KSU", "true") - .env("KSU_VER", defs::VERSION_NAME) - .env("KSU_KERNEL_VER_CODE", defs::VERSION_CODE) - .env("OUTFD", "1") - .status()?; - ensure!(result.success(), "Failed to execute action script"); - Ok(()) + exec_script(&action_script_path, true) } fn _enable_module(module_dir: &str, mid: &str, enable: bool) -> Result<()> {