From 139899d05d0e0d6a93c0cedb22e9b19a98644c0c Mon Sep 17 00:00:00 2001 From: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com> Date: Mon, 21 Jul 2025 21:14:09 +0800 Subject: [PATCH] [skip ci]manager: Add SUS loop path function --- .../ultra/ui/component/SuSFSConfigDialogs.kt | 889 ++++++++++++++++- .../ultra/ui/component/SuSFSConfigTabs.kt | 115 ++- .../com/sukisu/ultra/ui/screen/SuSFSConfig.kt | 113 ++- .../extensions/SuSFSConfigExtensions.kt | 924 ------------------ .../com/sukisu/ultra/ui/util/SuSFSManager.kt | 88 +- .../ultra/ui/util/SuSFSModuleScripts.kt | 25 +- .../src/main/res/values-zh-rCN/strings.xml | 20 + manager/app/src/main/res/values/strings.xml | 20 + 8 files changed, 1250 insertions(+), 944 deletions(-) delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/screen/extensions/SuSFSConfigExtensions.kt diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigDialogs.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigDialogs.kt index 7869f5b3..c2e1df16 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigDialogs.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigDialogs.kt @@ -2,15 +2,22 @@ package com.sukisu.ultra.ui.component import android.annotation.SuppressLint import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable import android.util.Log import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState @@ -18,8 +25,16 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Folder +import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.RadioButtonUnchecked import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Update +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -30,9 +45,12 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -41,20 +59,26 @@ 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.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import coil.request.ImageRequest import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.sukisu.ultra.R import com.sukisu.ultra.ui.util.SuSFSManager -import com.sukisu.ultra.ui.screen.extensions.AppInfoCache +import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel +import kotlinx.coroutines.launch /** * 添加路径对话框 @@ -890,4 +914,867 @@ fun ConfirmDialog( shape = RoundedCornerShape(12.dp) ) } +} + +// 应用信息缓存 +object AppInfoCache { + private val appInfoMap = mutableMapOf() + + data class CachedAppInfo( + val appName: String, + val packageInfo: PackageInfo?, + val drawable: Drawable?, + val timestamp: Long = System.currentTimeMillis() + ) + + fun getAppInfo(packageName: String): CachedAppInfo? { + return appInfoMap[packageName] + } + + fun putAppInfo(packageName: String, appInfo: CachedAppInfo) { + appInfoMap[packageName] = appInfo + } + + fun clearCache() { + appInfoMap.clear() + } + + fun hasCache(packageName: String): Boolean { + return appInfoMap.containsKey(packageName) + } + + fun getAppInfoFromSuperUser(packageName: String): CachedAppInfo? { + val superUserApp = SuperUserViewModel.apps.find { it.packageName == packageName } + return superUserApp?.let { app -> + CachedAppInfo( + appName = app.label, + packageInfo = app.packageInfo, + drawable = null + ) + } + } +} + +/** + * 空状态显示组件 + */ +@Composable +fun EmptyStateCard( + message: String, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.2f) + ), + shape = RoundedCornerShape(12.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = message, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + } + } +} + +/** + * 路径项目卡片组件 + */ +@Composable +fun PathItemCard( + path: String, + icon: ImageVector, + onDelete: () -> Unit, + onEdit: (() -> Unit)? = null, + isLoading: Boolean = false, + additionalInfo: String? = null +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 1.dp), + shape = RoundedCornerShape(8.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f) + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Column { + Text( + text = path, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + if (additionalInfo != null) { + Text( + text = additionalInfo, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (onEdit != null) { + IconButton( + onClick = onEdit, + enabled = !isLoading, + modifier = Modifier.size(32.dp) + ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = stringResource(R.string.edit), + tint = MaterialTheme.colorScheme.secondary, + modifier = Modifier.size(16.dp) + ) + } + } + IconButton( + onClick = onDelete, + enabled = !isLoading, + modifier = Modifier.size(32.dp) + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource(R.string.delete), + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(16.dp) + ) + } + } + } + } +} + +/** + * Kstat配置项目卡片组件 + */ +@Composable +fun KstatConfigItemCard( + config: String, + onDelete: () -> Unit, + onEdit: (() -> Unit)? = null, + isLoading: Boolean = false +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 1.dp), + shape = RoundedCornerShape(8.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f) + ) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Column { + val parts = config.split("|") + if (parts.isNotEmpty()) { + Text( + text = parts[0], // 路径 + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + if (parts.size > 1) { + Text( + text = parts.drop(1).joinToString(" "), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else { + Text( + text = config, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + } + } + } + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (onEdit != null) { + IconButton( + onClick = onEdit, + enabled = !isLoading, + modifier = Modifier.size(32.dp) + ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = stringResource(R.string.edit), + tint = MaterialTheme.colorScheme.secondary, + modifier = Modifier.size(16.dp) + ) + } + } + IconButton( + onClick = onDelete, + enabled = !isLoading, + modifier = Modifier.size(32.dp) + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource(R.string.delete), + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(16.dp) + ) + } + } + } + } +} + +/** + * Add Kstat路径项目卡片组件 + */ +@Composable +fun AddKstatPathItemCard( + path: String, + onDelete: () -> Unit, + onEdit: (() -> Unit)? = null, + onUpdate: () -> Unit, + onUpdateFullClone: () -> Unit, + isLoading: Boolean = false +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 1.dp), + shape = RoundedCornerShape(8.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f) + ) { + Icon( + imageVector = Icons.Default.Folder, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = path, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (onEdit != null) { + IconButton( + onClick = onEdit, + enabled = !isLoading, + modifier = Modifier.size(32.dp) + ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = stringResource(R.string.edit), + tint = MaterialTheme.colorScheme.secondary, + modifier = Modifier.size(16.dp) + ) + } + } + IconButton( + onClick = onUpdate, + enabled = !isLoading, + modifier = Modifier.size(32.dp) + ) { + Icon( + imageVector = Icons.Default.Update, + contentDescription = stringResource(R.string.update), + tint = MaterialTheme.colorScheme.secondary, + modifier = Modifier.size(16.dp) + ) + } + IconButton( + onClick = onUpdateFullClone, + enabled = !isLoading, + modifier = Modifier.size(32.dp) + ) { + Icon( + imageVector = Icons.Default.PlayArrow, + contentDescription = stringResource(R.string.susfs_update_full_clone), + tint = MaterialTheme.colorScheme.tertiary, + modifier = Modifier.size(16.dp) + ) + } + IconButton( + onClick = onDelete, + enabled = !isLoading, + modifier = Modifier.size(32.dp) + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource(R.string.delete), + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(16.dp) + ) + } + } + } + } +} + +/** + * 启用功能状态卡片组件 + */ +@Composable +fun FeatureStatusCard( + feature: SuSFSManager.EnabledFeature, + onRefresh: (() -> Unit)? = null, + @SuppressLint("ModifierParameter") modifier: Modifier = Modifier +) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + // 日志配置对话框状态 + var showLogConfigDialog by remember { mutableStateOf(false) } + var logEnabled by remember { mutableStateOf(SuSFSManager.getEnableLogState(context)) } + + // 日志配置对话框 + if (showLogConfigDialog) { + AlertDialog( + onDismissRequest = { showLogConfigDialog = false }, + title = { + Text( + text = stringResource(R.string.susfs_log_config_title), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = stringResource(R.string.susfs_log_config_description), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.susfs_enable_log_label), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + Switch( + checked = logEnabled, + onCheckedChange = { logEnabled = it } + ) + } + } + }, + confirmButton = { + Button( + onClick = { + coroutineScope.launch { + if (SuSFSManager.setEnableLog(context, logEnabled)) { + onRefresh?.invoke() + } + showLogConfigDialog = false + } + }, + shape = RoundedCornerShape(8.dp) + ) { + Text(stringResource(R.string.susfs_apply)) + } + }, + dismissButton = { + TextButton( + onClick = { + // 恢复原始状态 + logEnabled = SuSFSManager.getEnableLogState(context) + showLogConfigDialog = false + }, + shape = RoundedCornerShape(8.dp) + ) { + Text(stringResource(R.string.cancel)) + } + }, + shape = RoundedCornerShape(12.dp) + ) + } + + Card( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 1.dp) + .then( + if (feature.canConfigure) { + Modifier.clickable { + // 更新当前状态 + logEnabled = SuSFSManager.getEnableLogState(context) + showLogConfigDialog = true + } + } else { + Modifier + } + ), + shape = RoundedCornerShape(8.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = feature.name, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + if (feature.canConfigure) { + Text( + text = stringResource(R.string.susfs_feature_configurable), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 状态标签 + Surface( + shape = RoundedCornerShape(6.dp), + color = when { + feature.isEnabled -> MaterialTheme.colorScheme.primary + else -> Color.Gray + } + ) { + Text( + text = feature.statusText, + style = MaterialTheme.typography.labelLarge, + color = when { + feature.isEnabled -> MaterialTheme.colorScheme.onPrimary + else -> Color.White + }, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp) + ) + } + } + } + } +} + +/** + * SUS挂载隐藏控制卡片组件 + */ +@Composable +fun SusMountHidingControlCard( + hideSusMountsForAllProcs: Boolean, + isLoading: Boolean, + onToggleHiding: (Boolean) -> Unit +) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ), + shape = RoundedCornerShape(12.dp) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // 标题行 + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = if (hideSusMountsForAllProcs) Icons.Default.VisibilityOff else Icons.Default.Visibility, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.susfs_hide_mounts_control_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + } + + // 描述文本 + Text( + text = stringResource(R.string.susfs_hide_mounts_control_description), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + lineHeight = 16.sp + ) + + // 控制开关行 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = stringResource(R.string.susfs_hide_mounts_for_all_procs_label), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = if (hideSusMountsForAllProcs) { + stringResource(R.string.susfs_hide_mounts_for_all_procs_enabled_description) + } else { + stringResource(R.string.susfs_hide_mounts_for_all_procs_disabled_description) + }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + lineHeight = 14.sp + ) + } + Switch( + checked = hideSusMountsForAllProcs, + onCheckedChange = onToggleHiding, + enabled = !isLoading + ) + } + + // 当前设置显示 + Text( + text = stringResource( + R.string.susfs_hide_mounts_current_setting, + if (hideSusMountsForAllProcs) { + stringResource(R.string.susfs_hide_mounts_setting_all) + } else { + stringResource(R.string.susfs_hide_mounts_setting_non_ksu) + } + ), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Medium + ) + + // 建议文本 + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = stringResource(R.string.susfs_hide_mounts_recommendation), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + lineHeight = 14.sp, + modifier = Modifier.padding(12.dp) + ) + } + } + } +} + +/** + * 应用路径分组卡片 + */ +@Composable +fun AppPathGroupCard( + packageName: String, + paths: List, + onDeleteGroup: () -> Unit, + onEditGroup: (() -> Unit)? = null, + isLoading: Boolean +) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val superUserApps = SuperUserViewModel.apps + var cachedAppInfo by remember(packageName, superUserApps.size) { + mutableStateOf(AppInfoCache.getAppInfo(packageName)) + } + var isLoadingAppInfo by remember(packageName, superUserApps.size) { mutableStateOf(false) } + + LaunchedEffect(packageName, superUserApps.size) { + if (cachedAppInfo == null || superUserApps.isNotEmpty()) { + isLoadingAppInfo = true + coroutineScope.launch { + try { + val superUserAppInfo = AppInfoCache.getAppInfoFromSuperUser(packageName) + + if (superUserAppInfo != null) { + val packageManager = context.packageManager + val drawable = try { + superUserAppInfo.packageInfo?.applicationInfo?.let { + packageManager.getApplicationIcon(it) + } + } catch (_: Exception) { + null + } + + val newCachedInfo = AppInfoCache.CachedAppInfo( + appName = superUserAppInfo.appName, + packageInfo = superUserAppInfo.packageInfo, + drawable = drawable + ) + + AppInfoCache.putAppInfo(packageName, newCachedInfo) + cachedAppInfo = newCachedInfo + } else { + val packageManager = context.packageManager + val appInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA) + + val appName = try { + appInfo.applicationInfo?.let { + packageManager.getApplicationLabel(it).toString() + } ?: packageName + } catch (_: Exception) { + packageName + } + + val drawable = try { + appInfo.applicationInfo?.let { + packageManager.getApplicationIcon(it) + } + } catch (_: Exception) { + null + } + + val newCachedInfo = AppInfoCache.CachedAppInfo( + appName = appName, + packageInfo = appInfo, + drawable = drawable + ) + + AppInfoCache.putAppInfo(packageName, newCachedInfo) + cachedAppInfo = newCachedInfo + } + } catch (_: Exception) { + val newCachedInfo = AppInfoCache.CachedAppInfo( + appName = packageName, + packageInfo = null, + drawable = null + ) + AppInfoCache.putAppInfo(packageName, newCachedInfo) + cachedAppInfo = newCachedInfo + } finally { + isLoadingAppInfo = false + } + } + } + } + + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ), + shape = RoundedCornerShape(12.dp) + ) { + Column( + modifier = Modifier.padding(12.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + // 应用图标 + AppIcon( + packageName = packageName, + packageInfo = cachedAppInfo?.packageInfo, + modifier = Modifier.size(32.dp) + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Column(modifier = Modifier.weight(1f)) { + val displayName = cachedAppInfo?.appName?.ifEmpty { packageName } ?: packageName + Text( + text = displayName, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + if (!isLoadingAppInfo && cachedAppInfo?.appName?.isNotEmpty() == true && + cachedAppInfo?.appName != packageName) { + Text( + text = packageName, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (onEditGroup != null) { + IconButton( + onClick = onEditGroup, + enabled = !isLoading + ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = stringResource(R.string.edit), + tint = MaterialTheme.colorScheme.primary + ) + } + } + IconButton( + onClick = onDeleteGroup, + enabled = !isLoading + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource(R.string.delete), + tint = MaterialTheme.colorScheme.error + ) + } + } + } + + // 显示所有路径 + Spacer(modifier = Modifier.height(8.dp)) + + paths.forEach { path -> + Text( + text = path, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .fillMaxWidth() + .background( + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), + RoundedCornerShape(6.dp) + ) + .padding(8.dp) + ) + + if (path != paths.last()) { + Spacer(modifier = Modifier.height(4.dp)) + } + } + } + } +} + +/** + * 分组标题组件 + */ +@Composable +fun SectionHeader( + title: String, + subtitle: String?, + icon: ImageVector, + count: Int +) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) + ), + shape = RoundedCornerShape(12.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + subtitle?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + Surface( + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.primary + ) { + Text( + text = count.toString(), + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onPrimary, + fontWeight = FontWeight.Bold + ) + } + } + } } \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigTabs.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigTabs.kt index f1ef90e1..1fb1864c 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigTabs.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigTabs.kt @@ -19,6 +19,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Apps import androidx.compose.material.icons.filled.Folder +import androidx.compose.material.icons.filled.Loop import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Security import androidx.compose.material.icons.filled.Settings @@ -41,15 +42,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.sukisu.ultra.R -import com.sukisu.ultra.ui.screen.extensions.AddKstatPathItemCard -import com.sukisu.ultra.ui.screen.extensions.AppInfoCache -import com.sukisu.ultra.ui.screen.extensions.AppPathGroupCard -import com.sukisu.ultra.ui.screen.extensions.EmptyStateCard -import com.sukisu.ultra.ui.screen.extensions.FeatureStatusCard -import com.sukisu.ultra.ui.screen.extensions.KstatConfigItemCard -import com.sukisu.ultra.ui.screen.extensions.PathItemCard -import com.sukisu.ultra.ui.screen.extensions.SectionHeader -import com.sukisu.ultra.ui.screen.extensions.SusMountHidingControlCard import com.sukisu.ultra.ui.util.SuSFSManager import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion_1_5_8 import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel @@ -212,6 +204,111 @@ fun SusPathsContent( } } +/** + * SUS循环路径内容组件 + */ +@Composable +fun SusLoopPathsContent( + susLoopPaths: Set, + isLoading: Boolean, + onAddLoopPath: () -> Unit, + onRemoveLoopPath: (String) -> Unit, + onEditLoopPath: ((String) -> Unit)? = null +) { + Box(modifier = Modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // 说明卡片 + item { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) + ), + shape = RoundedCornerShape(12.dp) + ) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = stringResource(R.string.sus_loop_paths_description_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = stringResource(R.string.sus_loop_paths_description_text), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = stringResource(R.string.susfs_loop_path_restriction_warning), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.secondary + ) + } + } + } + + if (susLoopPaths.isEmpty()) { + item { + EmptyStateCard( + message = stringResource(R.string.susfs_no_loop_paths_configured) + ) + } + } else { + item { + SectionHeader( + title = stringResource(R.string.loop_paths_section), + subtitle = null, + icon = Icons.Default.Loop, + count = susLoopPaths.size + ) + } + + items(susLoopPaths.toList()) { path -> + PathItemCard( + path = path, + icon = Icons.Default.Loop, + onDelete = { onRemoveLoopPath(path) }, + onEdit = if (onEditLoopPath != null) { { onEditLoopPath(path) } } else null, + isLoading = isLoading + ) + } + } + + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = onAddLoopPath, + modifier = Modifier + .weight(1f) + .height(48.dp), + shape = RoundedCornerShape(8.dp) + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = stringResource(R.string.add_loop_path)) + } + } + } + } + } +} + /** * SUS挂载内容组件 */ diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt index e78f0207..a2e96cf4 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt @@ -78,10 +78,12 @@ import com.sukisu.ultra.ui.component.KstatConfigContent import com.sukisu.ultra.ui.component.PathSettingsContent import com.sukisu.ultra.ui.component.SusMountsContent import com.sukisu.ultra.ui.component.SusPathsContent +import com.sukisu.ultra.ui.component.SusLoopPathsContent import com.sukisu.ultra.ui.component.TryUmountContent import com.sukisu.ultra.ui.theme.CardConfig import com.sukisu.ultra.ui.util.SuSFSManager import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion_1_5_8 +import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion_1_5_9 import com.sukisu.ultra.ui.util.isAbDevice import kotlinx.coroutines.launch import java.io.File @@ -94,6 +96,7 @@ import java.util.* enum class SuSFSTab(val displayNameRes: Int) { BASIC_SETTINGS(R.string.susfs_tab_basic_settings), SUS_PATHS(R.string.susfs_tab_sus_paths), + SUS_LOOP_PATHS(R.string.susfs_tab_sus_loop_paths), SUS_MOUNTS(R.string.susfs_tab_sus_mounts), TRY_UMOUNT(R.string.susfs_tab_try_umount), KSTAT_CONFIG(R.string.susfs_tab_kstat_config), @@ -101,11 +104,11 @@ enum class SuSFSTab(val displayNameRes: Int) { ENABLED_FEATURES(R.string.susfs_tab_enabled_features); companion object { - fun getAllTabs(isSusVersion_1_5_8: Boolean): List { - return if (isSusVersion_1_5_8) { - entries.toList() - } else { - entries.filter { it != PATH_SETTINGS } + fun getAllTabs(isSusVersion_1_5_8: Boolean, isSusVersion_1_5_9: Boolean): List { + return when { + isSusVersion_1_5_9 -> entries.toList() + isSusVersion_1_5_8 -> entries.filter { it != SUS_LOOP_PATHS } + else -> entries.filter { it != PATH_SETTINGS && it != SUS_LOOP_PATHS } } } } @@ -142,6 +145,7 @@ fun SuSFSConfigScreen( // 路径管理相关状态 var susPaths by remember { mutableStateOf(emptySet()) } + var susLoopPaths by remember { mutableStateOf(emptySet()) } var susMounts by remember { mutableStateOf(emptySet()) } var tryUmounts by remember { mutableStateOf(emptySet()) } var androidDataPath by remember { mutableStateOf("") } @@ -165,6 +169,7 @@ fun SuSFSConfigScreen( // 对话框状态 var showAddPathDialog by remember { mutableStateOf(false) } + var showAddLoopPathDialog by remember { mutableStateOf(false) } var showAddAppPathDialog by remember { mutableStateOf(false) } var showAddMountDialog by remember { mutableStateOf(false) } var showAddUmountDialog by remember { mutableStateOf(false) } @@ -174,6 +179,7 @@ fun SuSFSConfigScreen( // 编辑状态 var editingPath by remember { mutableStateOf(null) } + var editingLoopPath by remember { mutableStateOf(null) } var editingMount by remember { mutableStateOf(null) } var editingUmount by remember { mutableStateOf(null) } var editingKstatConfig by remember { mutableStateOf(null) } @@ -181,6 +187,7 @@ fun SuSFSConfigScreen( // 重置确认对话框状态 var showResetPathsDialog by remember { mutableStateOf(false) } + var showResetLoopPathsDialog by remember { mutableStateOf(false) } var showResetMountsDialog by remember { mutableStateOf(false) } var showResetUmountsDialog by remember { mutableStateOf(false) } var showResetKstatDialog by remember { mutableStateOf(false) } @@ -194,7 +201,7 @@ fun SuSFSConfigScreen( var isNavigating by remember { mutableStateOf(false) } - val allTabs = SuSFSTab.getAllTabs(isSusVersion_1_5_8()) + val allTabs = SuSFSTab.getAllTabs(isSusVersion_1_5_8(), isSusVersion_1_5_9()) // 实时判断是否可以启用开机自启动 val canEnableAutoStart by remember { @@ -293,6 +300,7 @@ fun SuSFSConfigScreen( autoStartEnabled = SuSFSManager.isAutoStartEnabled(context) executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context) susPaths = SuSFSManager.getSusPaths(context) + susLoopPaths = SuSFSManager.getSusLoopPaths(context) susMounts = SuSFSManager.getSusMounts(context) tryUmounts = SuSFSManager.getTryUmounts(context) androidDataPath = SuSFSManager.getAndroidDataPath(context) @@ -462,6 +470,7 @@ fun SuSFSConfigScreen( autoStartEnabled = SuSFSManager.isAutoStartEnabled(context) executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context) susPaths = SuSFSManager.getSusPaths(context) + susLoopPaths = SuSFSManager.getSusLoopPaths(context) susMounts = SuSFSManager.getSusMounts(context) tryUmounts = SuSFSManager.getTryUmounts(context) androidDataPath = SuSFSManager.getAndroidDataPath(context) @@ -550,6 +559,35 @@ fun SuSFSConfigScreen( initialValue = editingPath ?: "" ) + AddPathDialog( + showDialog = showAddLoopPathDialog, + onDismiss = { + showAddLoopPathDialog = false + editingLoopPath = null + }, + onConfirm = { path -> + coroutineScope.launch { + isLoading = true + val success = if (editingLoopPath != null) { + SuSFSManager.editSusLoopPath(context, editingLoopPath!!, path) + } else { + SuSFSManager.addSusLoopPath(context, path) + } + if (success) { + susLoopPaths = SuSFSManager.getSusLoopPaths(context) + } + isLoading = false + showAddLoopPathDialog = false + editingLoopPath = null + } + }, + isLoading = isLoading, + titleRes = if (editingLoopPath != null) R.string.susfs_edit_sus_loop_path else R.string.susfs_add_sus_loop_path, + labelRes = R.string.susfs_loop_path_label, + placeholderRes = R.string.susfs_loop_path_placeholder, + initialValue = editingLoopPath ?: "" + ) + AddAppPathDialog( showDialog = showAddAppPathDialog, onDismiss = { showAddAppPathDialog = false }, @@ -752,6 +790,27 @@ fun SuSFSConfigScreen( isDestructive = true ) + ConfirmDialog( + showDialog = showResetLoopPathsDialog, + onDismiss = { showResetLoopPathsDialog = false }, + onConfirm = { + coroutineScope.launch { + isLoading = true + SuSFSManager.saveSusLoopPaths(context, emptySet()) + susLoopPaths = emptySet() + if (SuSFSManager.isAutoStartEnabled(context)) { + SuSFSManager.configureAutoStart(context, true) + } + isLoading = false + showResetLoopPathsDialog = false + } + }, + titleRes = R.string.susfs_reset_loop_paths_title, + messageRes = R.string.susfs_reset_loop_paths_message, + isLoading = isLoading, + isDestructive = true + ) + ConfirmDialog( showDialog = showResetMountsDialog, onDismiss = { showResetMountsDialog = false }, @@ -948,6 +1007,28 @@ fun SuSFSConfigScreen( } } + SuSFSTab.SUS_LOOP_PATHS -> { + OutlinedButton( + onClick = { showResetLoopPathsDialog = true }, + enabled = !isLoading && susLoopPaths.isNotEmpty(), + shape = RoundedCornerShape(8.dp), + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + ) { + Icon( + imageVector = Icons.Default.RestoreFromTrash, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + stringResource(R.string.susfs_reset_loop_paths_title), + fontWeight = FontWeight.Medium + ) + } + } + SuSFSTab.SUS_MOUNTS -> { OutlinedButton( onClick = { showResetMountsDialog = true }, @@ -1181,6 +1262,26 @@ fun SuSFSConfigScreen( forceRefreshApps = selectedTab == SuSFSTab.SUS_PATHS ) } + SuSFSTab.SUS_LOOP_PATHS -> { + SusLoopPathsContent( + susLoopPaths = susLoopPaths, + isLoading = isLoading, + onAddLoopPath = { showAddLoopPathDialog = true }, + onRemoveLoopPath = { path -> + coroutineScope.launch { + isLoading = true + if (SuSFSManager.removeSusLoopPath(context, path)) { + susLoopPaths = SuSFSManager.getSusLoopPaths(context) + } + isLoading = false + } + }, + onEditLoopPath = { path -> + editingLoopPath = path + showAddLoopPathDialog = true + } + ) + } SuSFSTab.SUS_MOUNTS -> { val isSusVersion_1_5_8 = remember { isSusVersion_1_5_8() } diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/extensions/SuSFSConfigExtensions.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/extensions/SuSFSConfigExtensions.kt deleted file mode 100644 index 014edb89..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/extensions/SuSFSConfigExtensions.kt +++ /dev/null @@ -1,924 +0,0 @@ -package com.sukisu.ultra.ui.screen.extensions - -import android.annotation.SuppressLint -import android.content.pm.PackageInfo -import android.content.pm.PackageManager -import android.graphics.drawable.Drawable -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.filled.Folder -import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material.icons.filled.Update -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material.icons.filled.Visibility -import androidx.compose.material.icons.filled.VisibilityOff -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -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.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.component.AppIcon -import com.sukisu.ultra.ui.util.SuSFSManager -import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel -import kotlinx.coroutines.launch - -// 应用信息缓存 -object AppInfoCache { - private val appInfoMap = mutableMapOf() - - data class CachedAppInfo( - val appName: String, - val packageInfo: PackageInfo?, - val drawable: Drawable?, - val timestamp: Long = System.currentTimeMillis() - ) - - fun getAppInfo(packageName: String): CachedAppInfo? { - return appInfoMap[packageName] - } - - fun putAppInfo(packageName: String, appInfo: CachedAppInfo) { - appInfoMap[packageName] = appInfo - } - - fun clearCache() { - appInfoMap.clear() - } - - fun hasCache(packageName: String): Boolean { - return appInfoMap.containsKey(packageName) - } - - fun getAppInfoFromSuperUser(packageName: String): CachedAppInfo? { - val superUserApp = SuperUserViewModel.apps.find { it.packageName == packageName } - return superUserApp?.let { app -> - CachedAppInfo( - appName = app.label, - packageInfo = app.packageInfo, - drawable = null - ) - } - } -} - -/** - * 空状态显示组件 - */ -@Composable -fun EmptyStateCard( - message: String, - modifier: Modifier = Modifier -) { - Card( - modifier = modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.2f) - ), - shape = RoundedCornerShape(12.dp) - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = message, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - } - } -} - -/** - * 路径项目卡片组件 - */ -@Composable -fun PathItemCard( - path: String, - icon: ImageVector, - onDelete: () -> Unit, - onEdit: (() -> Unit)? = null, - isLoading: Boolean = false, - additionalInfo: String? = null -) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 1.dp), - shape = RoundedCornerShape(8.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.weight(1f) - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - Column { - Text( - text = path, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium - ) - if (additionalInfo != null) { - Text( - text = additionalInfo, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - if (onEdit != null) { - IconButton( - onClick = onEdit, - enabled = !isLoading, - modifier = Modifier.size(32.dp) - ) { - Icon( - imageVector = Icons.Default.Edit, - contentDescription = stringResource(R.string.edit), - tint = MaterialTheme.colorScheme.secondary, - modifier = Modifier.size(16.dp) - ) - } - } - IconButton( - onClick = onDelete, - enabled = !isLoading, - modifier = Modifier.size(32.dp) - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = stringResource(R.string.delete), - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(16.dp) - ) - } - } - } - } -} - -/** - * Kstat配置项目卡片组件 - */ -@Composable -fun KstatConfigItemCard( - config: String, - onDelete: () -> Unit, - onEdit: (() -> Unit)? = null, - isLoading: Boolean = false -) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 1.dp), - shape = RoundedCornerShape(8.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.weight(1f) - ) { - Icon( - imageVector = Icons.Default.Settings, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - Column { - val parts = config.split("|") - if (parts.isNotEmpty()) { - Text( - text = parts[0], // 路径 - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium - ) - if (parts.size > 1) { - Text( - text = parts.drop(1).joinToString(" "), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } else { - Text( - text = config, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium - ) - } - } - } - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - if (onEdit != null) { - IconButton( - onClick = onEdit, - enabled = !isLoading, - modifier = Modifier.size(32.dp) - ) { - Icon( - imageVector = Icons.Default.Edit, - contentDescription = stringResource(R.string.edit), - tint = MaterialTheme.colorScheme.secondary, - modifier = Modifier.size(16.dp) - ) - } - } - IconButton( - onClick = onDelete, - enabled = !isLoading, - modifier = Modifier.size(32.dp) - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = stringResource(R.string.delete), - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(16.dp) - ) - } - } - } - } -} - -/** - * Add Kstat路径项目卡片组件 - */ -@Composable -fun AddKstatPathItemCard( - path: String, - onDelete: () -> Unit, - onEdit: (() -> Unit)? = null, - onUpdate: () -> Unit, - onUpdateFullClone: () -> Unit, - isLoading: Boolean = false -) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 1.dp), - shape = RoundedCornerShape(8.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.weight(1f) - ) { - Icon( - imageVector = Icons.Default.Folder, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - Text( - text = path, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium - ) - } - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - if (onEdit != null) { - IconButton( - onClick = onEdit, - enabled = !isLoading, - modifier = Modifier.size(32.dp) - ) { - Icon( - imageVector = Icons.Default.Edit, - contentDescription = stringResource(R.string.edit), - tint = MaterialTheme.colorScheme.secondary, - modifier = Modifier.size(16.dp) - ) - } - } - IconButton( - onClick = onUpdate, - enabled = !isLoading, - modifier = Modifier.size(32.dp) - ) { - Icon( - imageVector = Icons.Default.Update, - contentDescription = stringResource(R.string.update), - tint = MaterialTheme.colorScheme.secondary, - modifier = Modifier.size(16.dp) - ) - } - IconButton( - onClick = onUpdateFullClone, - enabled = !isLoading, - modifier = Modifier.size(32.dp) - ) { - Icon( - imageVector = Icons.Default.PlayArrow, - contentDescription = stringResource(R.string.susfs_update_full_clone), - tint = MaterialTheme.colorScheme.tertiary, - modifier = Modifier.size(16.dp) - ) - } - IconButton( - onClick = onDelete, - enabled = !isLoading, - modifier = Modifier.size(32.dp) - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = stringResource(R.string.delete), - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(16.dp) - ) - } - } - } - } -} - -/** - * 启用功能状态卡片组件 - */ -@Composable -fun FeatureStatusCard( - feature: SuSFSManager.EnabledFeature, - onRefresh: (() -> Unit)? = null, - @SuppressLint("ModifierParameter") modifier: Modifier = Modifier -) { - val context = LocalContext.current - val coroutineScope = rememberCoroutineScope() - - // 日志配置对话框状态 - var showLogConfigDialog by remember { mutableStateOf(false) } - var logEnabled by remember { mutableStateOf(SuSFSManager.getEnableLogState(context)) } - - // 日志配置对话框 - if (showLogConfigDialog) { - AlertDialog( - onDismissRequest = { showLogConfigDialog = false }, - title = { - Text( - text = stringResource(R.string.susfs_log_config_title), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - }, - text = { - Column( - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Text( - text = stringResource(R.string.susfs_log_config_description), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(R.string.susfs_enable_log_label), - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium - ) - Switch( - checked = logEnabled, - onCheckedChange = { logEnabled = it } - ) - } - } - }, - confirmButton = { - Button( - onClick = { - coroutineScope.launch { - if (SuSFSManager.setEnableLog(context, logEnabled)) { - onRefresh?.invoke() - } - showLogConfigDialog = false - } - }, - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(R.string.susfs_apply)) - } - }, - dismissButton = { - TextButton( - onClick = { - // 恢复原始状态 - logEnabled = SuSFSManager.getEnableLogState(context) - showLogConfigDialog = false - }, - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(R.string.cancel)) - } - }, - shape = RoundedCornerShape(12.dp) - ) - } - - Card( - modifier = modifier - .fillMaxWidth() - .padding(vertical = 1.dp) - .then( - if (feature.canConfigure) { - Modifier.clickable { - // 更新当前状态 - logEnabled = SuSFSManager.getEnableLogState(context) - showLogConfigDialog = true - } - } else { - Modifier - } - ), - shape = RoundedCornerShape(8.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = feature.name, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium - ) - if (feature.canConfigure) { - Text( - text = stringResource(R.string.susfs_feature_configurable), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - // 状态标签 - Surface( - shape = RoundedCornerShape(6.dp), - color = when { - feature.isEnabled -> MaterialTheme.colorScheme.primary - else -> Color.Gray - } - ) { - Text( - text = feature.statusText, - style = MaterialTheme.typography.labelLarge, - color = when { - feature.isEnabled -> MaterialTheme.colorScheme.onPrimary - else -> Color.White - }, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp) - ) - } - } - } - } -} - -/** - * SUS挂载隐藏控制卡片组件 - */ -@Composable -fun SusMountHidingControlCard( - hideSusMountsForAllProcs: Boolean, - isLoading: Boolean, - onToggleHiding: (Boolean) -> Unit -) { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - shape = RoundedCornerShape(12.dp) - ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - // 标题行 - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = if (hideSusMountsForAllProcs) Icons.Default.VisibilityOff else Icons.Default.Visibility, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.susfs_hide_mounts_control_title), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface - ) - } - - // 描述文本 - Text( - text = stringResource(R.string.susfs_hide_mounts_control_description), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = 16.sp - ) - - // 控制开关行 - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = stringResource(R.string.susfs_hide_mounts_for_all_procs_label), - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = if (hideSusMountsForAllProcs) { - stringResource(R.string.susfs_hide_mounts_for_all_procs_enabled_description) - } else { - stringResource(R.string.susfs_hide_mounts_for_all_procs_disabled_description) - }, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = 14.sp - ) - } - Switch( - checked = hideSusMountsForAllProcs, - onCheckedChange = onToggleHiding, - enabled = !isLoading - ) - } - - // 当前设置显示 - Text( - text = stringResource( - R.string.susfs_hide_mounts_current_setting, - if (hideSusMountsForAllProcs) { - stringResource(R.string.susfs_hide_mounts_setting_all) - } else { - stringResource(R.string.susfs_hide_mounts_setting_non_ksu) - } - ), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Medium - ) - - // 建议文本 - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) - ), - shape = RoundedCornerShape(8.dp) - ) { - Text( - text = stringResource(R.string.susfs_hide_mounts_recommendation), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = 14.sp, - modifier = Modifier.padding(12.dp) - ) - } - } - } -} - -/** - * 应用路径分组卡片 - */ -@Composable -fun AppPathGroupCard( - packageName: String, - paths: List, - onDeleteGroup: () -> Unit, - onEditGroup: (() -> Unit)? = null, - isLoading: Boolean -) { - val context = LocalContext.current - val coroutineScope = rememberCoroutineScope() - val superUserApps = SuperUserViewModel.apps - var cachedAppInfo by remember(packageName, superUserApps.size) { - mutableStateOf(AppInfoCache.getAppInfo(packageName)) - } - var isLoadingAppInfo by remember(packageName, superUserApps.size) { mutableStateOf(false) } - - LaunchedEffect(packageName, superUserApps.size) { - if (cachedAppInfo == null || superUserApps.isNotEmpty()) { - isLoadingAppInfo = true - coroutineScope.launch { - try { - val superUserAppInfo = AppInfoCache.getAppInfoFromSuperUser(packageName) - - if (superUserAppInfo != null) { - val packageManager = context.packageManager - val drawable = try { - superUserAppInfo.packageInfo?.applicationInfo?.let { - packageManager.getApplicationIcon(it) - } - } catch (_: Exception) { - null - } - - val newCachedInfo = AppInfoCache.CachedAppInfo( - appName = superUserAppInfo.appName, - packageInfo = superUserAppInfo.packageInfo, - drawable = drawable - ) - - AppInfoCache.putAppInfo(packageName, newCachedInfo) - cachedAppInfo = newCachedInfo - } else { - val packageManager = context.packageManager - val appInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA) - - val appName = try { - appInfo.applicationInfo?.let { - packageManager.getApplicationLabel(it).toString() - } ?: packageName - } catch (_: Exception) { - packageName - } - - val drawable = try { - appInfo.applicationInfo?.let { - packageManager.getApplicationIcon(it) - } - } catch (_: Exception) { - null - } - - val newCachedInfo = AppInfoCache.CachedAppInfo( - appName = appName, - packageInfo = appInfo, - drawable = drawable - ) - - AppInfoCache.putAppInfo(packageName, newCachedInfo) - cachedAppInfo = newCachedInfo - } - } catch (_: Exception) { - val newCachedInfo = AppInfoCache.CachedAppInfo( - appName = packageName, - packageInfo = null, - drawable = null - ) - AppInfoCache.putAppInfo(packageName, newCachedInfo) - cachedAppInfo = newCachedInfo - } finally { - isLoadingAppInfo = false - } - } - } - } - - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - shape = RoundedCornerShape(12.dp) - ) { - Column( - modifier = Modifier.padding(12.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - // 应用图标 - AppIcon( - packageName = packageName, - packageInfo = cachedAppInfo?.packageInfo, - modifier = Modifier.size(32.dp) - ) - - Spacer(modifier = Modifier.width(12.dp)) - - Column(modifier = Modifier.weight(1f)) { - val displayName = cachedAppInfo?.appName?.ifEmpty { packageName } ?: packageName - Text( - text = displayName, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - if (!isLoadingAppInfo && cachedAppInfo?.appName?.isNotEmpty() == true && - cachedAppInfo?.appName != packageName) { - Text( - text = packageName, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - if (onEditGroup != null) { - IconButton( - onClick = onEditGroup, - enabled = !isLoading - ) { - Icon( - imageVector = Icons.Default.Edit, - contentDescription = stringResource(R.string.edit), - tint = MaterialTheme.colorScheme.primary - ) - } - } - IconButton( - onClick = onDeleteGroup, - enabled = !isLoading - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = stringResource(R.string.delete), - tint = MaterialTheme.colorScheme.error - ) - } - } - } - - // 显示所有路径 - Spacer(modifier = Modifier.height(8.dp)) - - paths.forEach { path -> - Text( - text = path, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .fillMaxWidth() - .background( - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), - RoundedCornerShape(6.dp) - ) - .padding(8.dp) - ) - - if (path != paths.last()) { - Spacer(modifier = Modifier.height(4.dp)) - } - } - } - } -} - -/** - * 分组标题组件 - */ -@Composable -fun SectionHeader( - title: String, - subtitle: String?, - icon: ImageVector, - count: Int -) { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) - ), - shape = RoundedCornerShape(12.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface - ) - subtitle?.let { - Text( - text = it, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - Surface( - shape = RoundedCornerShape(16.dp), - color = MaterialTheme.colorScheme.primary - ) { - Text( - text = count.toString(), - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onPrimary, - fontWeight = FontWeight.Bold - ) - } - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt index 394fbea1..95ccfa3a 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt @@ -36,6 +36,7 @@ object SuSFSManager { private const val KEY_BUILD_TIME_VALUE = "build_time_value" private const val KEY_AUTO_START_ENABLED = "auto_start_enabled" private const val KEY_SUS_PATHS = "sus_paths" + private const val KEY_SUS_LOOP_PATHS = "sus_loop_paths" private const val KEY_SUS_MOUNTS = "sus_mounts" private const val KEY_TRY_UMOUNTS = "try_umounts" private const val KEY_ANDROID_DATA_PATH = "android_data_path" @@ -57,6 +58,7 @@ object SuSFSManager { private const val MODULE_ID = "susfs_manager" private const val MODULE_PATH = "/data/adb/modules/$MODULE_ID" private const val MIN_VERSION_FOR_HIDE_MOUNT = "1.5.8" + private const val MIN_VERSION_FOR_LOOP_PATH = "1.5.9" private const val BACKUP_FILE_EXTENSION = ".susfs_backup" private const val MEDIA_DATA_PATH = "/data/media/0/Android/data" @@ -142,6 +144,7 @@ object SuSFSManager { val buildTimeValue: String, val executeInPostFsData: Boolean, val susPaths: Set, + val susLoopPaths: Set, val susMounts: Set, val tryUmounts: Set, val androidDataPath: String, @@ -162,6 +165,7 @@ object SuSFSManager { return unameValue != DEFAULT_UNAME || buildTimeValue != DEFAULT_BUILD_TIME || susPaths.isNotEmpty() || + susLoopPaths.isNotEmpty() || susMounts.isNotEmpty() || tryUmounts.isNotEmpty() || kstatConfigs.isNotEmpty() || @@ -216,14 +220,26 @@ object SuSFSManager { } /** - * 版本检查方法 + * 检查是否支持设置sdcard路径等功能(1.5.8+) */ fun isSusVersion_1_5_8(): Boolean { return try { val currentVersion = getSuSFSVersion() compareVersions(currentVersion, MIN_VERSION_FOR_HIDE_MOUNT) >= 0 } catch (_: Exception) { - true // 默认支持新功能 + true + } + } + + /** + * 检查是否支持循环路径功能(1.5.9+) + */ + fun isSusVersion_1_5_9(): Boolean { + return try { + val currentVersion = getSuSFSVersion() + compareVersions(currentVersion, MIN_VERSION_FOR_LOOP_PATH) >= 0 + } catch (_: Exception) { + true } } @@ -237,6 +253,7 @@ object SuSFSManager { buildTimeValue = getBuildTimeValue(context), executeInPostFsData = getExecuteInPostFsData(context), susPaths = getSusPaths(context), + susLoopPaths = getSusLoopPaths(context), susMounts = getSusMounts(context), tryUmounts = getTryUmounts(context), androidDataPath = getAndroidDataPath(context), @@ -326,6 +343,13 @@ object SuSFSManager { fun getSusPaths(context: Context): Set = getPrefs(context).getStringSet(KEY_SUS_PATHS, emptySet()) ?: emptySet() + // 循环路径管理 + fun saveSusLoopPaths(context: Context, paths: Set) = + getPrefs(context).edit { putStringSet(KEY_SUS_LOOP_PATHS, paths) } + + fun getSusLoopPaths(context: Context): Set = + getPrefs(context).getStringSet(KEY_SUS_LOOP_PATHS, emptySet()) ?: emptySet() + fun saveSusMounts(context: Context, mounts: Set) = getPrefs(context).edit { putStringSet(KEY_SUS_MOUNTS, mounts) } @@ -465,6 +489,7 @@ object SuSFSManager { KEY_BUILD_TIME_VALUE to getBuildTimeValue(context), KEY_AUTO_START_ENABLED to isAutoStartEnabled(context), KEY_SUS_PATHS to getSusPaths(context), + KEY_SUS_LOOP_PATHS to getSusLoopPaths(context), KEY_SUS_MOUNTS to getSusMounts(context), KEY_TRY_UMOUNTS to getTryUmounts(context), KEY_ANDROID_DATA_PATH to getAndroidDataPath(context), @@ -771,6 +796,7 @@ object SuSFSManager { private fun getDefaultDisabledFeatures(context: Context): List { val defaultFeatures = listOf( "sus_path_feature_label" to context.getString(R.string.sus_path_feature_label), + "sus_loop_path_feature_label" to context.getString(R.string.sus_loop_path_feature_label), "sus_mount_feature_label" to context.getString(R.string.sus_mount_feature_label), "try_umount_feature_label" to context.getString(R.string.try_umount_feature_label), "spoof_uname_feature_label" to context.getString(R.string.spoof_uname_feature_label), @@ -930,6 +956,64 @@ object SuSFSManager { return false } + // 循环路径相关方法 + @SuppressLint("SdCardPath") + private fun isValidLoopPath(path: String): Boolean { + return !path.startsWith("/storage/") && !path.startsWith("/sdcard/") + } + + @SuppressLint("StringFormatInvalid") + suspend fun addSusLoopPath(context: Context, path: String): Boolean { + // 检查路径是否有效 + if (!isValidLoopPath(path)) { + showToast(context, context.getString(R.string.susfs_loop_path_invalid_location)) + return false + } + + // 执行添加循环路径命令 + val result = executeSusfsCommandWithOutput(context, "add_sus_path_loop '$path'") + val isActuallySuccessful = result.isSuccess && !result.output.contains("not found, skip adding") + + if (isActuallySuccessful) { + saveSusLoopPaths(context, getSusLoopPaths(context) + path) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, context.getString(R.string.susfs_loop_path_added_success, path)) + } else { + val errorMessage = if (result.output.contains("not found, skip adding")) { + context.getString(R.string.susfs_path_not_found_error, path) + } else { + "${context.getString(R.string.susfs_command_failed)}\n${result.output}\n${result.errorOutput}" + } + showToast(context, errorMessage) + } + return isActuallySuccessful + } + + suspend fun removeSusLoopPath(context: Context, path: String): Boolean { + saveSusLoopPaths(context, getSusLoopPaths(context) - path) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, context.getString(R.string.susfs_loop_path_removed, path)) + return true + } + + suspend fun editSusLoopPath(context: Context, oldPath: String, newPath: String): Boolean { + // 检查新路径是否有效 + if (!isValidLoopPath(newPath)) { + showToast(context, context.getString(R.string.susfs_loop_path_invalid_location)) + return false + } + + val currentPaths = getSusLoopPaths(context).toMutableSet() + if (currentPaths.remove(oldPath)) { + currentPaths.add(newPath) + saveSusLoopPaths(context, currentPaths) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, context.getString(R.string.susfs_loop_path_updated, oldPath, newPath)) + return true + } + return false + } + // 添加SUS挂载 suspend fun addSusMount(context: Context, mount: String): Boolean { val success = executeSusfsCommand(context, "add_sus_mount '$mount'") diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSModuleScripts.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSModuleScripts.kt index 6f979f0f..5d511084 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSModuleScripts.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSModuleScripts.kt @@ -103,6 +103,7 @@ object ScriptGenerator { */ private fun shouldConfigureInService(config: SuSFSManager.ModuleConfig): Boolean { return config.susPaths.isNotEmpty() || + config.susLoopPaths.isNotEmpty() || config.kstatConfigs.isNotEmpty() || config.addKstatPaths.isNotEmpty() || (!config.executeInPostFsData && (config.unameValue != DEFAULT_UNAME || config.buildTimeValue != DEFAULT_BUILD_TIME)) @@ -127,6 +128,17 @@ object ScriptGenerator { } } + private fun StringBuilder.generateSusLoopPathsSection(susLoopPaths: Set) { + if (susLoopPaths.isNotEmpty()) { + appendLine("# 添加SUS循环路径") + susLoopPaths.forEach { path -> + appendLine("\"${'$'}SUSFS_BIN\" add_sus_path_loop '$path'") + appendLine("echo \"$(get_current_time): 添加SUS循环路径: $path\" >> \"${'$'}LOG_FILE\"") + } + appendLine() + } + } + @SuppressLint("SdCardPath") private fun StringBuilder.generateKstatSection( kstatConfigs: Set, @@ -461,10 +473,19 @@ object ScriptGenerator { appendLine() // 路径设置和SUS路径设置 - if (config.susPaths.isNotEmpty()) { + if (config.susPaths.isNotEmpty() || config.susLoopPaths.isNotEmpty()) { generatePathSettingSection(config.androidDataPath, config.sdcardPath) appendLine() - generateSusPathsSection(config.susPaths) + + // 添加普通SUS路径 + if (config.susPaths.isNotEmpty()) { + generateSusPathsSection(config.susPaths) + } + + // 添加循环SUS路径 + if (config.susLoopPaths.isNotEmpty()) { + generateSusLoopPathsSection(config.susLoopPaths) + } } } 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 f57a1ce2..f56db87b 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -586,4 +586,24 @@ 活跃管理器 无活跃管理器 Zygisk 实现 + + SUS循环路径 + 添加SUS循环路径 + 编辑SUS循环路径 + SUS循环路径添加成功: %1$s + SUS循环路径已移除: %1$s + SUS循环路径已更新: %1$s -> %2$s + 未配置SUS循环路径 + 重置循环路径 + 确定要清空所有SUS循环路径吗?此操作无法撤销。 + 循环路径 + /data/example/path + 注意:只有不在/storage/和/sdcard/内的路径才能通过循环路径添加。 + 错误:循环路径不能位于/storage/或/sdcard/目录内 + 循环路径 + 添加循环路径 + + SUS循环路径 + 循环路径配置 + 循环路径会在每次非root用户应用或隔离服务启动时重新标记为SUS_PATH。这有助于解决添加的路径可能因inode状态重置或内核中inode重新创建而失效的问题 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 20b7cfcf..5ae37bad 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -589,4 +589,24 @@ No active manager SukiSU Zygisk implement + + SUS Loop Paths + Add SUS Loop Path + Edit SUS Loop Path + SUS loop path added successfully: %1$s + SUS loop path removed: %1$s + SUS loop path updated: %1$s -> %2$s + No SUS loop paths configured + Reset Loop Paths + Are you sure you want to clear all SUS loop paths? This action cannot be undone. + Loop Path + /data/example/path + Note: Only paths NOT inside /storage/ and /sdcard/ can be added via loop paths. + Error: Loop paths cannot be inside /storage/ or /sdcard/ directories + Loop Paths + Add Loop Path + + SUS Loop Path + Loop Path Configuration + Loop paths are re-flagged as SUS_PATH on each non-root user app or isolated service startup. This helps address issues where added paths may have their inode status reset or inode re-created in the kernel.