manager: Add action.sh for user to manually trigger modules' functionality from manager (#2156)
Commits: - manager: Add `action.sh` for user to manually trigger modules' functionality from manager - manager: Optimize ModuleItem - manager: uninstall button: TextButton -> FilledTonalButton - Optimize `run_action` function Commit Author & Thank: - @lightsummer233 - @lingqiqi5211 - [APatch](https://github.com/bmax121/APatch) Demo Show:  --------- Co-authored-by: Light summer <93428659+lightsummer233@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,143 @@
|
|||||||
|
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<RootGraph>
|
||||||
|
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()
|
||||||
|
var actionResult: Boolean
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (text.isNotEmpty()) {
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
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(
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -25,17 +25,20 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.only
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.selection.toggleable
|
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.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.Wysiwyg
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.outlined.PlayArrow
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -46,10 +49,8 @@ import androidx.compose.material3.SnackbarHostState
|
|||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -77,8 +78,10 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination
|
||||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||||
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
|
||||||
@@ -121,7 +124,11 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopBar(scrollBehavior = scrollBehavior)
|
TopAppBar(
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
title = { Text(stringResource(R.string.module)) },
|
||||||
|
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
if (hideInstallButton) {
|
if (hideInstallButton) {
|
||||||
@@ -147,8 +154,9 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
// select the zip file to install
|
// select the zip file to install
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
intent.type = "application/zip"
|
type = "application/zip"
|
||||||
|
}
|
||||||
selectZipLauncher.launch(intent)
|
selectZipLauncher.launch(intent)
|
||||||
},
|
},
|
||||||
icon = { Icon(Icons.Filled.Add, moduleInstall) },
|
icon = { Icon(Icons.Filled.Add, moduleInstall) },
|
||||||
@@ -174,8 +182,10 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
ModuleList(
|
ModuleList(
|
||||||
|
navigator,
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
boxModifier = Modifier.padding(innerPadding),
|
boxModifier = Modifier.padding(innerPadding),
|
||||||
@@ -200,9 +210,10 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun ModuleList(
|
private fun ModuleList(
|
||||||
|
navigator: DestinationsNavigator,
|
||||||
viewModel: ModuleViewModel,
|
viewModel: ModuleViewModel,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
boxModifier: Modifier = Modifier,
|
boxModifier: Modifier = Modifier,
|
||||||
@@ -392,9 +403,15 @@ private fun ModuleList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ModuleItem(module, isChecked, updatedModule.first, onUninstall = {
|
ModuleItem(
|
||||||
|
navigator = navigator,
|
||||||
|
module = module,
|
||||||
|
isChecked = isChecked,
|
||||||
|
updateUrl = updatedModule.first,
|
||||||
|
onUninstall = {
|
||||||
scope.launch { onModuleUninstall(module) }
|
scope.launch { onModuleUninstall(module) }
|
||||||
}, onCheckChanged = {
|
},
|
||||||
|
onCheckChanged = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val success = loadingDialog.withLoading {
|
val success = loadingDialog.withLoading {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
@@ -418,7 +435,8 @@ private fun ModuleList(
|
|||||||
snackBarHost.showSnackbar(message.format(module.name))
|
snackBarHost.showSnackbar(message.format(module.name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, onUpdate = {
|
},
|
||||||
|
onUpdate = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
onModuleUpdate(
|
onModuleUpdate(
|
||||||
module,
|
module,
|
||||||
@@ -427,9 +445,11 @@ private fun ModuleList(
|
|||||||
"${module.name}-${updatedModule.second}.zip"
|
"${module.name}-${updatedModule.second}.zip"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, onClick = {
|
},
|
||||||
|
onClick = {
|
||||||
onClickModule(it.id, it.name, it.hasWebUi)
|
onClickModule(it.id, it.name, it.hasWebUi)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// fix last item shadow incomplete in LazyColumn
|
// fix last item shadow incomplete in LazyColumn
|
||||||
Spacer(Modifier.height(1.dp))
|
Spacer(Modifier.height(1.dp))
|
||||||
@@ -443,20 +463,9 @@ private fun ModuleList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TopBar(
|
fun ModuleItem(
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
navigator: DestinationsNavigator,
|
||||||
) {
|
|
||||||
TopAppBar(
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
title = { Text(stringResource(R.string.module)) },
|
|
||||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ModuleItem(
|
|
||||||
module: ModuleViewModel.ModuleInfo,
|
module: ModuleViewModel.ModuleInfo,
|
||||||
isChecked: Boolean,
|
isChecked: Boolean,
|
||||||
updateUrl: String,
|
updateUrl: String,
|
||||||
@@ -494,7 +503,7 @@ private fun ModuleItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(24.dp, 16.dp, 24.dp, 0.dp)
|
.padding(22.dp, 18.dp, 22.dp, 12.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -503,7 +512,9 @@ private fun ModuleItem(
|
|||||||
val moduleVersion = stringResource(id = R.string.module_version)
|
val moduleVersion = stringResource(id = R.string.module_version)
|
||||||
val moduleAuthor = stringResource(id = R.string.module_author)
|
val moduleAuthor = stringResource(id = R.string.module_author)
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxWidth(0.8f)) {
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(0.8f)
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = module.name,
|
text = module.name,
|
||||||
fontSize = MaterialTheme.typography.titleMedium.fontSize,
|
fontSize = MaterialTheme.typography.titleMedium.fontSize,
|
||||||
@@ -558,57 +569,95 @@ private fun ModuleItem(
|
|||||||
textDecoration = textDecoration
|
textDecoration = textDecoration
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
HorizontalDivider(thickness = Dp.Hairline)
|
HorizontalDivider(thickness = Dp.Hairline)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(0.1f, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (module.hasWebUi) {
|
||||||
|
FilledTonalButton(
|
||||||
|
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
||||||
|
onClick = { onClick(module) },
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f, true))
|
Spacer(modifier = Modifier.weight(1f, true))
|
||||||
|
|
||||||
if (updateUrl.isNotEmpty()) {
|
if (updateUrl.isNotEmpty()) {
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier
|
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
||||||
.padding(0.dp)
|
|
||||||
.defaultMinSize(52.dp, 32.dp),
|
|
||||||
onClick = { onUpdate(module) },
|
onClick = { onUpdate(module) },
|
||||||
shape = RoundedCornerShape(6.dp),
|
shape = ButtonDefaults.textShape,
|
||||||
contentPadding = PaddingValues(0.dp)
|
contentPadding = ButtonDefaults.TextButtonContentPadding
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||||
text = stringResource(R.string.module_update),
|
text = stringResource(R.string.module_update)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(0.1f, true))
|
||||||
}
|
}
|
||||||
|
|
||||||
TextButton(
|
FilledTonalButton(
|
||||||
|
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
||||||
enabled = !module.remove,
|
enabled = !module.remove,
|
||||||
onClick = { onUninstall(module) },
|
onClick = { onUninstall(module) },
|
||||||
|
contentPadding = ButtonDefaults.TextButtonContentPadding
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||||
text = stringResource(R.string.uninstall),
|
text = stringResource(R.string.uninstall)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (module.hasWebUi) {
|
|
||||||
TextButton(
|
|
||||||
onClick = { onClick(module) },
|
|
||||||
interactionSource = interactionSource
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
|
||||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
|
||||||
text = stringResource(R.string.open),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -626,9 +675,10 @@ fun ModuleItemPreview() {
|
|||||||
description = "I am a test module and i do nothing but show a very long description",
|
description = "I am a test module and i do nothing but show a very long description",
|
||||||
enabled = true,
|
enabled = true,
|
||||||
update = true,
|
update = true,
|
||||||
remove = true,
|
remove = false,
|
||||||
updateJson = "",
|
updateJson = "",
|
||||||
hasWebUi = false,
|
hasWebUi = false,
|
||||||
|
hasActionScript = false
|
||||||
)
|
)
|
||||||
ModuleItem(module, true, "", {}, {}, {}, {})
|
ModuleItem(EmptyDestinationsNavigator, module, true, "", {}, {}, {}, {})
|
||||||
}
|
}
|
||||||
@@ -188,6 +188,30 @@ fun flashModule(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun runModuleAction(
|
||||||
|
moduleId: String, onStdout: (String) -> Unit, onStderr: (String) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
val shell = getRootShell()
|
||||||
|
|
||||||
|
val stdoutCallback: CallbackList<String?> = object : CallbackList<String?>() {
|
||||||
|
override fun onAddElement(s: String?) {
|
||||||
|
onStdout(s ?: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val stderrCallback: CallbackList<String?> = object : CallbackList<String?>() {
|
||||||
|
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(
|
fun restoreBoot(
|
||||||
onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit
|
onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class ModuleViewModel : ViewModel() {
|
|||||||
val remove: Boolean,
|
val remove: Boolean,
|
||||||
val updateJson: String,
|
val updateJson: String,
|
||||||
val hasWebUi: Boolean,
|
val hasWebUi: Boolean,
|
||||||
|
val hasActionScript: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ModuleUpdateInfo(
|
data class ModuleUpdateInfo(
|
||||||
@@ -87,7 +88,6 @@ class ModuleViewModel : ViewModel() {
|
|||||||
.map { obj ->
|
.map { obj ->
|
||||||
ModuleInfo(
|
ModuleInfo(
|
||||||
obj.getString("id"),
|
obj.getString("id"),
|
||||||
|
|
||||||
obj.optString("name"),
|
obj.optString("name"),
|
||||||
obj.optString("author", "Unknown"),
|
obj.optString("author", "Unknown"),
|
||||||
obj.optString("version", "Unknown"),
|
obj.optString("version", "Unknown"),
|
||||||
@@ -97,7 +97,8 @@ class ModuleViewModel : ViewModel() {
|
|||||||
obj.getBoolean("update"),
|
obj.getBoolean("update"),
|
||||||
obj.getBoolean("remove"),
|
obj.getBoolean("remove"),
|
||||||
obj.optString("updateJson"),
|
obj.optString("updateJson"),
|
||||||
obj.optBoolean("web")
|
obj.optBoolean("web"),
|
||||||
|
obj.optBoolean("action")
|
||||||
)
|
)
|
||||||
}.toList()
|
}.toList()
|
||||||
isNeedRefresh = false
|
isNeedRefresh = false
|
||||||
|
|||||||
@@ -105,6 +105,7 @@
|
|||||||
<string name="settings_check_update">检查更新</string>
|
<string name="settings_check_update">检查更新</string>
|
||||||
<string name="settings_check_update_summary">在应用启动后自动检查是否有最新版</string>
|
<string name="settings_check_update_summary">在应用启动后自动检查是否有最新版</string>
|
||||||
<string name="grant_root_failed">获取 root 失败!</string>
|
<string name="grant_root_failed">获取 root 失败!</string>
|
||||||
|
<string name="action">执行</string>
|
||||||
<string name="open">打开</string>
|
<string name="open">打开</string>
|
||||||
<string name="enable_web_debugging">启用 WebView 调试</string>
|
<string name="enable_web_debugging">启用 WebView 调试</string>
|
||||||
<string name="enable_web_debugging_summary">可用于调试 WebUI ,请仅在需要时启用。</string>
|
<string name="enable_web_debugging_summary">可用于调试 WebUI ,请仅在需要时启用。</string>
|
||||||
|
|||||||
@@ -107,6 +107,7 @@
|
|||||||
<string name="settings_check_update">Check update</string>
|
<string name="settings_check_update">Check update</string>
|
||||||
<string name="settings_check_update_summary">Automatically check for updates when opening the app</string>
|
<string name="settings_check_update_summary">Automatically check for updates when opening the app</string>
|
||||||
<string name="grant_root_failed">Failed to grant root!</string>
|
<string name="grant_root_failed">Failed to grant root!</string>
|
||||||
|
<string name="action">Action</string>
|
||||||
<string name="open">Open</string>
|
<string name="open">Open</string>
|
||||||
<string name="enable_web_debugging">Enable WebView debugging</string>
|
<string name="enable_web_debugging">Enable WebView debugging</string>
|
||||||
<string name="enable_web_debugging_summary">Can be used to debug WebUI, please enable only when needed.</string>
|
<string name="enable_web_debugging_summary">Can be used to debug WebUI, please enable only when needed.</string>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ navigation = "2.8.3"
|
|||||||
activity-compose = "1.9.3"
|
activity-compose = "1.9.3"
|
||||||
kotlinx-coroutines = "1.9.0"
|
kotlinx-coroutines = "1.9.0"
|
||||||
coil-compose = "2.7.0"
|
coil-compose = "2.7.0"
|
||||||
compose-destination = "2.1.0-beta13"
|
compose-destination = "2.1.0-beta14"
|
||||||
sheets-compose-dialogs = "1.3.0"
|
sheets-compose-dialogs = "1.3.0"
|
||||||
markdown = "4.6.2"
|
markdown = "4.6.2"
|
||||||
webkit = "1.12.1"
|
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-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-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" }
|
dev-rikka-rikkax-parcelablelist = { module = "dev.rikka.rikkax.parcelablelist:parcelablelist", version.ref = "parcelablelist" }
|
||||||
|
|
||||||
|
|||||||
@@ -223,6 +223,12 @@ enum Module {
|
|||||||
id: String,
|
id: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// run action for module <id>
|
||||||
|
Action {
|
||||||
|
// module id
|
||||||
|
id: String,
|
||||||
|
},
|
||||||
|
|
||||||
/// list all modules
|
/// list all modules
|
||||||
List,
|
List,
|
||||||
|
|
||||||
@@ -306,6 +312,7 @@ pub fn run() -> Result<()> {
|
|||||||
Module::Uninstall { id } => module::uninstall_module(&id),
|
Module::Uninstall { id } => module::uninstall_module(&id),
|
||||||
Module::Enable { id } => module::enable_module(&id),
|
Module::Enable { id } => module::enable_module(&id),
|
||||||
Module::Disable { id } => module::disable_module(&id),
|
Module::Disable { id } => module::disable_module(&id),
|
||||||
|
Module::Action { id } => module::run_action(&id),
|
||||||
Module::List => module::list_modules(),
|
Module::List => module::list_modules(),
|
||||||
Module::Shrink => module::shrink_ksu_images(),
|
Module::Shrink => module::shrink_ksu_images(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ pub const SYSTEM_RW_DIR: &str = concatcp!(MODULE_DIR, ".rw/");
|
|||||||
|
|
||||||
pub const TEMP_DIR: &str = "/debug_ramdisk";
|
pub const TEMP_DIR: &str = "/debug_ramdisk";
|
||||||
pub const MODULE_WEB_DIR: &str = "webroot";
|
pub const MODULE_WEB_DIR: &str = "webroot";
|
||||||
|
pub const MODULE_ACTION_SH: &str = "action.sh";
|
||||||
pub const DISABLE_FILE_NAME: &str = "disable";
|
pub const DISABLE_FILE_NAME: &str = "disable";
|
||||||
pub const UPDATE_FILE_NAME: &str = "update";
|
pub const UPDATE_FILE_NAME: &str = "update";
|
||||||
pub const REMOVE_FILE_NAME: &str = "remove";
|
pub const REMOVE_FILE_NAME: &str = "remove";
|
||||||
|
|||||||
@@ -570,6 +570,11 @@ 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);
|
||||||
|
exec_script(&action_script_path, true)
|
||||||
|
}
|
||||||
|
|
||||||
fn _enable_module(module_dir: &str, mid: &str, enable: bool) -> Result<()> {
|
fn _enable_module(module_dir: &str, mid: &str, enable: bool) -> Result<()> {
|
||||||
let src_module_path = format!("{module_dir}/{mid}");
|
let src_module_path = format!("{module_dir}/{mid}");
|
||||||
let src_module = Path::new(&src_module_path);
|
let src_module = Path::new(&src_module_path);
|
||||||
@@ -668,11 +673,13 @@ fn _list_modules(path: &str) -> Vec<HashMap<String, String>> {
|
|||||||
let update = path.join(defs::UPDATE_FILE_NAME).exists();
|
let update = path.join(defs::UPDATE_FILE_NAME).exists();
|
||||||
let remove = path.join(defs::REMOVE_FILE_NAME).exists();
|
let remove = path.join(defs::REMOVE_FILE_NAME).exists();
|
||||||
let web = path.join(defs::MODULE_WEB_DIR).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("enabled".to_owned(), enabled.to_string());
|
||||||
module_prop_map.insert("update".to_owned(), update.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("remove".to_owned(), remove.to_string());
|
||||||
module_prop_map.insert("web".to_owned(), web.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() {
|
if result.is_err() {
|
||||||
warn!("Failed to parse module.prop: {}", module_prop.display());
|
warn!("Failed to parse module.prop: {}", module_prop.display());
|
||||||
|
|||||||
Reference in New Issue
Block a user