diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt index 9224e2d6..36b8d0a0 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt @@ -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() - } - } - } - } - ) - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Personalization.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/settings/Personalization.kt similarity index 99% rename from manager/app/src/main/java/com/sukisu/ultra/ui/screen/Personalization.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/screen/settings/Personalization.kt index 479ea375..08b3056b 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Personalization.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/settings/Personalization.kt @@ -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 diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/settings/ToolsScreen.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/settings/ToolsScreen.kt new file mode 100644 index 00000000..edeb290a --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/settings/ToolsScreen.kt @@ -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 +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) +} diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index 13bddee5..bfa231ae 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -352,4 +352,19 @@ 以下 KPM 将被卸载:%s 无法检查模块文件是否存在 取消 + + 工具 + 更多高级功能 + SELinux 模式 + 当前:%1$s + SELinux 已切换为 %1$s + 切换 SELinux 模式失败 + 备份允许列表 + 选择位置导出允许列表 + 备份成功 + 备份失败 + 还原允许列表 + 选择备份文件进行导入 + 还原成功 + 还原失败 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index baa3a9d9..4d8c3848 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -356,4 +356,19 @@ The following KPM will be uninstalled: %s Unable to check if module file exists Cancel + + Tools + More advanced features. + SELinux mode + Current: %1$s + SELinux switched to %1$s + Failed to switch SELinux mode + Backup allowlist + Choose a location to export the allowlist + Backup succeeded + Backup failed + Restore allowlist + Choose a backup file to import + Restore succeeded + Restore failed