[skip ci]manager: Add SUS loop path function

This commit is contained in:
ShirkNeko
2025-07-21 21:14:09 +08:00
parent d2ab325e18
commit 139899d05d
8 changed files with 1250 additions and 944 deletions

View File

@@ -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<String, CachedAppInfo>()
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<String>,
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
)
}
}
}
}

View File

@@ -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<String>,
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挂载内容组件
*/

View File

@@ -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<SuSFSTab> {
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<SuSFSTab> {
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<String>()) }
var susLoopPaths by remember { mutableStateOf(emptySet<String>()) }
var susMounts by remember { mutableStateOf(emptySet<String>()) }
var tryUmounts by remember { mutableStateOf(emptySet<String>()) }
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<String?>(null) }
var editingLoopPath by remember { mutableStateOf<String?>(null) }
var editingMount by remember { mutableStateOf<String?>(null) }
var editingUmount by remember { mutableStateOf<String?>(null) }
var editingKstatConfig by remember { mutableStateOf<String?>(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() }

View File

@@ -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<String, CachedAppInfo>()
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<String>,
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
)
}
}
}
}

View File

@@ -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<String>,
val susLoopPaths: Set<String>,
val susMounts: Set<String>,
val tryUmounts: Set<String>,
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<String> =
getPrefs(context).getStringSet(KEY_SUS_PATHS, emptySet()) ?: emptySet()
// 循环路径管理
fun saveSusLoopPaths(context: Context, paths: Set<String>) =
getPrefs(context).edit { putStringSet(KEY_SUS_LOOP_PATHS, paths) }
fun getSusLoopPaths(context: Context): Set<String> =
getPrefs(context).getStringSet(KEY_SUS_LOOP_PATHS, emptySet()) ?: emptySet()
fun saveSusMounts(context: Context, mounts: Set<String>) =
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<EnabledFeature> {
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'")

View File

@@ -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<String>) {
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<String>,
@@ -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)
}
}
}