manager: Implement editable and removable mount points for LKM
This commit is contained in:
@@ -59,6 +59,7 @@ static int init_default_entries(void)
|
|||||||
{ "/product", true, 0 },
|
{ "/product", true, 0 },
|
||||||
{ "/system_ext", true, 0 },
|
{ "/system_ext", true, 0 },
|
||||||
{ "/data/adb/modules", false, MNT_DETACH },
|
{ "/data/adb/modules", false, MNT_DETACH },
|
||||||
|
{ "/debug_ramdisk", false, MNT_DETACH },
|
||||||
};
|
};
|
||||||
|
|
||||||
for (int i = 0; i < ARRAY_SIZE(defaults); i++) {
|
for (int i = 0; i < ARRAY_SIZE(defaults); i++) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||||
import androidx.compose.material.icons.automirrored.filled.Undo
|
import androidx.compose.material.icons.automirrored.filled.Undo
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material.icons.rounded.EnhancedEncryption
|
import androidx.compose.material.icons.rounded.EnhancedEncryption
|
||||||
@@ -43,6 +44,7 @@ import com.ramcosta.composedestinations.annotation.RootGraph
|
|||||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
||||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||||
import com.ramcosta.composedestinations.generated.destinations.LogViewerScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.LogViewerScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.UmountManagerScreenDestination
|
||||||
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
import com.sukisu.ultra.BuildConfig
|
import com.sukisu.ultra.BuildConfig
|
||||||
@@ -410,6 +412,19 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val lkmMode = Natives.isLkmMode
|
||||||
|
KsuIsValid {
|
||||||
|
if (lkmMode) {
|
||||||
|
SettingItem(
|
||||||
|
icon = Icons.Filled.FolderOff,
|
||||||
|
title = stringResource(R.string.umount_path_manager),
|
||||||
|
summary = stringResource(R.string.umount_path_manager_summary),
|
||||||
|
onClick = {
|
||||||
|
navigator.navigate(UmountManagerScreenDestination)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (showBottomsheet) {
|
if (showBottomsheet) {
|
||||||
LogBottomSheet(
|
LogBottomSheet(
|
||||||
@@ -452,8 +467,6 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val lkmMode = Natives.isLkmMode
|
|
||||||
if (lkmMode) {
|
if (lkmMode) {
|
||||||
UninstallItem(navigator) {
|
UninstallItem(navigator) {
|
||||||
loadingDialog.withLoading(it)
|
loadingDialog.withLoading(it)
|
||||||
@@ -1118,7 +1131,7 @@ fun SettingDropdown(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.ArrowForward,
|
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
|
|||||||
@@ -0,0 +1,442 @@
|
|||||||
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
||||||
|
import com.sukisu.ultra.ui.component.ConfirmResult
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
|
import com.sukisu.ultra.ui.theme.getCardColors
|
||||||
|
import com.sukisu.ultra.ui.theme.getCardElevation
|
||||||
|
import com.sukisu.ultra.ui.util.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
private val SPACING_SMALL = 3.dp
|
||||||
|
private val SPACING_MEDIUM = 8.dp
|
||||||
|
private val SPACING_LARGE = 16.dp
|
||||||
|
|
||||||
|
data class UmountPathEntry(
|
||||||
|
val path: String,
|
||||||
|
val checkMnt: Boolean,
|
||||||
|
val flags: Int,
|
||||||
|
val isDefault: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Destination<RootGraph>
|
||||||
|
@Composable
|
||||||
|
fun UmountManagerScreen(navigator: DestinationsNavigator) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
val snackBarHost = LocalSnackbarHost.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val confirmDialog = rememberConfirmDialog()
|
||||||
|
|
||||||
|
var pathList by remember { mutableStateOf<List<UmountPathEntry>>(emptyList()) }
|
||||||
|
var isLoading by remember { mutableStateOf(false) }
|
||||||
|
var showAddDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
fun loadPaths() {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
isLoading = true
|
||||||
|
val result = listUmountPaths()
|
||||||
|
val entries = parseUmountPaths(result)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
pathList = entries
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
loadPaths()
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(stringResource(R.string.umount_path_manager)) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navigator.navigateUp() }) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = { loadPaths() }) {
|
||||||
|
Icon(Icons.Filled.Refresh, contentDescription = null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(
|
||||||
|
alpha = CardConfig.cardAlpha
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = { showAddDialog = true }
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.Add, contentDescription = null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(snackBarHost) }
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(SPACING_LARGE),
|
||||||
|
colors = getCardColors(MaterialTheme.colorScheme.primaryContainer),
|
||||||
|
elevation = getCardElevation()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(SPACING_LARGE)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Info,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.umount_path_restart_notice),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(SPACING_MEDIUM)
|
||||||
|
) {
|
||||||
|
items(pathList, key = { it.path }) { entry ->
|
||||||
|
UmountPathCard(
|
||||||
|
entry = entry,
|
||||||
|
onDelete = {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
val success = removeUmountPath(entry.path)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (success) {
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
context.getString(R.string.umount_path_removed)
|
||||||
|
)
|
||||||
|
loadPaths()
|
||||||
|
} else {
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
context.getString(R.string.operation_failed)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(SPACING_LARGE))
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = SPACING_LARGE),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
if (confirmDialog.awaitConfirm(
|
||||||
|
title = context.getString(R.string.confirm_action),
|
||||||
|
content = context.getString(R.string.confirm_clear_custom_paths)
|
||||||
|
) == ConfirmResult.Confirmed) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val success = clearCustomUmountPaths()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (success) {
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
context.getString(R.string.custom_paths_cleared)
|
||||||
|
)
|
||||||
|
loadPaths()
|
||||||
|
} else {
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
context.getString(R.string.operation_failed)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.DeleteForever, contentDescription = null)
|
||||||
|
Spacer(modifier = Modifier.width(SPACING_MEDIUM))
|
||||||
|
Text(stringResource(R.string.clear_custom_paths))
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
val success = applyUmountConfigToKernel()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (success) {
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
context.getString(R.string.config_applied)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
context.getString(R.string.operation_failed)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.Check, contentDescription = null)
|
||||||
|
Spacer(modifier = Modifier.width(SPACING_MEDIUM))
|
||||||
|
Text(stringResource(R.string.apply_config))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showAddDialog) {
|
||||||
|
AddUmountPathDialog(
|
||||||
|
onDismiss = { showAddDialog = false },
|
||||||
|
onConfirm = { path, checkMnt, flags ->
|
||||||
|
showAddDialog = false
|
||||||
|
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
val success = addUmountPath(path, checkMnt, flags)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (success) {
|
||||||
|
saveUmountConfig()
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
context.getString(R.string.umount_path_added)
|
||||||
|
)
|
||||||
|
loadPaths()
|
||||||
|
} else {
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
context.getString(R.string.operation_failed)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UmountPathCard(
|
||||||
|
entry: UmountPathEntry,
|
||||||
|
onDelete: () -> Unit
|
||||||
|
) {
|
||||||
|
val confirmDialog = rememberConfirmDialog()
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerLow),
|
||||||
|
elevation = getCardElevation()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(SPACING_LARGE),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Folder,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (entry.isDefault)
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.secondary,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(SPACING_LARGE))
|
||||||
|
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = entry.path,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(SPACING_SMALL))
|
||||||
|
Text(
|
||||||
|
text = buildString {
|
||||||
|
append(context.getString(R.string.check_mount_type))
|
||||||
|
append(": ")
|
||||||
|
append(if (entry.checkMnt) context.getString(R.string.yes) else context.getString(R.string.no))
|
||||||
|
append(" | ")
|
||||||
|
append(context.getString(R.string.flags))
|
||||||
|
append(": ")
|
||||||
|
append(entry.flags.toUmountFlagName(context))
|
||||||
|
if (entry.isDefault) {
|
||||||
|
append(" | ")
|
||||||
|
append(context.getString(R.string.default_entry))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entry.isDefault) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
if (confirmDialog.awaitConfirm(
|
||||||
|
title = context.getString(R.string.confirm_delete),
|
||||||
|
content = context.getString(R.string.confirm_delete_umount_path, entry.path)
|
||||||
|
) == ConfirmResult.Confirmed) {
|
||||||
|
onDelete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Delete,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddUmountPathDialog(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirm: (String, Boolean, Int) -> Unit
|
||||||
|
) {
|
||||||
|
var path by rememberSaveable { mutableStateOf("") }
|
||||||
|
var checkMnt by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var flags by rememberSaveable { mutableStateOf("-1") }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(stringResource(R.string.add_umount_path)) },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = path,
|
||||||
|
onValueChange = { path = it },
|
||||||
|
label = { Text(stringResource(R.string.mount_path)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = checkMnt,
|
||||||
|
onCheckedChange = { checkMnt = it }
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(SPACING_SMALL))
|
||||||
|
Text(stringResource(R.string.check_mount_type_overlay))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = flags,
|
||||||
|
onValueChange = { flags = it },
|
||||||
|
label = { Text(stringResource(R.string.umount_flags)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
supportingText = { Text(stringResource(R.string.umount_flags_hint)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
val flagsInt = flags.toIntOrNull() ?: -1
|
||||||
|
onConfirm(path, checkMnt, flagsInt)
|
||||||
|
},
|
||||||
|
enabled = path.isNotBlank()
|
||||||
|
) {
|
||||||
|
Text(stringResource(android.R.string.ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringResource(android.R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseUmountPaths(output: String): List<UmountPathEntry> {
|
||||||
|
val lines = output.lines().filter { it.isNotBlank() }
|
||||||
|
if (lines.size < 2) return emptyList()
|
||||||
|
|
||||||
|
return lines.drop(2).mapNotNull { line ->
|
||||||
|
val parts = line.trim().split(Regex("\\s+"))
|
||||||
|
if (parts.size >= 4) {
|
||||||
|
UmountPathEntry(
|
||||||
|
path = parts[0],
|
||||||
|
checkMnt = parts[1].equals("true", ignoreCase = true),
|
||||||
|
flags = parts[2].toIntOrNull() ?: -1,
|
||||||
|
isDefault = parts[3].equals("Yes", ignoreCase = true)
|
||||||
|
)
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Int.toUmountFlagName(context: android.content.Context): String {
|
||||||
|
return when (this) {
|
||||||
|
-1 -> context.getString(R.string.mnt_detach)
|
||||||
|
else -> this.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -665,3 +665,56 @@ fun readUidScannerFile(): Boolean {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addUmountPath(path: String, checkMnt: Boolean, flags: Int): Boolean {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val checkMntFlag = if (checkMnt) "--check-mnt" else ""
|
||||||
|
val flagsArg = if (flags >= 0) "--flags $flags" else ""
|
||||||
|
val cmd = "${getKsuDaemonPath()} umount add $path $checkMntFlag $flagsArg"
|
||||||
|
val result = ShellUtils.fastCmdResult(shell, cmd)
|
||||||
|
Log.i(TAG, "add umount path $path result: $result")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeUmountPath(path: String): Boolean {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKsuDaemonPath()} umount remove $path"
|
||||||
|
val result = ShellUtils.fastCmdResult(shell, cmd)
|
||||||
|
Log.i(TAG, "remove umount path $path result: $result")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun listUmountPaths(): String {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKsuDaemonPath()} umount list"
|
||||||
|
return try {
|
||||||
|
runCmd(shell, cmd).trim()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to list umount paths", e)
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearCustomUmountPaths(): Boolean {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKsuDaemonPath()} umount clear-custom"
|
||||||
|
val result = ShellUtils.fastCmdResult(shell, cmd)
|
||||||
|
Log.i(TAG, "clear custom umount paths result: $result")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveUmountConfig(): Boolean {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKsuDaemonPath()} umount save"
|
||||||
|
val result = ShellUtils.fastCmdResult(shell, cmd)
|
||||||
|
Log.i(TAG, "save umount config result: $result")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyUmountConfigToKernel(): Boolean {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKsuDaemonPath()} umount apply"
|
||||||
|
val result = ShellUtils.fastCmdResult(shell, cmd)
|
||||||
|
Log.i(TAG, "apply umount config to kernel result: $result")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -729,4 +729,26 @@
|
|||||||
<string name="incompatible_kernel_msg">
|
<string name="incompatible_kernel_msg">
|
||||||
当前管理器与此内核不兼容!请将内核升级至版本 %2$d 或以上(当前 %1$d)
|
当前管理器与此内核不兼容!请将内核升级至版本 %2$d 或以上(当前 %1$d)
|
||||||
</string>
|
</string>
|
||||||
|
<string name="umount_path_manager">Umount 路径管理</string>
|
||||||
|
<string name="umount_path_manager_summary">管理内核卸载路径</string>
|
||||||
|
<string name="umount_path_restart_notice">添加或删除路径后需要重启设备才能生效。系统会在下次启动时应用新的配置。</string>
|
||||||
|
<string name="add_umount_path">添加 Umount 路径</string>
|
||||||
|
<string name="mount_path">挂载路径</string>
|
||||||
|
<string name="check_mount_type">检查挂载类型</string>
|
||||||
|
<string name="check_mount_type_overlay">检查是否为 overlay 类型</string>
|
||||||
|
<string name="umount_flags">卸载标志</string>
|
||||||
|
<string name="umount_flags_hint">0=正常卸载, 8=MNT_DETACH, -1=自动</string>
|
||||||
|
<string name="flags">标志</string>
|
||||||
|
<string name="default_entry">默认条目</string>
|
||||||
|
<string name="confirm_delete">确认删除</string>
|
||||||
|
<string name="confirm_delete_umount_path">确定要删除路径 %s 吗?</string>
|
||||||
|
<string name="umount_path_added">路径已添加,重启后生效</string>
|
||||||
|
<string name="umount_path_removed">路径已删除,重启后生效</string>
|
||||||
|
<string name="operation_failed">操作失败</string>
|
||||||
|
<string name="confirm_action">确认操作</string>
|
||||||
|
<string name="confirm_clear_custom_paths">确定要清除所有自定义路径吗?(默认路径将保留)</string>
|
||||||
|
<string name="custom_paths_cleared">自定义路径已清除</string>
|
||||||
|
<string name="clear_custom_paths">清除自定义</string>
|
||||||
|
<string name="apply_config">应用配置</string>
|
||||||
|
<string name="config_applied">配置已应用到内核</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -737,4 +737,27 @@ Important Note:\n
|
|||||||
<string name="incompatible_kernel_msg">
|
<string name="incompatible_kernel_msg">
|
||||||
The current manager is incompatible with this kernel! Please upgrade the kernel to version %2$d or higher (currently %1$d)
|
The current manager is incompatible with this kernel! Please upgrade the kernel to version %2$d or higher (currently %1$d)
|
||||||
</string>
|
</string>
|
||||||
|
<string name="umount_path_manager">Umount Path Management</string>
|
||||||
|
<string name="umount_path_manager_summary">Manage kernel unmount paths</string>
|
||||||
|
<string name="umount_path_restart_notice">A reboot is required for changes to take effect. The system will apply the new configuration on the next boot.</string>
|
||||||
|
<string name="add_umount_path">Add Umount Path</string>
|
||||||
|
<string name="mount_path">Mount Path</string>
|
||||||
|
<string name="check_mount_type">Check Mount Type</string>
|
||||||
|
<string name="check_mount_type_overlay">Check if it is an overlay type</string>
|
||||||
|
<string name="umount_flags">Unmount Flags</string>
|
||||||
|
<string name="umount_flags_hint">0=Normal unmount, 8=MNT_DETACH, -1=Auto</string>
|
||||||
|
<string name="flags">Flags</string>
|
||||||
|
<string name="default_entry">Default Entry</string>
|
||||||
|
<string name="confirm_delete">Confirm Delete</string>
|
||||||
|
<string name="confirm_delete_umount_path">Are you sure you want to delete the path %s?</string>
|
||||||
|
<string name="umount_path_added">Path added, will take effect after reboot</string>
|
||||||
|
<string name="umount_path_removed">Path removed, will take effect after reboot</string>
|
||||||
|
<string name="operation_failed">Operation failed</string>
|
||||||
|
<string name="confirm_action">Confirm Action</string>
|
||||||
|
<string name="confirm_clear_custom_paths">Are you sure you want to clear all custom paths? (Default paths will be preserved)</string>
|
||||||
|
<string name="custom_paths_cleared">Custom paths cleared</string>
|
||||||
|
<string name="clear_custom_paths">Clear Custom Paths</string>
|
||||||
|
<string name="apply_config">Apply Configuration</string>
|
||||||
|
<string name="config_applied">Configuration applied to kernel</string>
|
||||||
|
<string name="mnt_detach">MNT_DETACH</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -170,7 +170,13 @@ impl UmountManager {
|
|||||||
UmountEntry {
|
UmountEntry {
|
||||||
path: "/data/adb/modules".to_string(),
|
path: "/data/adb/modules".to_string(),
|
||||||
check_mnt: false,
|
check_mnt: false,
|
||||||
flags: -1,
|
flags: -1, // MNT_DETACH
|
||||||
|
is_default: true,
|
||||||
|
},
|
||||||
|
UmountEntry {
|
||||||
|
path: "/debug_ramdisk".to_string(),
|
||||||
|
check_mnt: false,
|
||||||
|
flags: -1, // MNT_DETACH
|
||||||
is_default: true,
|
is_default: true,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user