Merge pull request #13 from ShirkNeko/dev

Dev
This commit is contained in:
ShirkNeko
2025-04-01 16:18:17 +08:00
committed by GitHub
6 changed files with 201 additions and 115 deletions

View File

@@ -549,8 +549,18 @@ private fun InfoCard() {
if (!isSimpleMode) { if (!isSimpleMode) {
Spacer(Modifier.height(16.dp)) var showKpmVersion by remember { mutableStateOf(true) }
InfoCardItem(stringResource(R.string.home_kpm_version), getKpmVersion()) LaunchedEffect(Unit) {
try {
getKpmVersion()
} catch (e: Exception) {
showKpmVersion = false
}
}
AnimatedVisibility(visible = showKpmVersion) {
Spacer(Modifier.height(16.dp))
InfoCardItem(stringResource(R.string.home_kpm_version), getKpmVersion())
}
} }

View File

@@ -1,18 +1,17 @@
package shirkneko.zako.sukisu.ui.screen
import android.app.Activity.RESULT_OK import android.app.Activity.RESULT_OK
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.SpringSpec
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
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.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -24,6 +23,7 @@ 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.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import shirkneko.zako.sukisu.R import shirkneko.zako.sukisu.R
import shirkneko.zako.sukisu.ui.component.ConfirmResult import shirkneko.zako.sukisu.ui.component.ConfirmResult
@@ -39,7 +39,6 @@ import java.io.File
import androidx.core.content.edit import androidx.core.content.edit
import shirkneko.zako.sukisu.ui.theme.ThemeConfig import shirkneko.zako.sukisu.ui.theme.ThemeConfig
/** /**
* KPM 管理界面 * KPM 管理界面
* 以下内核模块功能由KernelPatch开发经过修改后加入SukiSU Ultra的内核模块功能 * 以下内核模块功能由KernelPatch开发经过修改后加入SukiSU Ultra的内核模块功能
@@ -103,27 +102,24 @@ fun KpmScreen(
if (confirmResult == ConfirmResult.Confirmed) { if (confirmResult == ConfirmResult.Confirmed) {
val success = loadingDialog.withLoading { val success = loadingDialog.withLoading {
try { try {
val process = ProcessBuilder("nsenter", "-t", "1", "-m").start()
process.waitFor()
loadKpmModule(tempFile.absolutePath) loadKpmModule(tempFile.absolutePath)
true
} catch (e: Exception) { } catch (e: Exception) {
Log.e("KsuCli", "Failed to execute nsenter command: ${e.message}") Log.e("KsuCli", "Failed to load KPM module: ${e.message}")
"failed" false
} }
} }
Log.d("KsuCli", "loadKpmModule result: $success") if (success) {
if (success.contains("Success", ignoreCase = true)) {
viewModel.fetchModuleList() viewModel.fetchModuleList()
snackBarHost.showSnackbar( snackBarHost.showSnackbar(
message = kpmInstallSuccess, message = kpmInstallSuccess,
duration = SnackbarDuration.Long duration = SnackbarDuration.Short
) )
} else { } else {
snackBarHost.showSnackbar( snackBarHost.showSnackbar(
message = kpmInstallFailed, message = kpmInstallFailed,
duration = SnackbarDuration.Long duration = SnackbarDuration.Short
) )
} }
} }
@@ -132,11 +128,12 @@ fun KpmScreen(
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (viewModel.moduleList.isEmpty()) { while(true) {
viewModel.fetchModuleList() viewModel.fetchModuleList()
delay(5000)
} }
} }
// 使用 SharedPreferences 存储声明是否关闭的状态
val sharedPreferences = context.getSharedPreferences("app_preferences", Context.MODE_PRIVATE) val sharedPreferences = context.getSharedPreferences("app_preferences", Context.MODE_PRIVATE)
var isNoticeClosed by remember { mutableStateOf(sharedPreferences.getBoolean("is_notice_closed", false)) } var isNoticeClosed by remember { mutableStateOf(sharedPreferences.getBoolean("is_notice_closed", false)) }
@@ -196,7 +193,7 @@ fun KpmScreen(
) )
IconButton(onClick = { IconButton(onClick = {
isNoticeClosed = true isNoticeClosed = true
sharedPreferences.edit() { putBoolean("is_notice_closed", true) } sharedPreferences.edit { putBoolean("is_notice_closed", true) }
}) { }) {
Icon( Icon(
imageVector = Icons.Outlined.Close, imageVector = Icons.Outlined.Close,
@@ -206,64 +203,63 @@ fun KpmScreen(
} }
} }
PullToRefreshBox( if (viewModel.moduleList.isEmpty()) {
onRefresh = { viewModel.fetchModuleList() }, Box(
isRefreshing = viewModel.isRefreshing, modifier = Modifier.fillMaxSize(),
modifier = Modifier contentAlignment = Alignment.Center
) { ) {
if (viewModel.moduleList.isEmpty()) { Text(
Box( stringResource(R.string.kpm_empty),
modifier = Modifier.fillMaxSize(), textAlign = TextAlign.Center
contentAlignment = Alignment.Center )
) { }
Text( } else {
stringResource(R.string.kpm_empty), LazyColumn(
textAlign = TextAlign.Center modifier = Modifier.fillMaxSize(),
) contentPadding = PaddingValues(16.dp),
} verticalArrangement = Arrangement.spacedBy(16.dp)
} else { ) {
LazyColumn( items(viewModel.moduleList) { module ->
modifier = Modifier.fillMaxSize(), val kpmUninstallConfirm = String.format(kpmUninstallConfirmTemplate, module.name)
contentPadding = PaddingValues(16.dp), KpmModuleItem(
verticalArrangement = Arrangement.spacedBy(16.dp) module = module,
) { onUninstall = {
items(viewModel.moduleList) { module -> scope.launch {
val kpmUninstallConfirm = String.format(kpmUninstallConfirmTemplate, module.name) val confirmResult = confirmDialog.awaitConfirm(
KpmModuleItem( title = kpmUninstall,
module = module, content = kpmUninstallConfirm,
onUninstall = { confirm = uninstall,
scope.launch { dismiss = cancel
val confirmResult = confirmDialog.awaitConfirm( )
title = kpmUninstall, if (confirmResult == ConfirmResult.Confirmed) {
content = kpmUninstallConfirm, val success = loadingDialog.withLoading {
confirm = uninstall, try {
dismiss = cancel
)
if (confirmResult == ConfirmResult.Confirmed) {
val success = loadingDialog.withLoading {
unloadKpmModule(module.id) unloadKpmModule(module.id)
} true
Log.d("KsuCli", "unloadKpmModule result: $success") } catch (e: Exception) {
if (success.contains("Success", ignoreCase = true)) { Log.e("KsuCli", "Failed to unload KPM module: ${e.message}")
viewModel.fetchModuleList() false
snackBarHost.showSnackbar(
message = kpmUninstallSuccess,
duration = SnackbarDuration.Long
)
} else {
snackBarHost.showSnackbar(
message = kpmUninstallFailed,
duration = SnackbarDuration.Long
)
} }
} }
if (success) {
viewModel.fetchModuleList()
snackBarHost.showSnackbar(
message = kpmUninstallSuccess,
duration = SnackbarDuration.Short
)
} else {
snackBarHost.showSnackbar(
message = kpmUninstallFailed,
duration = SnackbarDuration.Short
)
}
} }
},
onControl = {
viewModel.loadModuleDetail(module.id)
} }
) },
} onControl = {
viewModel.loadModuleDetail(module.id)
}
)
} }
} }
} }
@@ -277,6 +273,13 @@ private fun KpmModuleItem(
onUninstall: () -> Unit, onUninstall: () -> Unit,
onControl: () -> Unit onControl: () -> Unit
) { ) {
val viewModel: KpmViewModel = viewModel()
val scope = rememberCoroutineScope()
val snackBarHost = remember { SnackbarHostState() }
val successMessage = stringResource(R.string.kpm_control_success)
val failureMessage = stringResource(R.string.kpm_control_failed)
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer), colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation()) elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
@@ -323,7 +326,18 @@ private fun KpmModuleItem(
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
FilledTonalButton( FilledTonalButton(
onClick = onControl onClick = {
scope.launch {
val result = viewModel.controlModule(module.id, module.args)
val message = when (result) {
0 -> successMessage
else -> failureMessage
}
snackBarHost.showSnackbar(message)
onControl()
}
},
enabled = module.hasAction
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.Settings, imageVector = Icons.Outlined.Settings,

View File

@@ -490,14 +490,14 @@ fun loadKpmModule(path: String, args: String? = null): Boolean {
val shell = getRootShell() val shell = getRootShell()
val cmd = "${getKpmmgrPath()} load $path ${args ?: ""}" val cmd = "${getKpmmgrPath()} load $path ${args ?: ""}"
val result = ShellUtils.fastCmd(shell, cmd) val result = ShellUtils.fastCmd(shell, cmd)
return result.contains("Success") return result.contains("Success", ignoreCase = true)
} }
fun unloadKpmModule(name: String): Boolean { fun unloadKpmModule(name: String): Boolean {
val shell = getRootShell() val shell = getRootShell()
val cmd = "${getKpmmgrPath()} unload $name" val cmd = "${getKpmmgrPath()} unload $name"
val result = ShellUtils.fastCmd(shell, cmd) val result = ShellUtils.fastCmd(shell, cmd)
return result.trim().toIntOrNull() == 0 return result.trim().isEmpty() || result.trim() == "0"
} }
fun getKpmModuleCount(): Int { fun getKpmModuleCount(): Int {
@@ -507,24 +507,40 @@ fun getKpmModuleCount(): Int {
return result.trim().toIntOrNull() ?: 0 return result.trim().toIntOrNull() ?: 0
} }
fun runCmd(shell : Shell, cmd : String) : String {
return shell.newJob()
.add(cmd)
.to(mutableListOf<String>(), null)
.exec().out
.joinToString("\n")
}
fun listKpmModules(): String { fun listKpmModules(): String {
val shell = getRootShell() val shell = getRootShell()
val cmd = "${getKpmmgrPath()} list" val cmd = "${getKpmmgrPath()} list"
val result = ShellUtils.fastCmd(shell, cmd) return try {
return result.trim() runCmd(shell, cmd).trim()
} catch (e: Exception) {
Log.e(TAG, "Failed to list KPM modules", e)
""
}
} }
fun getKpmModuleInfo(name: String): String { fun getKpmModuleInfo(name: String): String {
val shell = getRootShell() val shell = getRootShell()
val cmd = "${getKpmmgrPath()} info $name" val cmd = "${getKpmmgrPath()} info $name"
val result = ShellUtils.fastCmd(shell, cmd) return try {
return result.trim() runCmd(shell, cmd).trim()
} catch (e: Exception) {
Log.e(TAG, "Failed to get KPM module info: $name", e)
""
}
} }
fun controlKpmModule(name: String, args: String? = null): Int { fun controlKpmModule(name: String, args: String? = null): Int {
val shell = getRootShell() val shell = getRootShell()
val cmd = "${getKpmmgrPath()} control $name ${args ?: ""}" val cmd = "${getKpmmgrPath()} control $name ${args ?: ""}"
val result = ShellUtils.fastCmd(shell, cmd) val result = runCmd(shell, cmd)
return result.trim().toIntOrNull() ?: -1 return result.trim().toIntOrNull() ?: -1
} }

View File

@@ -15,7 +15,6 @@ class KpmViewModel : ViewModel() {
var moduleList by mutableStateOf(emptyList<ModuleInfo>()) var moduleList by mutableStateOf(emptyList<ModuleInfo>())
private set private set
var search by mutableStateOf("") var search by mutableStateOf("")
internal set internal set
@@ -25,19 +24,6 @@ class KpmViewModel : ViewModel() {
var currentModuleDetail by mutableStateOf("") var currentModuleDetail by mutableStateOf("")
private set 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() { fun fetchModuleList() {
viewModelScope.launch { viewModelScope.launch {
isRefreshing = true isRefreshing = true
@@ -45,40 +31,96 @@ class KpmViewModel : ViewModel() {
val moduleCount = getKpmModuleCount() val moduleCount = getKpmModuleCount()
Log.d("KsuCli", "Module count: $moduleCount") Log.d("KsuCli", "Module count: $moduleCount")
val moduleInfo = listKpmModules() moduleList = getAllKpmModuleInfo()
Log.d("KsuCli", "Module info: $moduleInfo")
val modules = parseModuleList(moduleInfo)
moduleList = modules
// 获取 KPM 版本信息 // 获取 KPM 版本信息
val kpmVersion = getKpmVersion() val kpmVersion = getKpmVersion()
Log.d("KsuCli", "KPM Version: $kpmVersion") Log.d("KsuCli", "KPM Version: $kpmVersion")
} catch (e: Exception) {
Log.e("KsuCli", "获取模块列表失败", e)
} finally { } finally {
isRefreshing = false isRefreshing = false
} }
} }
} }
private fun parseModuleList(output: String): List<ModuleInfo> { private fun getAllKpmModuleInfo(): List<ModuleInfo> {
return output.split("\n").mapNotNull { line -> val result = mutableListOf<ModuleInfo>()
if (line.isBlank()) return@mapNotNull null try {
val parts = line.split("|") val str = listKpmModules()
if (parts.size < 7) return@mapNotNull null val moduleNames = str
.split("\n")
.filter { it.isNotBlank() }
ModuleInfo( for (name in moduleNames) {
id = parts[0].trim(), try {
name = parts[1].trim(), val moduleInfo = parseModuleInfo(name)
version = parts[2].trim(), moduleInfo?.let { result.add(it) }
author = parts[3].trim(), } catch (e: Exception) {
description = parts[4].trim(), Log.e("KsuCli", "Error processing module $name", e)
args = parts[6].trim(), }
enabled = true, }
hasAction = controlKpmModule(parts[0].trim()).isNotBlank() } catch (e: Exception) {
) Log.e("KsuCli", "Failed to get module list", e)
} }
return result
} }
private fun parseModuleInfo(name: String): ModuleInfo? {
val info = getKpmModuleInfo(name)
if (info.isBlank()) return null
val properties = info.lineSequence()
.filter { line ->
val trimmed = line.trim()
trimmed.isNotEmpty() && !trimmed.startsWith("#")
}
.mapNotNull { line ->
line.split("=", limit = 2).let { parts ->
when (parts.size) {
2 -> parts[0].trim() to parts[1].trim()
1 -> parts[0].trim() to ""
else -> null
}
}
}
.toMap()
return ModuleInfo(
id = name,
name = properties["name"] ?: name,
version = properties["version"] ?: "",
author = properties["author"] ?: "",
description = properties["description"] ?: "",
args = properties["args"] ?: "",
enabled = true,
hasAction = true
)
}
fun loadModuleDetail(moduleId: String) {
viewModelScope.launch {
try {
currentModuleDetail = withContext(Dispatchers.IO) {
getKpmModuleInfo(moduleId)
}
Log.d("KsuCli", "Module detail loaded: $currentModuleDetail")
} catch (e: Exception) {
Log.e("KsuCli", "Failed to load module detail", e)
currentModuleDetail = "Error: ${e.message}"
}
}
}
fun controlModule(moduleId: String, args: String? = null): Int {
return try {
val result = controlKpmModule(moduleId, args)
Log.d("KsuCli", "Control module $moduleId result: $result")
result
} catch (e: Exception) {
Log.e("KsuCli", "Failed to control module $moduleId", e)
-1
}
}
data class ModuleInfo( data class ModuleInfo(
val id: String, val id: String,

View File

@@ -236,6 +236,8 @@
<string name="kpm_install_failed">加载kpm模块失败</string> <string name="kpm_install_failed">加载kpm模块失败</string>
<string name="home_kpm_version">KPM 版本</string> <string name="home_kpm_version">KPM 版本</string>
<string name="close_notice">关闭</string> <string name="close_notice">关闭</string>
<string name="kpm_control_success">成功</string>
<string name="kpm_control_failed">错误</string>
<string name="kernel_module_notice">以下内核模块功能由KernelPatch开发经过修改后加入SukiSU Ultra的内核模块功能</string> <string name="kernel_module_notice">以下内核模块功能由KernelPatch开发经过修改后加入SukiSU Ultra的内核模块功能</string>
<string name="home_ContributionCard_kernelsu">SukiSU Ultra展望</string> <string name="home_ContributionCard_kernelsu">SukiSU Ultra展望</string>
<string name="home_click_to_ContributionCard_kernelsu">SukiSU Ultra未来将会成为一个相对独立的KSU分支但是依然感谢官方KernelSU和MKSU等做出的贡献</string> <string name="home_click_to_ContributionCard_kernelsu">SukiSU Ultra未来将会成为一个相对独立的KSU分支但是依然感谢官方KernelSU和MKSU等做出的贡献</string>

View File

@@ -237,10 +237,12 @@
<string name="kpm_install_success">Load of kpm module successful</string> <string name="kpm_install_success">Load of kpm module successful</string>
<string name="kpm_install_failed">Load of kpm module failed</string> <string name="kpm_install_failed">Load of kpm module failed</string>
<string name="kpm_args">kpm parameters</string> <string name="kpm_args">kpm parameters</string>
<string name="kpm_control">kpm control</string> <string name="kpm_control">fulfillment</string>
<string name="home_kpm_version">KPM Version</string> <string name="home_kpm_version">KPM Version</string>
<string name="close_notice">close</string> <string name="close_notice">close</string>
<string name="kernel_module_notice">The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra</string> <string name="kernel_module_notice">The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra</string>
<string name="home_ContributionCard_kernelsu">SukiSU Ultra Look forward to</string> <string name="home_ContributionCard_kernelsu">SukiSU Ultra Look forward to</string>
<string name="kpm_control_success">success</string>
<string name="kpm_control_failed">failed</string>
<string name="home_click_to_ContributionCard_kernelsu">SukiSU Ultra will be a relatively independent branch of KSU in the future, but thanks to the official KernelSU and MKSU etc. for their contributions!</string> <string name="home_click_to_ContributionCard_kernelsu">SukiSU Ultra will be a relatively independent branch of KSU in the future, but thanks to the official KernelSU and MKSU etc. for their contributions!</string>
</resources> </resources>