Step 5: Add a settings tool page to migrate some settings to it

- Add SELinux status toggle

- Add backup and restore for the allowlist
This commit is contained in:
ShirkNeko
2025-11-20 16:28:37 +08:00
parent c753dd1345
commit 6465e7a874
5 changed files with 621 additions and 216 deletions

View File

@@ -1,14 +1,6 @@
package com.sukisu.ultra.ui.screen package com.sukisu.ultra.ui.screen
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.WindowInsetsSides
@@ -21,11 +13,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.rounded.Palette import androidx.compose.material.icons.rounded.Palette
import androidx.compose.ui.graphics.toArgb
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CleaningServices
import androidx.compose.material.icons.filled.Groups
import androidx.compose.material.icons.filled.Scanner
import androidx.compose.material.icons.rounded.Adb import androidx.compose.material.icons.rounded.Adb
import androidx.compose.material.icons.rounded.BugReport import androidx.compose.material.icons.rounded.BugReport
import androidx.compose.material.icons.rounded.ContactPage import androidx.compose.material.icons.rounded.ContactPage
@@ -35,19 +23,16 @@ import androidx.compose.material.icons.rounded.DeveloperMode
import androidx.compose.material.icons.rounded.EnhancedEncryption import androidx.compose.material.icons.rounded.EnhancedEncryption
import androidx.compose.material.icons.rounded.Fence import androidx.compose.material.icons.rounded.Fence
import androidx.compose.material.icons.rounded.FolderDelete import androidx.compose.material.icons.rounded.FolderDelete
import androidx.compose.material.icons.rounded.Palette
import androidx.compose.material.icons.rounded.RemoveCircle import androidx.compose.material.icons.rounded.RemoveCircle
import androidx.compose.material.icons.rounded.RemoveModerator import androidx.compose.material.icons.rounded.RemoveModerator
import androidx.compose.material.icons.rounded.RestartAlt import androidx.compose.material.icons.rounded.RestartAlt
import androidx.compose.material.icons.rounded.Update import androidx.compose.material.icons.rounded.Update
import androidx.compose.material.icons.rounded.UploadFile import androidx.compose.material.icons.rounded.UploadFile
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -65,7 +50,7 @@ import com.ramcosta.composedestinations.generated.destinations.AboutScreenDestin
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
import com.ramcosta.composedestinations.generated.destinations.LogViewerDestination import com.ramcosta.composedestinations.generated.destinations.LogViewerDestination
import com.ramcosta.composedestinations.generated.destinations.PersonalizationDestination import com.ramcosta.composedestinations.generated.destinations.PersonalizationDestination
import com.ramcosta.composedestinations.generated.destinations.UmountManagerDestination import com.ramcosta.composedestinations.generated.destinations.ToolsDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.HazeStyle import dev.chrisbanes.haze.HazeStyle
@@ -74,23 +59,11 @@ import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeSource
import com.sukisu.ultra.Natives import com.sukisu.ultra.Natives
import com.sukisu.ultra.R import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.ConfirmResult
import com.sukisu.ultra.ui.component.DynamicManagerCard
import com.sukisu.ultra.ui.component.KsuIsValid import com.sukisu.ultra.ui.component.KsuIsValid
import com.sukisu.ultra.ui.component.SendLogDialog import com.sukisu.ultra.ui.component.SendLogDialog
import com.sukisu.ultra.ui.component.UninstallDialog import com.sukisu.ultra.ui.component.UninstallDialog
import com.sukisu.ultra.ui.component.rememberConfirmDialog
import com.sukisu.ultra.ui.component.rememberLoadingDialog import com.sukisu.ultra.ui.component.rememberLoadingDialog
import com.sukisu.ultra.ui.util.cleanRuntimeEnvironment
import com.sukisu.ultra.ui.util.execKsud import com.sukisu.ultra.ui.util.execKsud
import com.sukisu.ultra.ui.util.getUidMultiUserScan
import com.sukisu.ultra.ui.util.readUidScannerFile
import com.sukisu.ultra.ui.util.setUidAutoScan
import com.sukisu.ultra.ui.util.setUidMultiUserScan
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
@@ -238,6 +211,33 @@ fun SettingPager(
) )
} }
KsuIsValid {
val toolsTitle = stringResource(id = R.string.settings_tools)
Card(
modifier = Modifier
.padding(top = 12.dp)
.fillMaxWidth(),
) {
SuperArrow(
title = toolsTitle,
summary = stringResource(id = R.string.settings_tools_summary),
leftAction = {
Icon(
Icons.Rounded.DeveloperMode,
modifier = Modifier.padding(end = 16.dp),
contentDescription = toolsTitle,
tint = colorScheme.onBackground
)
},
onClick = {
navigator.navigate(ToolsDestination) {
launchSingleTop = true
}
}
)
}
}
KsuIsValid { KsuIsValid {
Card( Card(
modifier = Modifier modifier = Modifier
@@ -528,24 +528,6 @@ fun SettingPager(
} }
} }
KsuIsValid {
DynamicManagerCard()
}
KsuIsValid {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
Card(
modifier = Modifier
.padding(top = 12.dp)
.fillMaxWidth(),
) {
UidScannerSection(prefs = prefs, scope = scope, context = context)
}
}
KsuIsValid { KsuIsValid {
val lkmMode = Natives.isLkmMode val lkmMode = Natives.isLkmMode
if (lkmMode) { if (lkmMode) {
@@ -597,28 +579,6 @@ fun SettingPager(
} }
) )
} }
KsuIsValid {
val lkmMode = Natives.isLkmMode
if (lkmMode) {
val umontManager = stringResource(id = R.string.umount_path_manager)
SuperArrow(
title = umontManager,
leftAction = {
Icon(
Icons.Rounded.FolderDelete,
modifier = Modifier.padding(end = 16.dp),
contentDescription = umontManager,
tint = colorScheme.onBackground
)
},
onClick = {
navigator.navigate(UmountManagerDestination) {
}
}
)
}
}
SuperArrow( SuperArrow(
title = stringResource(id = R.string.send_log), title = stringResource(id = R.string.send_log),
leftAction = { leftAction = {
@@ -676,150 +636,3 @@ enum class UninstallType(val icon: ImageVector, val title: Int, val message: Int
), ),
NONE(Icons.Rounded.Adb, 0, 0) NONE(Icons.Rounded.Adb, 0, 0)
} }
@Composable
fun UidScannerSection(
prefs: SharedPreferences,
scope: CoroutineScope,
context: Context
) {
val realAuto = Natives.isUidScannerEnabled()
val realMulti = getUidMultiUserScan()
var autoOn by remember { mutableStateOf(realAuto) }
var multiOn by remember { mutableStateOf(realMulti) }
LaunchedEffect(Unit) {
autoOn = realAuto
multiOn = realMulti
prefs.edit {
putBoolean("uid_auto_scan", autoOn)
putBoolean("uid_multi_user_scan", multiOn)
}
}
SuperSwitch(
title = stringResource(R.string.uid_auto_scan_title),
summary = stringResource(R.string.uid_auto_scan_summary),
leftAction = {
Icon(
Icons.Filled.Scanner,
modifier = Modifier.padding(end = 16.dp),
contentDescription = stringResource(R.string.uid_auto_scan_title),
tint = colorScheme.onBackground
)
},
checked = autoOn,
onCheckedChange = { target ->
autoOn = target
if (!target) multiOn = false
scope.launch(Dispatchers.IO) {
setUidAutoScan(target)
val actual = Natives.isUidScannerEnabled() || readUidScannerFile()
withContext(Dispatchers.Main) {
autoOn = actual
if (!actual) multiOn = false
prefs.edit {
putBoolean("uid_auto_scan", actual)
putBoolean("uid_multi_user_scan", multiOn)
}
if (actual != target) {
Toast.makeText(
context,
context.getString(R.string.uid_scanner_setting_failed),
Toast.LENGTH_SHORT
).show()
}
}
}
}
)
AnimatedVisibility(
visible = autoOn,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
SuperSwitch(
title = stringResource(R.string.uid_multi_user_scan_title),
summary = stringResource(R.string.uid_multi_user_scan_summary),
leftAction = {
Icon(
Icons.Filled.Groups,
modifier = Modifier.padding(end = 16.dp),
contentDescription = stringResource(R.string.uid_multi_user_scan_title),
tint = colorScheme.onBackground
)
},
checked = multiOn,
onCheckedChange = { target ->
scope.launch(Dispatchers.IO) {
val ok = setUidMultiUserScan(target)
withContext(Dispatchers.Main) {
if (ok) {
multiOn = target
prefs.edit { putBoolean("uid_multi_user_scan", target) }
} else {
Toast.makeText(
context,
context.getString(R.string.uid_scanner_setting_failed),
Toast.LENGTH_SHORT
).show()
}
}
}
}
)
}
AnimatedVisibility(
visible = autoOn,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
val confirmDialog = rememberConfirmDialog()
SuperArrow(
title = stringResource(R.string.clean_runtime_environment),
summary = stringResource(R.string.clean_runtime_environment_summary),
leftAction = {
Icon(
Icons.Filled.CleaningServices,
modifier = Modifier.padding(end = 16.dp),
contentDescription = stringResource(R.string.clean_runtime_environment),
tint = colorScheme.onBackground
)
},
onClick = {
scope.launch {
if (confirmDialog.awaitConfirm(
title = context.getString(R.string.clean_runtime_environment),
content = context.getString(R.string.clean_runtime_environment_confirm)
) == ConfirmResult.Confirmed
) {
if (cleanRuntimeEnvironment()) {
autoOn = false
multiOn = false
prefs.edit {
putBoolean("uid_auto_scan", false)
putBoolean("uid_multi_user_scan", false)
}
Natives.setUidScannerEnabled(false)
Toast.makeText(
context,
context.getString(R.string.clean_runtime_environment_success),
Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
context,
context.getString(R.string.clean_runtime_environment_failed),
Toast.LENGTH_SHORT
).show()
}
}
}
}
)
}
}

View File

@@ -1,4 +1,4 @@
package com.sukisu.ultra.ui.screen package com.sukisu.ultra.ui.screen.settings
import android.content.Context import android.content.Context
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility

View File

@@ -0,0 +1,562 @@
package com.sukisu.ultra.ui.screen.settings
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.add
import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CleaningServices
import androidx.compose.material.icons.filled.Groups
import androidx.compose.material.icons.filled.Scanner
import androidx.compose.material.icons.rounded.Backup
import androidx.compose.material.icons.rounded.FolderDelete
import androidx.compose.material.icons.rounded.Restore
import androidx.compose.material.icons.rounded.Security
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.generated.destinations.UmountManagerDestination
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.ConfirmResult
import com.sukisu.ultra.ui.component.DynamicManagerCard
import com.sukisu.ultra.ui.component.KsuIsValid
import com.sukisu.ultra.ui.component.rememberConfirmDialog
import com.sukisu.ultra.ui.util.cleanRuntimeEnvironment
import com.sukisu.ultra.ui.util.getUidMultiUserScan
import com.sukisu.ultra.ui.util.readUidScannerFile
import com.sukisu.ultra.ui.util.setUidAutoScan
import com.sukisu.ultra.ui.util.setUidMultiUserScan
import com.sukisu.ultra.ui.util.getSELinuxStatus
import com.topjohnwu.superuser.Shell
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.HazeStyle
import dev.chrisbanes.haze.HazeTint
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.IconButton
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.extra.SuperArrow
import top.yukonga.miuix.kmp.extra.SuperSwitch
import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.icons.useful.Back
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
import top.yukonga.miuix.kmp.utils.getWindowSize
import top.yukonga.miuix.kmp.utils.overScrollVertical
import top.yukonga.miuix.kmp.utils.scrollEndHaptic
@Composable
@Destination<RootGraph>
fun Tools(
navigator: DestinationsNavigator
) {
val scrollBehavior = MiuixScrollBehavior()
val hazeState = remember { HazeState() }
val hazeStyle = HazeStyle(
backgroundColor = colorScheme.surface,
tint = HazeTint(colorScheme.surface.copy(0.8f))
)
val context = LocalContext.current
val scope = rememberCoroutineScope()
val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
Scaffold(
topBar = {
TopAppBar(
modifier = Modifier.hazeEffect(hazeState) {
style = hazeStyle
blurRadius = 30.dp
noiseFactor = 0f
},
color = Color.Transparent,
title = stringResource(R.string.tools),
scrollBehavior = scrollBehavior,
navigationIcon = {
IconButton(onClick = { navigator.popBackStack() }) {
Icon(
imageVector = MiuixIcons.Useful.Back,
contentDescription = "Back"
)
}
}
)
},
popupHost = { },
contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal)
) { innerPadding ->
LazyColumn(
modifier = Modifier
.height(getWindowSize().height.dp)
.scrollEndHaptic()
.overScrollVertical()
.nestedScroll(scrollBehavior.nestedScrollConnection)
.hazeSource(state = hazeState)
.padding(horizontal = 12.dp),
contentPadding = innerPadding,
overscrollEffect = null,
) {
item {
KsuIsValid {
SelinuxToggleSection(scope = scope, context = context)
Card(
modifier = Modifier
.padding(top = 12.dp)
.fillMaxWidth(),
) {
UidScannerSection(prefs = prefs, scope = scope, context = context)
}
DynamicManagerCard()
Card(
modifier = Modifier
.padding(top = 12.dp)
.fillMaxWidth(),
) {
val lkmMode = Natives.isLkmMode
if (lkmMode) {
val umontManager = stringResource(id = R.string.umount_path_manager)
SuperArrow(
title = umontManager,
leftAction = {
Icon(
Icons.Rounded.FolderDelete,
modifier = Modifier.padding(end = 16.dp),
contentDescription = umontManager,
tint = colorScheme.onBackground
)
},
onClick = {
navigator.navigate(UmountManagerDestination) {
}
}
)
}
}
AllowlistBackupSection(scope = scope, context = context)
}
}
}
}
}
@Composable
fun UidScannerSection(
prefs: SharedPreferences,
scope: CoroutineScope,
context: Context
) {
val realAuto = Natives.isUidScannerEnabled()
val realMulti = getUidMultiUserScan()
var autoOn by remember { mutableStateOf(realAuto) }
var multiOn by remember { mutableStateOf(realMulti) }
LaunchedEffect(Unit) {
autoOn = realAuto
multiOn = realMulti
prefs.edit {
putBoolean("uid_auto_scan", autoOn)
putBoolean("uid_multi_user_scan", multiOn)
}
}
SuperSwitch(
title = stringResource(R.string.uid_auto_scan_title),
summary = stringResource(R.string.uid_auto_scan_summary),
leftAction = {
Icon(
imageVector = Icons.Filled.Scanner,
modifier = Modifier.padding(end = 16.dp),
contentDescription = stringResource(R.string.uid_auto_scan_title),
tint = colorScheme.onBackground
)
},
checked = autoOn,
onCheckedChange = { target ->
autoOn = target
if (!target) multiOn = false
scope.launch(Dispatchers.IO) {
setUidAutoScan(target)
val actual = Natives.isUidScannerEnabled() || readUidScannerFile()
withContext(Dispatchers.Main) {
autoOn = actual
if (!actual) multiOn = false
prefs.edit {
putBoolean("uid_auto_scan", actual)
putBoolean("uid_multi_user_scan", multiOn)
}
if (actual != target) {
Toast.makeText(
context,
context.getString(R.string.uid_scanner_setting_failed),
Toast.LENGTH_SHORT
).show()
}
}
}
}
)
AnimatedVisibility(
visible = autoOn,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
SuperSwitch(
title = stringResource(R.string.uid_multi_user_scan_title),
summary = stringResource(R.string.uid_multi_user_scan_summary),
leftAction = {
Icon(
imageVector = Icons.Filled.Groups,
modifier = Modifier.padding(end = 16.dp),
contentDescription = stringResource(R.string.uid_multi_user_scan_title),
tint = colorScheme.onBackground
)
},
checked = multiOn,
onCheckedChange = { target ->
scope.launch(Dispatchers.IO) {
val ok = setUidMultiUserScan(target)
withContext(Dispatchers.Main) {
if (ok) {
multiOn = target
prefs.edit { putBoolean("uid_multi_user_scan", target) }
} else {
Toast.makeText(
context,
context.getString(R.string.uid_scanner_setting_failed),
Toast.LENGTH_SHORT
).show()
}
}
}
}
)
}
AnimatedVisibility(
visible = autoOn,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
val confirmDialog = rememberConfirmDialog()
SuperArrow(
title = stringResource(R.string.clean_runtime_environment),
summary = stringResource(R.string.clean_runtime_environment_summary),
leftAction = {
Icon(
imageVector = Icons.Filled.CleaningServices,
modifier = Modifier.padding(end = 16.dp),
contentDescription = stringResource(R.string.clean_runtime_environment),
tint = colorScheme.onBackground
)
},
onClick = {
scope.launch {
if (confirmDialog.awaitConfirm(
title = context.getString(R.string.clean_runtime_environment),
content = context.getString(R.string.clean_runtime_environment_confirm)
) == ConfirmResult.Confirmed
) {
if (cleanRuntimeEnvironment()) {
autoOn = false
multiOn = false
prefs.edit {
putBoolean("uid_auto_scan", false)
putBoolean("uid_multi_user_scan", false)
}
Natives.setUidScannerEnabled(false)
Toast.makeText(
context,
context.getString(R.string.clean_runtime_environment_success),
Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
context,
context.getString(R.string.clean_runtime_environment_failed),
Toast.LENGTH_SHORT
).show()
}
}
}
}
)
}
}
@Composable
fun SelinuxToggleSection(
scope: CoroutineScope,
context: Context
) {
var selinuxEnforcing by remember { mutableStateOf(true) }
var selinuxLoading by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
val current = withContext(Dispatchers.IO) { !isSelinuxPermissive() }
selinuxEnforcing = current
selinuxLoading = false
}
Card(
modifier = Modifier
.padding(top = 12.dp)
.fillMaxWidth(),
) {
val statusLabel = getSELinuxStatus()
SuperSwitch(
title = stringResource(R.string.tools_selinux_toggle),
summary = stringResource(
R.string.tools_selinux_summary,
statusLabel
),
leftAction = {
Icon(
imageVector = Icons.Rounded.Security,
modifier = Modifier.padding(end = 16.dp),
contentDescription = stringResource(id = R.string.tools_selinux_toggle),
tint = colorScheme.onBackground
)
},
checked = selinuxEnforcing,
enabled = !selinuxLoading,
onCheckedChange = { target ->
selinuxLoading = true
scope.launch(Dispatchers.IO) {
val success = if (target) {
setSelinuxPermissive(false)
} else {
setSelinuxPermissive(true)
}
val actual = !isSelinuxPermissive()
withContext(Dispatchers.Main) {
selinuxEnforcing = actual
selinuxLoading = false
Toast.makeText(
context,
if (success && actual == target) {
context.getString(
R.string.tools_selinux_apply_success,
context.getString(
if (actual) {
R.string.selinux_status_enforcing
} else {
R.string.selinux_status_permissive
}
)
)
} else {
context.getString(R.string.tools_selinux_apply_failed)
},
Toast.LENGTH_SHORT
).show()
}
}
}
)
}
}
@Composable
private fun AllowlistBackupSection(
scope: CoroutineScope,
context: Context
) {
val contextRef = remember { context }
val backupLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/octet-stream")
) { uri ->
if (uri == null) {
return@rememberLauncherForActivityResult
}
scope.launch {
val success = backupAllowlistToUri(contextRef, uri)
Toast.makeText(
contextRef,
contextRef.getString(
if (success) {
R.string.allowlist_backup_success
} else {
R.string.allowlist_backup_failed
}
),
Toast.LENGTH_SHORT
).show()
}
}
val restoreLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) { uri ->
if (uri == null) {
return@rememberLauncherForActivityResult
}
scope.launch {
val success = restoreAllowlistFromUri(contextRef, uri)
Toast.makeText(
contextRef,
contextRef.getString(
if (success) {
R.string.allowlist_restore_success
} else {
R.string.allowlist_restore_failed
}
),
Toast.LENGTH_SHORT
).show()
}
}
Card(
modifier = Modifier
.padding(vertical = 12.dp)
.fillMaxWidth(),
) {
SuperArrow(
title = stringResource(R.string.allowlist_backup_title),
summary = stringResource(R.string.allowlist_backup_summary_picker),
leftAction = {
Icon(
imageVector = Icons.Rounded.Backup,
modifier = Modifier.padding(end = 16.dp),
contentDescription = stringResource(R.string.allowlist_backup_title),
tint = colorScheme.onBackground
)
},
onClick = {
backupLauncher.launch("ksu_allowlist_backup.bin")
}
)
SuperArrow(
title = stringResource(R.string.allowlist_restore_title),
summary = stringResource(R.string.allowlist_restore_summary_picker),
leftAction = {
Icon(
imageVector = Icons.Rounded.Restore,
modifier = Modifier.padding(end = 16.dp),
contentDescription = stringResource(R.string.allowlist_restore_title),
tint = colorScheme.onBackground
)
},
onClick = {
restoreLauncher.launch(arrayOf("*/*"))
}
)
}
}
private fun isSelinuxPermissive(): Boolean {
val result = Shell.cmd("getenforce").exec()
val output = result.out.joinToString("\n").trim().lowercase()
return output == "permissive"
}
private fun setSelinuxPermissive(permissive: Boolean): Boolean {
val target = if (permissive) "0" else "1"
val result = Shell.cmd("setenforce $target").exec()
return result.isSuccess
}
private suspend fun backupAllowlistToUri(context: Context, targetUri: Uri): Boolean = withContext(Dispatchers.IO) {
val tempFile = File(context.cacheDir, "allowlist_backup_tmp.bin")
try {
if (!copyAllowlistToFile(tempFile)) return@withContext false
return@withContext runCatching {
context.contentResolver.openOutputStream(targetUri, "w")?.use { output ->
tempFile.inputStream().use { input ->
input.copyTo(output)
}
true
} ?: false
}.getOrElse { false }
} finally {
tempFile.delete()
}
}
private suspend fun restoreAllowlistFromUri(context: Context, sourceUri: Uri): Boolean = withContext(Dispatchers.IO) {
val tempFile = File(context.cacheDir, "allowlist_restore_tmp.bin")
try {
val downloaded = runCatching {
context.contentResolver.openInputStream(sourceUri)?.use { input ->
tempFile.outputStream().use { output ->
input.copyTo(output)
}
true
} ?: false
}.getOrElse { false }
if (!downloaded) return@withContext false
return@withContext copyFileToAllowlist(tempFile)
} finally {
tempFile.delete()
}
}
private suspend fun copyAllowlistToFile(targetFile: File): Boolean = withContext(Dispatchers.IO) {
runCatching {
targetFile.parentFile?.mkdirs()
val result = Shell.cmd(
"cp /data/adb/ksu/.allowlist \"${targetFile.absolutePath}\"",
"chmod 0644 \"${targetFile.absolutePath}\""
).exec()
result.isSuccess
}.getOrDefault(false)
}
private suspend fun copyFileToAllowlist(sourceFile: File): Boolean = withContext(Dispatchers.IO) {
if (!sourceFile.exists()) return@withContext false
runCatching {
val result = Shell.cmd(
"cp \"${sourceFile.absolutePath}\" /data/adb/ksu/.allowlist",
"chmod 0644 /data/adb/ksu/.allowlist"
).exec()
result.isSuccess
}.getOrDefault(false)
}

View File

@@ -352,4 +352,19 @@
<string name="confirm_uninstall_content">以下 KPM 将被卸载:%s</string> <string name="confirm_uninstall_content">以下 KPM 将被卸载:%s</string>
<string name="snackbar_failed_to_check_module_file">无法检查模块文件是否存在</string> <string name="snackbar_failed_to_check_module_file">无法检查模块文件是否存在</string>
<string name="cancel">取消</string> <string name="cancel">取消</string>
<!-- Tools -->
<string name="settings_tools">工具</string>
<string name="settings_tools_summary">更多高级功能</string>
<string name="tools_selinux_toggle">SELinux 模式</string>
<string name="tools_selinux_summary">当前:%1$s</string>
<string name="tools_selinux_apply_success">SELinux 已切换为 %1$s</string>
<string name="tools_selinux_apply_failed">切换 SELinux 模式失败</string>
<string name="allowlist_backup_title">备份允许列表</string>
<string name="allowlist_backup_summary_picker">选择位置导出允许列表</string>
<string name="allowlist_backup_success">备份成功</string>
<string name="allowlist_backup_failed">备份失败</string>
<string name="allowlist_restore_title">还原允许列表</string>
<string name="allowlist_restore_summary_picker">选择备份文件进行导入</string>
<string name="allowlist_restore_success">还原成功</string>
<string name="allowlist_restore_failed">还原失败</string>
</resources> </resources>

View File

@@ -356,4 +356,19 @@
<string name="confirm_uninstall_content">The following KPM will be uninstalled: %s</string> <string name="confirm_uninstall_content">The following KPM will be uninstalled: %s</string>
<string name="snackbar_failed_to_check_module_file">Unable to check if module file exists</string> <string name="snackbar_failed_to_check_module_file">Unable to check if module file exists</string>
<string name="cancel">Cancel</string> <string name="cancel">Cancel</string>
<!-- Tools -->
<string name="settings_tools">Tools</string>
<string name="settings_tools_summary">More advanced features.</string>
<string name="tools_selinux_toggle">SELinux mode</string>
<string name="tools_selinux_summary">Current: %1$s</string>
<string name="tools_selinux_apply_success">SELinux switched to %1$s</string>
<string name="tools_selinux_apply_failed">Failed to switch SELinux mode</string>
<string name="allowlist_backup_title">Backup allowlist</string>
<string name="allowlist_backup_summary_picker">Choose a location to export the allowlist</string>
<string name="allowlist_backup_success">Backup succeeded</string>
<string name="allowlist_backup_failed">Backup failed</string>
<string name="allowlist_restore_title">Restore allowlist</string>
<string name="allowlist_restore_summary_picker">Choose a backup file to import</string>
<string name="allowlist_restore_success">Restore succeeded</string>
<string name="allowlist_restore_failed">Restore failed</string>
</resources> </resources>