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
|
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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="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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user