You've already forked KernelSU-Next
mirror of
https://github.com/KernelSU-Next/KernelSU-Next.git
synced 2025-08-27 23:46:34 +00:00
manager: added allowlist backup and restore (#9)
manager: create separate module & restore screen
This commit is contained in:
@@ -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<RootGraph>
|
||||
@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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -81,8 +81,13 @@
|
||||
<string name="reboot_message">The changes will take effect after the system restart. Do you want to reboot now?</string>
|
||||
<string name="module_restore">Restore module</string>
|
||||
<string name="module_restore_message">Restore modules from recent backup.</string>
|
||||
<string name="backup_restore">Backup & Restore</string>
|
||||
<string name="module_backup">Backup module</string>
|
||||
<string name="module_backup_message">Backup currently installed modules.</string>
|
||||
<string name="allowlist_restore">Restore allowlist</string>
|
||||
<string name="allowlist_restore_message">Restore allowlist from recent backup.</string>
|
||||
<string name="allowlist_backup">Backup allowlist</string>
|
||||
<string name="allowlist_backup_message">Backup currently configured allowlist.</string>
|
||||
<string name="warning">Warning</string>
|
||||
<string name="warning_message">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.</string>
|
||||
<string name="proceed">Proceed</string>
|
||||
|
||||
Reference in New Issue
Block a user