manager: Add install screen
This commit is contained in:
@@ -0,0 +1,115 @@
|
|||||||
|
package me.weishu.kernelsu.ui.screen
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Environment
|
||||||
|
import android.util.Log
|
||||||
|
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.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Save
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
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.ramcosta.composedestinations.annotation.Destination
|
||||||
|
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.ksuApp
|
||||||
|
import me.weishu.kernelsu.ui.util.LocalSnackbarHost
|
||||||
|
import me.weishu.kernelsu.ui.util.installModule
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author weishu
|
||||||
|
* @date 2023/1/1.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
@Destination
|
||||||
|
fun InstallScreen(navigator: DestinationsNavigator, uri: Uri) {
|
||||||
|
|
||||||
|
var text by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
val snackBarHost = LocalSnackbarHost.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
installModule(uri) {
|
||||||
|
text += "$it\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(ksuApp.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "KernelSU_install_log_${date}.log")
|
||||||
|
file.writeText(text)
|
||||||
|
snackBarHost.showSnackbar("Log saved to ${file.absolutePath}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(1f)
|
||||||
|
.padding(innerPadding)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(8.dp),
|
||||||
|
text = text,
|
||||||
|
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||||
|
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||||
|
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(stringResource(R.string.install)) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = onBack
|
||||||
|
) { Icon(Icons.Filled.ArrowBack, contentDescription = null) }
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = onSave) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Save,
|
||||||
|
contentDescription = "Localized description"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun InstallPreview() {
|
||||||
|
// InstallScreen(DestinationsNavigator(), uri = Uri.EMPTY)
|
||||||
|
}
|
||||||
@@ -24,15 +24,19 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.weishu.kernelsu.R
|
import me.weishu.kernelsu.R
|
||||||
|
import me.weishu.kernelsu.ui.screen.destinations.InstallScreenDestination
|
||||||
import me.weishu.kernelsu.ui.util.LocalSnackbarHost
|
import me.weishu.kernelsu.ui.util.LocalSnackbarHost
|
||||||
|
import me.weishu.kernelsu.ui.util.toggleModule
|
||||||
|
import me.weishu.kernelsu.ui.util.uninstallModule
|
||||||
import me.weishu.kernelsu.ui.viewmodel.ModuleViewModel
|
import me.weishu.kernelsu.ui.viewmodel.ModuleViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Destination
|
@Destination
|
||||||
@Composable
|
@Composable
|
||||||
fun ModuleScreen() {
|
fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||||
val viewModel = viewModel<ModuleViewModel>()
|
val viewModel = viewModel<ModuleViewModel>()
|
||||||
val snackBarHost = LocalSnackbarHost.current
|
val snackBarHost = LocalSnackbarHost.current
|
||||||
|
|
||||||
@@ -59,9 +63,8 @@ fun ModuleScreen() {
|
|||||||
val data = it.data ?: return@rememberLauncherForActivityResult
|
val data = it.data ?: return@rememberLauncherForActivityResult
|
||||||
val uri = data.data ?: return@rememberLauncherForActivityResult
|
val uri = data.data ?: return@rememberLauncherForActivityResult
|
||||||
|
|
||||||
scope.launch {
|
navigator.navigate(InstallScreenDestination(uri))
|
||||||
viewModel.installModule(uri)
|
|
||||||
}
|
|
||||||
Log.i("ModuleScreen", "select zip result: ${it.data}")
|
Log.i("ModuleScreen", "select zip result: ${it.data}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,11 +107,11 @@ fun ModuleScreen() {
|
|||||||
isChecked,
|
isChecked,
|
||||||
onUninstall = {
|
onUninstall = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val result = viewModel.uninstallModule(module.id)
|
val result = uninstallModule(module.id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onCheckChanged = {
|
onCheckChanged = {
|
||||||
val success = viewModel.toggleModule(module.id, isChecked)
|
val success = toggleModule(module.id, isChecked)
|
||||||
if (success) {
|
if (success) {
|
||||||
isChecked = it
|
isChecked = it
|
||||||
} else scope.launch {
|
} else scope.launch {
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package me.weishu.kernelsu.ui.util
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import com.topjohnwu.superuser.CallbackList
|
||||||
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
|
import me.weishu.kernelsu.ksuApp
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author weishu
|
||||||
|
* @date 2023/1/1.
|
||||||
|
*/
|
||||||
|
private const val TAG = "KsuCli"
|
||||||
|
|
||||||
|
fun execKsud(args: String): Boolean {
|
||||||
|
val shell = ksuApp.createRootShell()
|
||||||
|
val ksduLib = ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libksud.so"
|
||||||
|
return ShellUtils.fastCmdResult(shell, "$ksduLib $args")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleModule(id: String, enable: Boolean): Boolean {
|
||||||
|
val cmd = if (enable) {
|
||||||
|
"module enable $id"
|
||||||
|
} else {
|
||||||
|
"module disable $id"
|
||||||
|
}
|
||||||
|
val result = execKsud(cmd)
|
||||||
|
Log.i(TAG, "toggle module $id result: $result")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uninstallModule(id: String) : Boolean {
|
||||||
|
val cmd = "module uninstall $id"
|
||||||
|
val result = execKsud(cmd)
|
||||||
|
Log.i(TAG, "uninstall module $id result: $result")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun installModule(uri: Uri, onOutput: (String) -> Unit) : Boolean {
|
||||||
|
val resolver = ksuApp.contentResolver
|
||||||
|
with(resolver.openInputStream(uri)) {
|
||||||
|
val file = File(ksuApp.cacheDir, "module.zip")
|
||||||
|
file.outputStream().use { output ->
|
||||||
|
this?.copyTo(output)
|
||||||
|
}
|
||||||
|
val cmd = "module install ${file.absolutePath}"
|
||||||
|
|
||||||
|
val shell = ksuApp.createRootShell()
|
||||||
|
val ksduLib = ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libksud.so"
|
||||||
|
|
||||||
|
val callbackList: CallbackList<String?> = object : CallbackList<String?>() {
|
||||||
|
override fun onAddElement(s: String?) {
|
||||||
|
onOutput(s ?: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = shell.newJob().add("$ksduLib $cmd").to(callbackList, callbackList).exec()
|
||||||
|
Log.i("KernelSU", "install module $uri result: $result")
|
||||||
|
|
||||||
|
file.delete()
|
||||||
|
|
||||||
|
return result.isSuccess
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,45 +77,4 @@ class ModuleViewModel : ViewModel() {
|
|||||||
Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}, modules: $modules")
|
Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}, modules: $modules")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun execKsud(args: String): Boolean {
|
|
||||||
val shell = ksuApp.createRootShell()
|
|
||||||
val ksduLib = ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libksud.so"
|
|
||||||
return ShellUtils.fastCmdResult(shell, "$ksduLib $args")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleModule(id: String, enable: Boolean): Boolean {
|
|
||||||
val cmd = if (enable) {
|
|
||||||
"module enable $id"
|
|
||||||
} else {
|
|
||||||
"module disable $id"
|
|
||||||
}
|
|
||||||
val result = execKsud(cmd)
|
|
||||||
Log.i(TAG, "toggle module $id result: $result")
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
fun uninstallModule(id: String) : Boolean {
|
|
||||||
val cmd = "module uninstall $id"
|
|
||||||
val result = execKsud(cmd)
|
|
||||||
Log.i(TAG, "uninstall module $id result: $result")
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
fun installModule(uri: Uri) : Boolean {
|
|
||||||
val resolver = ksuApp.contentResolver
|
|
||||||
with(resolver.openInputStream(uri)) {
|
|
||||||
val file = File(ksuApp.cacheDir, "module.zip")
|
|
||||||
file.outputStream().use { output ->
|
|
||||||
this?.copyTo(output)
|
|
||||||
}
|
|
||||||
val cmd = "module install ${file.absolutePath}"
|
|
||||||
val result = execKsud(cmd)
|
|
||||||
Log.i(TAG, "install module $uri result: $result")
|
|
||||||
|
|
||||||
file.delete()
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,4 +20,5 @@
|
|||||||
<string name="module">Module</string>
|
<string name="module">Module</string>
|
||||||
<string name="uninstall">Uninstall</string>
|
<string name="uninstall">Uninstall</string>
|
||||||
<string name="module_install">Install</string>
|
<string name="module_install">Install</string>
|
||||||
|
<string name="install">Install</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user