Step 5-1: Move the KPM interface to the settings

- Avoid multiple page re-rendering
- Add hook type information
- Clean up code
This commit is contained in:
ShirkNeko
2025-11-20 18:38:53 +08:00
parent 6826406494
commit 385f4ab2c5
12 changed files with 139 additions and 125 deletions

View File

@@ -335,11 +335,6 @@ NativeBridge(getUserName, jstring, jint uid) {
return NULL;
}
// Check if KPM is enabled
NativeBridgeNP(isKPMEnabled, jboolean) {
return is_KPM_enable();
}
// Get HOOK type
NativeBridgeNP(getHookType, jstring) {
char hook_type[32] = { 0 };

View File

@@ -259,14 +259,6 @@ void get_full_version(char* buff) {
}
}
bool is_KPM_enable(void) {
struct ksu_enable_kpm_cmd cmd = {};
if (ksuctl(KSU_IOCTL_ENABLE_KPM, &cmd) == 0 && cmd.enabled) {
return true;
}
return legacy_is_KPM_enable();
}
void get_hook_type(char *buff) {
struct ksu_hook_type_cmd cmd = {0};
if (ksuctl(KSU_IOCTL_HOOK_TYPE, &cmd) == 0) {

View File

@@ -229,10 +229,6 @@ struct ksu_hook_type_cmd {
char hook_type[32]; // Output: hook type string
};
struct ksu_enable_kpm_cmd {
uint8_t enabled; // Output: true if KPM is enabled
};
struct ksu_dynamic_manager_cmd {
struct dynamic_manager_user_config config; // Input/Output: dynamic manager config
};

View File

@@ -88,12 +88,6 @@ bool legacy_is_su_enabled() {
return enabled;
}
bool legacy_is_KPM_enable() {
int enabled = false;
ksuctl(CMD_ENABLE_KPM, &enabled, NULL);
return enabled;
}
bool legacy_get_hook_type(char* hook_type, size_t size) {
if (hook_type == NULL || size == 0) {
return false;

View File

@@ -121,8 +121,6 @@ object Natives {
*/
external fun isSuLogEnabled(): Boolean
external fun setSuLogEnabled(enabled: Boolean): Boolean
external fun isKPMEnabled(): Boolean
external fun getHookType(): String
/**

View File

@@ -1,7 +1,6 @@
package com.sukisu.ultra.ui
import android.content.SharedPreferences
import androidx.compose.ui.graphics.Color
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
@@ -31,6 +30,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.navigation.NavBackStackEntry
import androidx.navigation.compose.rememberNavController
import com.ramcosta.composedestinations.DestinationsNavHost
@@ -47,13 +47,11 @@ import kotlinx.coroutines.launch
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ui.component.BottomBar
import com.sukisu.ultra.ui.screen.HomePager
import com.sukisu.ultra.ui.screen.KpmScreen
import com.sukisu.ultra.ui.screen.ModulePager
import com.sukisu.ultra.ui.screen.SettingPager
import com.sukisu.ultra.ui.screen.SuperUserPager
import com.sukisu.ultra.ui.theme.KernelSUTheme
import com.sukisu.ultra.ui.util.install
import com.sukisu.ultra.ui.util.rememberKpmAvailable
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.theme.MiuixTheme
@@ -94,6 +92,7 @@ class MainActivity : ComponentActivity() {
android.graphics.Color.TRANSPARENT
) { darkMode },
)
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
when (key) {
"color_mode" -> colorMode = prefs.getInt("color_mode", 0)
@@ -161,11 +160,7 @@ val LocalHandlePageChange = compositionLocalOf<(Int) -> Unit> { error("No handle
fun MainScreen(navController: DestinationsNavigator) {
val activity = LocalActivity.current
val coroutineScope = rememberCoroutineScope()
val isKpmAvailable = rememberKpmAvailable()
val pageCount = if (isKpmAvailable) 5 else 4
val pagerState = rememberPagerState(initialPage = 0, pageCount = { pageCount })
val pagerState = rememberPagerState(initialPage = 0, pageCount = { 4 })
val hazeState = remember { HazeState() }
val hazeStyle = HazeStyle(
backgroundColor = MiuixTheme.colorScheme.background,
@@ -193,7 +188,7 @@ fun MainScreen(navController: DestinationsNavigator) {
) {
Scaffold(
bottomBar = {
BottomBar(hazeState, hazeStyle, isKpmAvailable)
BottomBar(hazeState, hazeStyle)
},
) { innerPadding ->
HorizontalPager(
@@ -202,17 +197,6 @@ fun MainScreen(navController: DestinationsNavigator) {
beyondViewportPageCount = 2,
userScrollEnabled = false
) {
when {
isKpmAvailable -> {
when (it) {
0 -> HomePager(pagerState, navController, innerPadding.calculateBottomPadding())
1 -> KpmScreen(bottomInnerPadding = innerPadding.calculateBottomPadding())
2 -> SuperUserPager(navController, innerPadding.calculateBottomPadding())
3 -> ModulePager(navController, innerPadding.calculateBottomPadding())
4 -> SettingPager(navController, innerPadding.calculateBottomPadding())
}
}
else -> {
when (it) {
0 -> HomePager(pagerState, navController, innerPadding.calculateBottomPadding())
1 -> SuperUserPager(navController, innerPadding.calculateBottomPadding())
@@ -222,6 +206,4 @@ fun MainScreen(navController: DestinationsNavigator) {
}
}
}
}
}
}

View File

@@ -2,7 +2,6 @@ package com.sukisu.ultra.ui.component
import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Code
import androidx.compose.material.icons.rounded.Cottage
import androidx.compose.material.icons.rounded.Extension
import androidx.compose.material.icons.rounded.Security
@@ -28,8 +27,7 @@ import top.yukonga.miuix.kmp.basic.NavigationItem
@Composable
fun BottomBar(
hazeState: HazeState,
hazeStyle: HazeStyle,
isKpmAvailable: Boolean = false
hazeStyle: HazeStyle
) {
val isManager = Natives.isManager
val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()
@@ -39,25 +37,13 @@ fun BottomBar(
if (!fullFeatured) return
val destinations = if (isKpmAvailable) {
BottomBarDestination.entries
} else {
BottomBarDestination.entries.filter { it != BottomBarDestination.KPM }
}
val item = destinations.mapIndexed { index, destination ->
val item = BottomBarDestination.entries.mapIndexed { index, destination ->
NavigationItem(
label = stringResource(destination.label),
icon = destination.icon,
)
}
val bottomBarIndex = if (!isKpmAvailable) {
page.coerceIn(0, item.size - 1)
} else {
page.coerceIn(0, item.size - 1)
}
NavigationBar(
modifier = Modifier
.hazeEffect(hazeState) {
@@ -67,10 +53,8 @@ fun BottomBar(
},
color = Color.Transparent,
items = item,
selected = bottomBarIndex,
onClick = { index ->
handlePageChange(index)
}
selected = page,
onClick = handlePageChange
)
}
@@ -79,7 +63,6 @@ enum class BottomBarDestination(
val icon: ImageVector,
) {
Home(R.string.home, Icons.Rounded.Cottage),
KPM(R.string.kpm_title, Icons.Rounded.Code),
SuperUser(R.string.superuser, Icons.Rounded.Security),
Module(R.string.module, Icons.Rounded.Extension),
Setting(R.string.settings, Icons.Rounded.Settings)

View File

@@ -37,11 +37,14 @@ import androidx.compose.material.icons.rounded.CheckCircleOutline
import androidx.compose.material.icons.rounded.ErrorOutline
import androidx.compose.material.icons.rounded.Link
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -150,8 +153,6 @@ fun HomePager(
if (kernelVersion.isGKI()) Natives.isLkmMode else null
}
val isKpmAvailable = rememberKpmAvailable()
Column(
modifier = Modifier.padding(vertical = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
@@ -179,12 +180,12 @@ fun HomePager(
},
onClickSuperuser = {
coroutineScope.launch {
pagerState.animateScrollToPage(getSuperuserPageIndex(isKpmAvailable))
pagerState.animateScrollToPage(1)
}
},
onclickModule = {
coroutineScope.launch {
pagerState.animateScrollToPage(getModulePageIndex(isKpmAvailable))
pagerState.animateScrollToPage(2)
}
},
themeMode = themeMode
@@ -609,6 +610,19 @@ fun DonateCard() {
private fun InfoCard() {
val manualHookText = stringResource(R.string.manual_hook)
val inlineHookText = stringResource(R.string.inline_hook)
val TracepointHookText = stringResource(R.string.tracepoint_hook)
val unknownHookText = stringResource(R.string.selinux_status_unknown)
val susfsInfo = rememberSusfsInfo(manualHookText, inlineHookText)
val isSusfsSupported = susfsInfo.status == SusfsStatus.Supported
val hookTypeLabel = remember(manualHookText, inlineHookText, TracepointHookText) {
val localized = when (val rawType = Natives.getHookType()) {
"Manual" -> manualHookText
"Tracepoint" -> TracepointHookText
else -> rawType
}
localized.ifBlank { unknownHookText }
}
@Composable
fun InfoText(
title: String,
@@ -632,23 +646,6 @@ private fun InfoCard() {
val context = LocalContext.current
val uname = Os.uname()
val managerVersion = getManagerVersion(context)
val susfsPair by produceState(initialValue = "" to "") {
value = withContext(Dispatchers.IO) {
val rawFeature = getSuSFSFeatures()
val status = if (rawFeature.isNotEmpty() && !rawFeature.startsWith("[-]")) "Supported" else rawFeature
if (status == "Supported") {
val version = getSuSFSVersion()
val hook = when (Natives.getHookType()) {
"Manual" -> "($manualHookText)"
"Inline" -> "($inlineHookText)"
else -> "(${Natives.getHookType()})"
}
status to "$version $hook".trim()
} else {
"" to ""
}
}
}
Card {
Column(
@@ -689,27 +686,26 @@ private fun InfoCard() {
)
}
}
InfoText(
title = stringResource(R.string.home_fingerprint),
content = Build.FINGERPRINT
)
if (susfsPair.first == "Supported" && susfsPair.second.isNotEmpty()) {
InfoText(
title = stringResource(R.string.home_selinux_status),
content = getSELinuxStatus(),
)
if (isSusfsSupported) {
InfoText(
title = stringResource(R.string.home_susfs_version),
content = susfsPair.second,
bottomPadding = 0.dp
content = susfsInfo.detail
)
} else {
InfoText(
title = stringResource(R.string.home_selinux_status),
content = getSELinuxStatus(),
bottomPadding = 0.dp
title = stringResource(R.string.hook_type),
content = hookTypeLabel
)
}
InfoText(
title = stringResource(R.string.home_selinux_status),
content = getSELinuxStatus(),
)
InfoText(
title = stringResource(R.string.home_fingerprint),
content = Build.FINGERPRINT,
bottomPadding = 0.dp
)
}
}
}
@@ -720,10 +716,52 @@ fun getManagerVersion(context: Context): Pair<String, Long> {
return Pair(packageInfo.versionName!!, versionCode)
}
fun getSuperuserPageIndex(isKpmAvailable: Boolean): Int {
return if (isKpmAvailable) 2 else 1
private enum class SusfsStatus {
Idle, Loading, Supported, Unsupported, Error
}
fun getModulePageIndex(isKpmAvailable: Boolean): Int {
return if (isKpmAvailable) 3 else 2
private data class SusfsInfoState(
val status: SusfsStatus = SusfsStatus.Idle,
val detail: String = "",
)
@Composable
private fun rememberSusfsInfo(
manualHookLabel: String,
inlineHookLabel: String,
): SusfsInfoState {
var susfsInfo by remember { mutableStateOf(SusfsInfoState(status = SusfsStatus.Loading)) }
LaunchedEffect(manualHookLabel, inlineHookLabel) {
val info = withContext(Dispatchers.IO) {
runCatching {
val rawFeature = getSuSFSFeatures()
val supported = rawFeature.isNotEmpty() && !rawFeature.startsWith("[-]")
if (supported) {
val version = getSuSFSVersion().trim()
val hookLabel = when (val type = Natives.getHookType()) {
"Manual" -> manualHookLabel
"Inline" -> inlineHookLabel
else -> type
}.takeIf { it.isNotBlank() }?.let { "($it)" }.orEmpty()
SusfsInfoState(
status = SusfsStatus.Supported,
detail = listOf(version, hookLabel).filter { it.isNotBlank() }.joinToString(" ")
)
} else {
SusfsInfoState(
status = SusfsStatus.Unsupported,
detail = rawFeature
)
}
}.getOrElse {
SusfsInfoState(status = SusfsStatus.Error)
}
}
if (susfsInfo != info) {
susfsInfo = info
}
}
return susfsInfo
}

View File

@@ -16,6 +16,7 @@ import androidx.compose.material.icons.rounded.Palette
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Adb
import androidx.compose.material.icons.rounded.BugReport
import androidx.compose.material.icons.rounded.Code
import androidx.compose.material.icons.rounded.ContactPage
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.DeleteForever
@@ -49,6 +50,7 @@ import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.AboutScreenDestination
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
import com.ramcosta.composedestinations.generated.destinations.LogViewerDestination
import com.ramcosta.composedestinations.generated.destinations.KpmScreenDestination
import com.ramcosta.composedestinations.generated.destinations.PersonalizationDestination
import com.ramcosta.composedestinations.generated.destinations.ToolsDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -64,6 +66,7 @@ import com.sukisu.ultra.ui.component.SendLogDialog
import com.sukisu.ultra.ui.component.UninstallDialog
import com.sukisu.ultra.ui.component.rememberLoadingDialog
import com.sukisu.ultra.ui.util.execKsud
import com.sukisu.ultra.ui.util.rememberKpmAvailable
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
@@ -94,6 +97,8 @@ fun SettingPager(
tint = HazeTint(colorScheme.surface.copy(0.8f))
)
val isKpmAvailable = rememberKpmAvailable()
Scaffold(
topBar = {
TopAppBar(
@@ -265,6 +270,33 @@ fun SettingPager(
}
}
if (isKpmAvailable) {
Card(
modifier = Modifier
.padding(top = 12.dp)
.fillMaxWidth(),
) {
val kpmTitle = stringResource(id = R.string.kpm_title)
SuperArrow(
title = kpmTitle,
summary = stringResource(id = R.string.settings_kpm_summary),
leftAction = {
Icon(
Icons.Rounded.Code,
modifier = Modifier.padding(end = 16.dp),
contentDescription = kpmTitle,
tint = colorScheme.onBackground
)
},
onClick = {
navigator.navigate(KpmScreenDestination) {
launchSingleTop = true
}
}
)
}
}
KsuIsValid {
Card(
modifier = Modifier

View File

@@ -12,7 +12,10 @@ import android.system.Os
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
@@ -727,14 +730,13 @@ fun applyUmountConfigToKernel(): Boolean {
// 检查 KPM 版本是否可用
@Composable
fun rememberKpmAvailable(): Boolean {
val kpmVersion by produceState(initialValue = "") {
value = withContext(Dispatchers.IO) {
try {
getKpmVersion()
} catch (_: Exception) {
""
}
var cachedVersion by rememberSaveable { mutableStateOf("") }
val kpmVersion by produceState(initialValue = cachedVersion) {
val result = withContext(Dispatchers.IO) {
runCatching { getKpmVersion() }.getOrElse { "" }
}
cachedVersion = result
value = result
}
return kpmVersion.isNotEmpty() && !kpmVersion.contains("Error", ignoreCase = true)
}

View File

@@ -178,9 +178,8 @@
<string name="color_pink">粉色</string>
<string name="color_brown">棕色</string>
<!-- Customize -->
<string name="hook_type">钩子类型</string>
<string name="home_susfs_version">SuSFS 版本</string>
<string name="manual_hook">Manual Hook</string>
<string name="inline_hook">Inline Hook</string>
<string name="multi_manager_list">活跃管理器</string>
<string name="default_signature">SukiSU</string>
<string name="dynamic_managerature">Dynamic</string>
@@ -320,7 +319,7 @@
<string name="horizon_kernel">AnyKernel3 内核</string>
<string name="horizon_kernel_summary">刷入AnyKernel3格式的内核zip包</string>
<!-- kpm -->
<string name="kpm_title">KPM</string>
<string name="settings_kpm_summary">使用 KPM 管理内核模块</string>
<string name="kpm_empty">当前没有安装内核模块</string>
<string name="kpm_version">版本</string>
<string name="kpm_author">作者</string>

View File

@@ -180,9 +180,11 @@
<string name="color_pink">Pink</string>
<string name="color_brown">Brown</string>
<!-- Customize -->
<string name="home_susfs_version">SuSFS Version</string>
<string name="hook_type">Hook Type</string>
<string name="manual_hook">Manual Hook</string>
<string name="inline_hook">Inline Hook</string>
<string name="tracepoint_hook">Tracepoint Hook</string>
<string name="home_susfs_version">SuSFS Version</string>
<string name="multi_manager_list">Active Manager</string>
<string name="default_signature">SukiSU</string>
<string name="dynamic_managerature">Dynamic</string>
@@ -325,6 +327,7 @@
<string name="horizon_kernel_summary">Flash AnyKernel3 format kernel zip</string>
<!-- kpm -->
<string name="kpm_title">KPM</string>
<string name="settings_kpm_summary">Manage kernel modules with KPM</string>
<string name="kpm_empty">No installed kernel modules at this time</string>
<string name="kpm_version">Version</string>
<string name="kpm_author">Author</string>