Add KPM module support, update related strings and view models
This commit is contained in:
@@ -9,6 +9,7 @@ import com.ramcosta.composedestinations.generated.destinations.HomeScreenDestina
|
||||
import com.ramcosta.composedestinations.generated.destinations.ModuleScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.SuperUserScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.SettingScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.KpmScreenDestination
|
||||
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
|
||||
import shirkneko.zako.sukisu.R
|
||||
|
||||
@@ -22,5 +23,6 @@ enum class BottomBarDestination(
|
||||
Home(HomeScreenDestination, R.string.home, Icons.Filled.Home, Icons.Outlined.Home, false),
|
||||
SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.Security, Icons.Outlined.Security, true),
|
||||
Module(ModuleScreenDestination, R.string.module, Icons.Filled.Apps, Icons.Outlined.Apps, true),
|
||||
Kpm(KpmScreenDestination, R.string.kpm_title, Icons.Filled.Build, Icons.Outlined.Build, true),
|
||||
Settings(SettingScreenDestination, R.string.settings, Icons.Filled.Settings, Icons.Outlined.Settings, false),
|
||||
}
|
||||
|
||||
297
manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/kpm.kt
Normal file
297
manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/kpm.kt
Normal file
@@ -0,0 +1,297 @@
|
||||
package shirkneko.zako.sukisu.ui.screen
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.launch
|
||||
import shirkneko.zako.sukisu.R
|
||||
import shirkneko.zako.sukisu.ui.component.ConfirmResult
|
||||
import shirkneko.zako.sukisu.ui.component.SearchAppBar
|
||||
import shirkneko.zako.sukisu.ui.component.rememberConfirmDialog
|
||||
import shirkneko.zako.sukisu.ui.component.rememberLoadingDialog
|
||||
import shirkneko.zako.sukisu.ui.theme.getCardColors
|
||||
import shirkneko.zako.sukisu.ui.theme.getCardElevation
|
||||
import shirkneko.zako.sukisu.ui.viewmodel.KpmViewModel
|
||||
import shirkneko.zako.sukisu.ui.util.loadKpmModule
|
||||
import shirkneko.zako.sukisu.ui.util.unloadKpmModule
|
||||
import java.io.File
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun KpmScreen(
|
||||
navigator: DestinationsNavigator,
|
||||
viewModel: KpmViewModel = viewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackBarHost = remember { SnackbarHostState() }
|
||||
val confirmDialog = rememberConfirmDialog()
|
||||
val loadingDialog = rememberLoadingDialog()
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
val kpmInstall = stringResource(R.string.kpm_install)
|
||||
val kpmInstallConfirm = stringResource(R.string.kpm_install_confirm)
|
||||
val kpmInstallSuccess = stringResource(R.string.kpm_install_success)
|
||||
val kpmInstallFailed = stringResource(R.string.kpm_install_failed)
|
||||
val install = stringResource(R.string.install)
|
||||
val cancel = stringResource(R.string.cancel)
|
||||
val kpmUninstall = stringResource(R.string.kpm_uninstall)
|
||||
val kpmUninstallConfirmTemplate = stringResource(R.string.kpm_uninstall_confirm)
|
||||
val uninstall = stringResource(R.string.uninstall)
|
||||
val kpmUninstallSuccess = stringResource(R.string.kpm_uninstall_success)
|
||||
val kpmUninstallFailed = stringResource(R.string.kpm_uninstall_failed)
|
||||
|
||||
val selectPatchLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode != RESULT_OK) return@rememberLauncherForActivityResult
|
||||
|
||||
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
|
||||
|
||||
scope.launch {
|
||||
// 复制文件到临时目录
|
||||
val tempFile = File(context.cacheDir, "temp_patch.kpm")
|
||||
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||
tempFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
val confirmResult = confirmDialog.awaitConfirm(
|
||||
title = kpmInstall,
|
||||
content = kpmInstallConfirm,
|
||||
confirm = install,
|
||||
dismiss = cancel
|
||||
)
|
||||
|
||||
if (confirmResult == ConfirmResult.Confirmed) {
|
||||
val success = loadingDialog.withLoading {
|
||||
loadKpmModule(tempFile.absolutePath)
|
||||
}
|
||||
|
||||
Log.d("KsuCli", "loadKpmModule result: $success")
|
||||
|
||||
if (success == "success") {
|
||||
viewModel.fetchModuleList()
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmInstallSuccess,
|
||||
duration = SnackbarDuration.Long
|
||||
)
|
||||
} else {
|
||||
// 修正为显示安装失败的消息
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmInstallFailed,
|
||||
duration = SnackbarDuration.Long
|
||||
)
|
||||
}
|
||||
}
|
||||
tempFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (viewModel.moduleList.isEmpty()) {
|
||||
viewModel.fetchModuleList()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SearchAppBar(
|
||||
title = { Text(stringResource(R.string.kpm_title)) },
|
||||
searchText = viewModel.search,
|
||||
onSearchTextChange = { viewModel.search = it },
|
||||
onClearClick = { viewModel.search = "" },
|
||||
scrollBehavior = scrollBehavior,
|
||||
dropdownContent = {
|
||||
IconButton(onClick = { viewModel.fetchModuleList() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Refresh,
|
||||
contentDescription = stringResource(R.string.refresh)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
selectPatchLauncher.launch(
|
||||
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "application/*"
|
||||
}
|
||||
)
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Add,
|
||||
contentDescription = stringResource(R.string.kpm_install)
|
||||
)
|
||||
},
|
||||
text = { Text(stringResource(R.string.kpm_install)) },
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackBarHost) }
|
||||
) { padding ->
|
||||
PullToRefreshBox(
|
||||
onRefresh = { viewModel.fetchModuleList() },
|
||||
isRefreshing = viewModel.isRefreshing,
|
||||
modifier = Modifier.padding(padding)
|
||||
) {
|
||||
if (viewModel.moduleList.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.kpm_empty),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
items(viewModel.moduleList) { module ->
|
||||
val kpmUninstallConfirm = String.format(kpmUninstallConfirmTemplate, module.name)
|
||||
KpmModuleItem(
|
||||
module = module,
|
||||
onUninstall = {
|
||||
scope.launch {
|
||||
val confirmResult = confirmDialog.awaitConfirm(
|
||||
title = kpmUninstall,
|
||||
content = kpmUninstallConfirm,
|
||||
confirm = uninstall,
|
||||
dismiss = cancel
|
||||
)
|
||||
if (confirmResult == ConfirmResult.Confirmed) {
|
||||
val success = loadingDialog.withLoading {
|
||||
unloadKpmModule(module.id)
|
||||
}
|
||||
Log.d("KsuCli", "unloadKpmModule result: $success")
|
||||
if (success == "success") {
|
||||
viewModel.fetchModuleList()
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmUninstallSuccess,
|
||||
duration = SnackbarDuration.Long
|
||||
)
|
||||
} else {
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmUninstallFailed,
|
||||
duration = SnackbarDuration.Long
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onControl = {
|
||||
viewModel.loadModuleDetail(module.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KpmModuleItem(
|
||||
module: KpmViewModel.ModuleInfo,
|
||||
onUninstall: () -> Unit,
|
||||
onControl: () -> Unit
|
||||
) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = module.name,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Text(
|
||||
text = "${stringResource(R.string.kpm_version)}: ${module.version}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
text = "${stringResource(R.string.kpm_author)}: ${module.author}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
text = "${stringResource(R.string.kpm_args)}: ${module.args}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = module.description,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
FilledTonalButton(
|
||||
onClick = onControl
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = null
|
||||
)
|
||||
Text(stringResource(R.string.kpm_control))
|
||||
}
|
||||
|
||||
FilledTonalButton(
|
||||
onClick = onUninstall
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Delete,
|
||||
contentDescription = null
|
||||
)
|
||||
Text(stringResource(R.string.kpm_uninstall))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -480,3 +480,57 @@ fun susfsSUS_SU_Mode(): String {
|
||||
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} sus_su mode")
|
||||
return result
|
||||
}
|
||||
|
||||
private fun getKpmmgrPath(): String {
|
||||
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libkpmmgr.so"
|
||||
}
|
||||
|
||||
|
||||
fun loadKpmModule(path: String, args: String? = null): String {
|
||||
val shell = getRootShell()
|
||||
val cmd = "${getKpmmgrPath()} load $path ${args ?: ""}"
|
||||
val result = ShellUtils.fastCmd(shell, cmd)
|
||||
return result
|
||||
}
|
||||
|
||||
fun unloadKpmModule(name: String): String {
|
||||
val shell = getRootShell()
|
||||
val cmd = "${getKpmmgrPath()} unload $name"
|
||||
val result = ShellUtils.fastCmd(shell, cmd)
|
||||
return result
|
||||
}
|
||||
|
||||
fun getKpmModuleCount(): String {
|
||||
val shell = getRootShell()
|
||||
val cmd = "${getKpmmgrPath()} num"
|
||||
val result = ShellUtils.fastCmd(shell, cmd)
|
||||
return result
|
||||
}
|
||||
|
||||
fun listKpmModules(): String {
|
||||
val shell = getRootShell()
|
||||
val cmd = "${getKpmmgrPath()} list"
|
||||
val result = ShellUtils.fastCmd(shell, cmd)
|
||||
return result
|
||||
}
|
||||
|
||||
fun getKpmModuleInfo(name: String): String {
|
||||
val shell = getRootShell()
|
||||
val cmd = "${getKpmmgrPath()} info $name"
|
||||
val result = ShellUtils.fastCmd(shell, cmd)
|
||||
return result
|
||||
}
|
||||
|
||||
fun controlKpmModule(name: String, args: String? = null): String {
|
||||
val shell = getRootShell()
|
||||
val cmd = "${getKpmmgrPath()} control $name ${args ?: ""}"
|
||||
val result = ShellUtils.fastCmd(shell, cmd)
|
||||
return result
|
||||
}
|
||||
|
||||
fun printKpmModules(): String {
|
||||
val shell = getRootShell()
|
||||
val cmd = "${getKpmmgrPath()} print"
|
||||
val result = ShellUtils.fastCmd(shell, cmd)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package shirkneko.zako.sukisu.ui.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import shirkneko.zako.sukisu.ui.util.*
|
||||
|
||||
class KpmViewModel : ViewModel() {
|
||||
var moduleList by mutableStateOf(emptyList<ModuleInfo>())
|
||||
private set
|
||||
|
||||
|
||||
var search by mutableStateOf("")
|
||||
internal set
|
||||
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
var currentModuleDetail by mutableStateOf("")
|
||||
private set
|
||||
|
||||
fun loadModuleDetail(moduleId: String) {
|
||||
viewModelScope.launch {
|
||||
currentModuleDetail = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
getKpmModuleInfo(moduleId)
|
||||
} catch (e: Exception) {
|
||||
"无法获取模块详细信息: ${e.message}"
|
||||
}
|
||||
}
|
||||
Log.d("KsuCli", "Module detail: $currentModuleDetail")
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchModuleList() {
|
||||
viewModelScope.launch {
|
||||
isRefreshing = true
|
||||
try {
|
||||
val moduleCount = getKpmModuleCount()
|
||||
Log.d("KsuCli", "Module count: $moduleCount")
|
||||
|
||||
val moduleInfo = listKpmModules()
|
||||
Log.d("KsuCli", "Module info: $moduleInfo")
|
||||
|
||||
val modules = parseModuleList(moduleInfo)
|
||||
moduleList = modules
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInstalledKernelPatches(): List<ModuleInfo> {
|
||||
return try {
|
||||
val output = printKpmModules()
|
||||
parseModuleList(output)
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseModuleList(output: String): List<ModuleInfo> {
|
||||
return output.split("\n").mapNotNull { line ->
|
||||
if (line.isBlank()) return@mapNotNull null
|
||||
val parts = line.split("|")
|
||||
if (parts.size < 7) return@mapNotNull null
|
||||
|
||||
ModuleInfo(
|
||||
id = parts[0].trim(),
|
||||
name = parts[1].trim(),
|
||||
version = parts[2].trim(),
|
||||
author = parts[3].trim(),
|
||||
description = parts[4].trim(),
|
||||
args = parts[6].trim(),
|
||||
enabled = true,
|
||||
hasAction = controlKpmModule(parts[0].trim()).isNotBlank()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class ModuleInfo(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val version: String,
|
||||
val author: String,
|
||||
val description: String,
|
||||
val args: String,
|
||||
val enabled: Boolean,
|
||||
val hasAction: Boolean
|
||||
)
|
||||
}
|
||||
@@ -236,4 +236,6 @@
|
||||
<string name="kpm_install_confirm">Confirm installation?</string>
|
||||
<string name="kpm_install_success">Installation of kpm module successful</string>
|
||||
<string name="kpm_install_failed">Installation of kpm module failed</string>
|
||||
<string name="kpm_args">kpm 参数</string>
|
||||
<string name="kpm_control">kpm 控制</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user