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:
@@ -1,14 +1,6 @@
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
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.WindowInsets
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.rounded.Palette
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
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.BugReport
|
||||
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.Fence
|
||||
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.RemoveModerator
|
||||
import androidx.compose.material.icons.rounded.RestartAlt
|
||||
import androidx.compose.material.icons.rounded.Update
|
||||
import androidx.compose.material.icons.rounded.UploadFile
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
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.LogViewerDestination
|
||||
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 dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.HazeStyle
|
||||
@@ -74,23 +59,11 @@ import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
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.SendLogDialog
|
||||
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.util.cleanRuntimeEnvironment
|
||||
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.Icon
|
||||
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 {
|
||||
Card(
|
||||
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 {
|
||||
val lkmMode = Natives.isLkmMode
|
||||
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(
|
||||
title = stringResource(id = R.string.send_log),
|
||||
leftAction = {
|
||||
@@ -676,150 +636,3 @@ enum class UninstallType(val icon: ImageVector, val title: Int, val message: Int
|
||||
),
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.sukisu.ultra.ui.screen
|
||||
package com.sukisu.ultra.ui.screen.settings
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
@@ -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)
|
||||
}
|
||||
@@ -352,4 +352,19 @@
|
||||
<string name="confirm_uninstall_content">以下 KPM 将被卸载:%s</string>
|
||||
<string name="snackbar_failed_to_check_module_file">无法检查模块文件是否存在</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>
|
||||
|
||||
@@ -356,4 +356,19 @@
|
||||
<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="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>
|
||||
|
||||
Reference in New Issue
Block a user