diff --git a/manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/Home.kt b/manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/Home.kt index 364c2584..a6fc7fc0 100644 --- a/manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/Home.kt +++ b/manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/Home.kt @@ -549,8 +549,18 @@ private fun InfoCard() { if (!isSimpleMode) { - Spacer(Modifier.height(16.dp)) - InfoCardItem(stringResource(R.string.home_kpm_version), getKpmVersion()) + var showKpmVersion by remember { mutableStateOf(true) } + 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()) + } } diff --git a/manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/kpm.kt b/manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/kpm.kt index 6c7729ba..3d8574da 100644 --- a/manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/kpm.kt +++ b/manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/kpm.kt @@ -1,18 +1,17 @@ -package shirkneko.zako.sukisu.ui.screen - import android.app.Activity.RESULT_OK import android.content.Context import android.content.Intent import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult 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.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 @@ -24,6 +23,7 @@ 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.delay import kotlinx.coroutines.launch import shirkneko.zako.sukisu.R import shirkneko.zako.sukisu.ui.component.ConfirmResult @@ -39,7 +39,6 @@ import java.io.File import androidx.core.content.edit import shirkneko.zako.sukisu.ui.theme.ThemeConfig - /** * KPM 管理界面 * 以下内核模块功能由KernelPatch开发,经过修改后加入SukiSU Ultra的内核模块功能 @@ -103,27 +102,24 @@ fun KpmScreen( if (confirmResult == ConfirmResult.Confirmed) { val success = loadingDialog.withLoading { try { - val process = ProcessBuilder("nsenter", "-t", "1", "-m").start() - process.waitFor() loadKpmModule(tempFile.absolutePath) + true } catch (e: Exception) { - Log.e("KsuCli", "Failed to execute nsenter command: ${e.message}") - "failed" + Log.e("KsuCli", "Failed to load KPM module: ${e.message}") + false } } - Log.d("KsuCli", "loadKpmModule result: $success") - - if (success.contains("Success", ignoreCase = true)) { + if (success) { viewModel.fetchModuleList() snackBarHost.showSnackbar( message = kpmInstallSuccess, - duration = SnackbarDuration.Long + duration = SnackbarDuration.Short ) } else { snackBarHost.showSnackbar( message = kpmInstallFailed, - duration = SnackbarDuration.Long + duration = SnackbarDuration.Short ) } } @@ -132,11 +128,12 @@ fun KpmScreen( } LaunchedEffect(Unit) { - if (viewModel.moduleList.isEmpty()) { + while(true) { viewModel.fetchModuleList() + delay(5000) } } - // 使用 SharedPreferences 存储声明是否关闭的状态 + val sharedPreferences = context.getSharedPreferences("app_preferences", Context.MODE_PRIVATE) var isNoticeClosed by remember { mutableStateOf(sharedPreferences.getBoolean("is_notice_closed", false)) } @@ -196,7 +193,7 @@ fun KpmScreen( ) IconButton(onClick = { isNoticeClosed = true - sharedPreferences.edit() { putBoolean("is_notice_closed", true) } + sharedPreferences.edit { putBoolean("is_notice_closed", true) } }) { Icon( imageVector = Icons.Outlined.Close, @@ -206,64 +203,63 @@ fun KpmScreen( } } - PullToRefreshBox( - onRefresh = { viewModel.fetchModuleList() }, - isRefreshing = viewModel.isRefreshing, - modifier = Modifier - ) { - 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 { + 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 { + try { unloadKpmModule(module.id) - } - Log.d("KsuCli", "unloadKpmModule result: $success") - if (success.contains("Success", ignoreCase = true)) { - viewModel.fetchModuleList() - snackBarHost.showSnackbar( - message = kpmUninstallSuccess, - duration = SnackbarDuration.Long - ) - } else { - snackBarHost.showSnackbar( - message = kpmUninstallFailed, - duration = SnackbarDuration.Long - ) + true + } catch (e: Exception) { + Log.e("KsuCli", "Failed to unload KPM module: ${e.message}") + false } } + 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, 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( colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer), elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation()) @@ -323,7 +326,18 @@ private fun KpmModuleItem( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { 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( imageVector = Icons.Outlined.Settings, diff --git a/manager/app/src/main/java/shirkneko/zako/sukisu/ui/util/KsuCli.kt b/manager/app/src/main/java/shirkneko/zako/sukisu/ui/util/KsuCli.kt index 1b5df7db..9cd8bd29 100644 --- a/manager/app/src/main/java/shirkneko/zako/sukisu/ui/util/KsuCli.kt +++ b/manager/app/src/main/java/shirkneko/zako/sukisu/ui/util/KsuCli.kt @@ -490,14 +490,14 @@ fun loadKpmModule(path: String, args: String? = null): Boolean { val shell = getRootShell() val cmd = "${getKpmmgrPath()} load $path ${args ?: ""}" val result = ShellUtils.fastCmd(shell, cmd) - return result.contains("Success") + return result.contains("Success", ignoreCase = true) } fun unloadKpmModule(name: String): Boolean { val shell = getRootShell() val cmd = "${getKpmmgrPath()} unload $name" val result = ShellUtils.fastCmd(shell, cmd) - return result.trim().toIntOrNull() == 0 + return result.trim().isEmpty() || result.trim() == "0" } fun getKpmModuleCount(): Int { @@ -507,24 +507,40 @@ fun getKpmModuleCount(): Int { return result.trim().toIntOrNull() ?: 0 } +fun runCmd(shell : Shell, cmd : String) : String { + return shell.newJob() + .add(cmd) + .to(mutableListOf(), null) + .exec().out + .joinToString("\n") +} + fun listKpmModules(): String { val shell = getRootShell() val cmd = "${getKpmmgrPath()} list" - val result = ShellUtils.fastCmd(shell, cmd) - return result.trim() + return try { + runCmd(shell, cmd).trim() + } catch (e: Exception) { + Log.e(TAG, "Failed to list KPM modules", e) + "" + } } fun getKpmModuleInfo(name: String): String { val shell = getRootShell() val cmd = "${getKpmmgrPath()} info $name" - val result = ShellUtils.fastCmd(shell, cmd) - return result.trim() + return try { + 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 { val shell = getRootShell() val cmd = "${getKpmmgrPath()} control $name ${args ?: ""}" - val result = ShellUtils.fastCmd(shell, cmd) + val result = runCmd(shell, cmd) return result.trim().toIntOrNull() ?: -1 } diff --git a/manager/app/src/main/java/shirkneko/zako/sukisu/ui/viewmodel/KpmViewModel.kt b/manager/app/src/main/java/shirkneko/zako/sukisu/ui/viewmodel/KpmViewModel.kt index 803c3222..da49c1b2 100644 --- a/manager/app/src/main/java/shirkneko/zako/sukisu/ui/viewmodel/KpmViewModel.kt +++ b/manager/app/src/main/java/shirkneko/zako/sukisu/ui/viewmodel/KpmViewModel.kt @@ -15,7 +15,6 @@ class KpmViewModel : ViewModel() { var moduleList by mutableStateOf(emptyList()) private set - var search by mutableStateOf("") internal set @@ -25,19 +24,6 @@ class KpmViewModel : ViewModel() { 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 @@ -45,40 +31,96 @@ class KpmViewModel : ViewModel() { val moduleCount = getKpmModuleCount() Log.d("KsuCli", "Module count: $moduleCount") - val moduleInfo = listKpmModules() - Log.d("KsuCli", "Module info: $moduleInfo") - - val modules = parseModuleList(moduleInfo) - moduleList = modules + moduleList = getAllKpmModuleInfo() // 获取 KPM 版本信息 val kpmVersion = getKpmVersion() Log.d("KsuCli", "KPM Version: $kpmVersion") + } catch (e: Exception) { + Log.e("KsuCli", "获取模块列表失败", e) } finally { isRefreshing = false } } } - private fun parseModuleList(output: String): List { - return output.split("\n").mapNotNull { line -> - if (line.isBlank()) return@mapNotNull null - val parts = line.split("|") - if (parts.size < 7) return@mapNotNull null + private fun getAllKpmModuleInfo(): List { + val result = mutableListOf() + try { + val str = listKpmModules() + val moduleNames = str + .split("\n") + .filter { it.isNotBlank() } - 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() - ) + for (name in moduleNames) { + try { + val moduleInfo = parseModuleInfo(name) + moduleInfo?.let { result.add(it) } + } catch (e: Exception) { + Log.e("KsuCli", "Error processing module $name", e) + } + } + } 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( val id: String, @@ -90,4 +132,4 @@ class KpmViewModel : ViewModel() { val enabled: Boolean, val hasAction: Boolean ) -} \ No newline at end of file +} 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 08e3f5be..217aae2c 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -236,6 +236,8 @@ 加载kpm模块失败 KPM 版本 关闭 + 成功 + 错误 以下内核模块功能由KernelPatch开发,经过修改后加入SukiSU Ultra的内核模块功能 SukiSU Ultra展望 SukiSU Ultra未来将会成为一个相对独立的KSU分支,但是依然感谢官方KernelSU和MKSU等做出的贡献 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index c24c465c..dbeb0674 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -237,10 +237,12 @@ Load of kpm module successful Load of kpm module failed kpm parameters - kpm control + fulfillment KPM Version close The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra SukiSU Ultra Look forward to + success + failed 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!