diff --git a/manager/app/src/main/java/com/rifsxd/ksunext/ui/screen/BackupRestore.kt b/manager/app/src/main/java/com/rifsxd/ksunext/ui/screen/BackupRestore.kt new file mode 100644 index 00000000..59123081 --- /dev/null +++ b/manager/app/src/main/java/com/rifsxd/ksunext/ui/screen/BackupRestore.kt @@ -0,0 +1,275 @@ +package com.rifsxd.ksunext.ui.screen + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Undo +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.Text +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.LaunchedEffect +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.LineHeightStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.content.FileProvider +import com.maxkeppeker.sheets.core.models.base.Header +import com.maxkeppeker.sheets.core.models.base.IconSource +import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState +import com.maxkeppeler.sheets.list.ListDialog +import com.maxkeppeler.sheets.list.models.ListOption +import com.maxkeppeler.sheets.list.models.ListSelection +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import com.rifsxd.ksunext.BuildConfig +import com.rifsxd.ksunext.Natives +import com.rifsxd.ksunext.ksuApp +import com.rifsxd.ksunext.R +import com.rifsxd.ksunext.ui.component.AboutDialog +import com.rifsxd.ksunext.ui.component.ConfirmResult +import com.rifsxd.ksunext.ui.component.DialogHandle +import com.rifsxd.ksunext.ui.component.SwitchItem +import com.rifsxd.ksunext.ui.component.rememberConfirmDialog +import com.rifsxd.ksunext.ui.component.rememberCustomDialog +import com.rifsxd.ksunext.ui.component.rememberLoadingDialog +import com.rifsxd.ksunext.ui.util.LocalSnackbarHost +import com.rifsxd.ksunext.ui.util.getBugreportFile +import com.rifsxd.ksunext.ui.util.* +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +/** + * @author rifsxd + * @date 2025/1/14. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Destination +@Composable +fun BackupRestoreScreen(navigator: DestinationsNavigator) { + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val snackBarHost = LocalSnackbarHost.current + + val isManager = Natives.becomeManager(ksuApp.packageName) + val ksuVersion = if (isManager) Natives.version else null + + Scaffold( + topBar = { + TopBar( + scrollBehavior = scrollBehavior + ) + }, + snackbarHost = { SnackbarHost(snackBarHost) }, + contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + ) { paddingValues -> + val loadingDialog = rememberLoadingDialog() + val restoreDialog = rememberConfirmDialog() + val backupDialog = rememberConfirmDialog() + + Column( + modifier = Modifier + .padding(paddingValues) + .nestedScroll(scrollBehavior.nestedScrollConnection) + .verticalScroll(rememberScrollState()) + ) { + + val context = LocalContext.current + val scope = rememberCoroutineScope() + + var showRebootDialog by remember { mutableStateOf(false) } + + if (showRebootDialog) { + AlertDialog( + onDismissRequest = { showRebootDialog = false }, + title = { Text(stringResource(R.string.reboot_required)) }, + text = { Text(stringResource(R.string.reboot_message)) }, + confirmButton = { + TextButton(onClick = { + showRebootDialog = false + reboot() + }) { + Text(stringResource(R.string.reboot)) + } + }, + dismissButton = { + TextButton(onClick = { showRebootDialog = false }) { + Text(stringResource(R.string.later)) + } + } + ) + } + + val moduleBackup = stringResource(id = R.string.module_backup) + val backupMessage = stringResource(id = R.string.module_backup_message) + ListItem( + leadingContent = { + Icon( + Icons.Filled.Backup, + moduleBackup + ) + }, + headlineContent = { Text(moduleBackup) }, + modifier = Modifier.clickable { + scope.launch { + val result = backupDialog.awaitConfirm(title = moduleBackup, content = backupMessage) + if (result == ConfirmResult.Confirmed) { + loadingDialog.withLoading { + moduleBackup() + } + } + } + } + ) + + if (showRebootDialog) { + AlertDialog( + onDismissRequest = { showRebootDialog = false }, + title = { Text(stringResource(R.string.reboot_required)) }, + text = { Text(stringResource(R.string.reboot_message)) }, + confirmButton = { + TextButton(onClick = { + showRebootDialog = false + reboot() + }) { + Text(stringResource(R.string.reboot)) + } + }, + dismissButton = { + TextButton(onClick = { showRebootDialog = false }) { + Text(stringResource(R.string.later)) + } + } + ) + } + + val moduleRestore = stringResource(id = R.string.module_restore) + val restoreMessage = stringResource(id = R.string.module_restore_message) + ListItem( + leadingContent = { + Icon( + Icons.Filled.Restore, + moduleRestore + ) + }, + headlineContent = { Text(moduleRestore) }, + modifier = Modifier.clickable { + scope.launch { + val result = restoreDialog.awaitConfirm(title = moduleRestore, content = restoreMessage) + if (result == ConfirmResult.Confirmed) { + loadingDialog.withLoading { + moduleRestore() + showRebootDialog = true + } + } + } + } + ) + + val allowlistBackup = stringResource(id = R.string.allowlist_backup) + val allowlistbackupMessage = stringResource(id = R.string.allowlist_backup_message) + ListItem( + leadingContent = { + Icon( + Icons.Filled.Backup, + allowlistBackup + ) + }, + headlineContent = { Text(allowlistBackup) }, + modifier = Modifier.clickable { + scope.launch { + val result = backupDialog.awaitConfirm(title = allowlistBackup, content = allowlistbackupMessage) + if (result == ConfirmResult.Confirmed) { + loadingDialog.withLoading { + allowlistBackup() + } + } + } + } + ) + + val allowlistRestore = stringResource(id = R.string.allowlist_restore) + val allowlistrestoreMessage = stringResource(id = R.string.allowlist_restore_message) + ListItem( + leadingContent = { + Icon( + Icons.Filled.Restore, + allowlistRestore + ) + }, + headlineContent = { Text(allowlistRestore) }, + modifier = Modifier.clickable { + scope.launch { + val result = restoreDialog.awaitConfirm(title = allowlistRestore, content = allowlistrestoreMessage) + if (result == ConfirmResult.Confirmed) { + loadingDialog.withLoading { + allowlistRestore() + } + } + } + } + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TopBar( + scrollBehavior: TopAppBarScrollBehavior? = null +) { + TopAppBar( + title = { Text(stringResource(R.string.backup_restore)) }, + windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + scrollBehavior = scrollBehavior + ) +} + +@Preview +@Composable +private fun BackupPreview() { + BackupRestoreScreen(EmptyDestinationsNavigator) +} diff --git a/manager/app/src/main/java/com/rifsxd/ksunext/ui/screen/Settings.kt b/manager/app/src/main/java/com/rifsxd/ksunext/ui/screen/Settings.kt index 59680975..c899619e 100644 --- a/manager/app/src/main/java/com/rifsxd/ksunext/ui/screen/Settings.kt +++ b/manager/app/src/main/java/com/rifsxd/ksunext/ui/screen/Settings.kt @@ -63,6 +63,7 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination +import com.ramcosta.composedestinations.generated.destinations.BackupRestoreScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator import kotlinx.coroutines.Dispatchers @@ -113,8 +114,6 @@ fun SettingScreen(navigator: DestinationsNavigator) { } val loadingDialog = rememberLoadingDialog() val shrinkDialog = rememberConfirmDialog() - val restoreDialog = rememberConfirmDialog() - val backupDialog = rememberConfirmDialog() Column( modifier = Modifier @@ -163,6 +162,7 @@ fun SettingScreen(navigator: DestinationsNavigator) { title = stringResource(id = R.string.settings_umount_modules_default), summary = stringResource(id = R.string.settings_umount_modules_default_summary), checked = umountChecked + ) { if (Natives.setDefaultUmountModules(it)) { umountChecked = it @@ -436,50 +436,17 @@ fun SettingScreen(navigator: DestinationsNavigator) { } if (ksuVersion != null) { - val moduleBackup = stringResource(id = R.string.module_backup) - val backupMessage = stringResource(id = R.string.module_backup_message) + val backupRestore = stringResource(id = R.string.backup_restore) ListItem( leadingContent = { Icon( Icons.Filled.Backup, - moduleBackup + backupRestore ) }, - headlineContent = { Text(moduleBackup) }, + headlineContent = { Text(backupRestore) }, modifier = Modifier.clickable { - scope.launch { - val result = backupDialog.awaitConfirm(title = moduleBackup, content = backupMessage) - if (result == ConfirmResult.Confirmed) { - loadingDialog.withLoading { - moduleBackup() - } - } - } - } - ) - } - - if (ksuVersion != null) { - val moduleRestore = stringResource(id = R.string.module_restore) - val restoreMessage = stringResource(id = R.string.module_restore_message) - ListItem( - leadingContent = { - Icon( - Icons.Filled.Restore, - moduleRestore - ) - }, - headlineContent = { Text(moduleRestore) }, - modifier = Modifier.clickable { - scope.launch { - val result = restoreDialog.awaitConfirm(title = moduleRestore, content = restoreMessage) - if (result == ConfirmResult.Confirmed) { - loadingDialog.withLoading { - moduleRestore() - showRebootDialog = true - } - } - } + navigator.navigate(BackupRestoreScreenDestination) } ) } diff --git a/manager/app/src/main/java/com/rifsxd/ksunext/ui/util/KsuCli.kt b/manager/app/src/main/java/com/rifsxd/ksunext/ui/util/KsuCli.kt index 61179db9..cc6dceb7 100644 --- a/manager/app/src/main/java/com/rifsxd/ksunext/ui/util/KsuCli.kt +++ b/manager/app/src/main/java/com/rifsxd/ksunext/ui/util/KsuCli.kt @@ -503,6 +503,59 @@ fun moduleRestore(): Boolean { return result.isEmpty() } +fun allowlistBackupDir(): String? { + val shell = getRootShell() + val baseBackupDir = "/data/adb/allowlist_bak" + val resultBase = ShellUtils.fastCmd(shell, "mkdir -p $baseBackupDir").trim() + if (resultBase.isNotEmpty()) return null + + val timestamp = ShellUtils.fastCmd(shell, "date +%Y%m%d_%H%M%S").trim() + if (timestamp.isEmpty()) return null + + val newBackupDir = "$baseBackupDir/$timestamp" + val resultNewDir = ShellUtils.fastCmd(shell, "mkdir -p $newBackupDir").trim() + + if (resultNewDir.isEmpty()) return newBackupDir + return null +} + +fun allowlistBackup(): Boolean { + val shell = getRootShell() + + val checkEmptyCommand = "if [ -z \"$(ls -A /data/adb/ksu/.allowlist)\" ]; then echo 'empty'; fi" + val resultCheckEmpty = ShellUtils.fastCmd(shell, checkEmptyCommand).trim() + + if (resultCheckEmpty == "empty") { + return false + } + + val backupDir = allowlistBackupDir() ?: return false + val command = "cp -rp /data/adb/ksu/.allowlist $backupDir" + val result = ShellUtils.fastCmd(shell, command).trim() + + return result.isEmpty() +} + +fun allowlistRestore(): Boolean { + val shell = getRootShell() + + val command = "ls -t /data/adb/allowlist_bak | head -n 1" + val latestBackupDir = ShellUtils.fastCmd(shell, command).trim() + + if (latestBackupDir.isEmpty()) return false + + val sourceDir = "/data/adb/allowlist_bak/$latestBackupDir" + val destinationDir = "/data/adb/ksu/" + + val createDestDirCommand = "mkdir -p $destinationDir" + ShellUtils.fastCmd(shell, createDestDirCommand) + + val moveCommand = "cp -rp $sourceDir/.allowlist $destinationDir" + val result = ShellUtils.fastCmd(shell, moveCommand).trim() + + return result.isEmpty() +} + private fun getSuSFSDaemonPath(): String { return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libsusfsd.so" } diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 38d5dd51..46aad40c 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -81,8 +81,13 @@ The changes will take effect after the system restart. Do you want to reboot now? Restore module Restore modules from recent backup. + Backup & Restore Backup module Backup currently installed modules. + Restore allowlist + Restore allowlist from recent backup. + Backup allowlist + Backup currently configured allowlist. Warning This feature is still in beta and under development. Please ensure you backup your modules before proceeding. Only use this feature if you understand the potential risks. Proceed with caution. Proceed