[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.annotation.SuppressLint
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.util.Log import android.util.Log
import androidx.compose.foundation.Image 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.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
@@ -18,8 +25,16 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle 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.RadioButtonUnchecked
import androidx.compose.material.icons.filled.Search 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.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
@@ -30,9 +45,12 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -41,20 +59,26 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip 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.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight 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.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.request.ImageRequest import coil.request.ImageRequest
import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.google.accompanist.drawablepainter.rememberDrawablePainter
import com.sukisu.ultra.R import com.sukisu.ultra.R
import com.sukisu.ultra.ui.util.SuSFSManager 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
/** /**
* 添加路径对话框 * 添加路径对话框
@@ -891,3 +915,866 @@ fun ConfirmDialog(
) )
} }
} }
// 应用信息缓存
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.Add
import androidx.compose.material.icons.filled.Apps import androidx.compose.material.icons.filled.Apps
import androidx.compose.material.icons.filled.Folder 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.PlayArrow
import androidx.compose.material.icons.filled.Security import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.Settings 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.sukisu.ultra.R 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
import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion_1_5_8 import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion_1_5_8
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel 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挂载内容组件 * 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.PathSettingsContent
import com.sukisu.ultra.ui.component.SusMountsContent import com.sukisu.ultra.ui.component.SusMountsContent
import com.sukisu.ultra.ui.component.SusPathsContent 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.component.TryUmountContent
import com.sukisu.ultra.ui.theme.CardConfig import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.util.SuSFSManager 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_8
import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion_1_5_9
import com.sukisu.ultra.ui.util.isAbDevice import com.sukisu.ultra.ui.util.isAbDevice
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
@@ -94,6 +96,7 @@ import java.util.*
enum class SuSFSTab(val displayNameRes: Int) { enum class SuSFSTab(val displayNameRes: Int) {
BASIC_SETTINGS(R.string.susfs_tab_basic_settings), BASIC_SETTINGS(R.string.susfs_tab_basic_settings),
SUS_PATHS(R.string.susfs_tab_sus_paths), 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), SUS_MOUNTS(R.string.susfs_tab_sus_mounts),
TRY_UMOUNT(R.string.susfs_tab_try_umount), TRY_UMOUNT(R.string.susfs_tab_try_umount),
KSTAT_CONFIG(R.string.susfs_tab_kstat_config), 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); ENABLED_FEATURES(R.string.susfs_tab_enabled_features);
companion object { companion object {
fun getAllTabs(isSusVersion_1_5_8: Boolean): List<SuSFSTab> { fun getAllTabs(isSusVersion_1_5_8: Boolean, isSusVersion_1_5_9: Boolean): List<SuSFSTab> {
return if (isSusVersion_1_5_8) { return when {
entries.toList() isSusVersion_1_5_9 -> entries.toList()
} else { isSusVersion_1_5_8 -> entries.filter { it != SUS_LOOP_PATHS }
entries.filter { it != PATH_SETTINGS } else -> entries.filter { it != PATH_SETTINGS && it != SUS_LOOP_PATHS }
} }
} }
} }
@@ -142,6 +145,7 @@ fun SuSFSConfigScreen(
// 路径管理相关状态 // 路径管理相关状态
var susPaths by remember { mutableStateOf(emptySet<String>()) } var susPaths by remember { mutableStateOf(emptySet<String>()) }
var susLoopPaths by remember { mutableStateOf(emptySet<String>()) }
var susMounts by remember { mutableStateOf(emptySet<String>()) } var susMounts by remember { mutableStateOf(emptySet<String>()) }
var tryUmounts by remember { mutableStateOf(emptySet<String>()) } var tryUmounts by remember { mutableStateOf(emptySet<String>()) }
var androidDataPath by remember { mutableStateOf("") } var androidDataPath by remember { mutableStateOf("") }
@@ -165,6 +169,7 @@ fun SuSFSConfigScreen(
// 对话框状态 // 对话框状态
var showAddPathDialog by remember { mutableStateOf(false) } var showAddPathDialog by remember { mutableStateOf(false) }
var showAddLoopPathDialog by remember { mutableStateOf(false) }
var showAddAppPathDialog by remember { mutableStateOf(false) } var showAddAppPathDialog by remember { mutableStateOf(false) }
var showAddMountDialog by remember { mutableStateOf(false) } var showAddMountDialog by remember { mutableStateOf(false) }
var showAddUmountDialog by remember { mutableStateOf(false) } var showAddUmountDialog by remember { mutableStateOf(false) }
@@ -174,6 +179,7 @@ fun SuSFSConfigScreen(
// 编辑状态 // 编辑状态
var editingPath by remember { mutableStateOf<String?>(null) } var editingPath by remember { mutableStateOf<String?>(null) }
var editingLoopPath by remember { mutableStateOf<String?>(null) }
var editingMount by remember { mutableStateOf<String?>(null) } var editingMount by remember { mutableStateOf<String?>(null) }
var editingUmount by remember { mutableStateOf<String?>(null) } var editingUmount by remember { mutableStateOf<String?>(null) }
var editingKstatConfig 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 showResetPathsDialog by remember { mutableStateOf(false) }
var showResetLoopPathsDialog by remember { mutableStateOf(false) }
var showResetMountsDialog by remember { mutableStateOf(false) } var showResetMountsDialog by remember { mutableStateOf(false) }
var showResetUmountsDialog by remember { mutableStateOf(false) } var showResetUmountsDialog by remember { mutableStateOf(false) }
var showResetKstatDialog by remember { mutableStateOf(false) } var showResetKstatDialog by remember { mutableStateOf(false) }
@@ -194,7 +201,7 @@ fun SuSFSConfigScreen(
var isNavigating by remember { mutableStateOf(false) } 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 { val canEnableAutoStart by remember {
@@ -293,6 +300,7 @@ fun SuSFSConfigScreen(
autoStartEnabled = SuSFSManager.isAutoStartEnabled(context) autoStartEnabled = SuSFSManager.isAutoStartEnabled(context)
executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context) executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context)
susPaths = SuSFSManager.getSusPaths(context) susPaths = SuSFSManager.getSusPaths(context)
susLoopPaths = SuSFSManager.getSusLoopPaths(context)
susMounts = SuSFSManager.getSusMounts(context) susMounts = SuSFSManager.getSusMounts(context)
tryUmounts = SuSFSManager.getTryUmounts(context) tryUmounts = SuSFSManager.getTryUmounts(context)
androidDataPath = SuSFSManager.getAndroidDataPath(context) androidDataPath = SuSFSManager.getAndroidDataPath(context)
@@ -462,6 +470,7 @@ fun SuSFSConfigScreen(
autoStartEnabled = SuSFSManager.isAutoStartEnabled(context) autoStartEnabled = SuSFSManager.isAutoStartEnabled(context)
executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context) executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context)
susPaths = SuSFSManager.getSusPaths(context) susPaths = SuSFSManager.getSusPaths(context)
susLoopPaths = SuSFSManager.getSusLoopPaths(context)
susMounts = SuSFSManager.getSusMounts(context) susMounts = SuSFSManager.getSusMounts(context)
tryUmounts = SuSFSManager.getTryUmounts(context) tryUmounts = SuSFSManager.getTryUmounts(context)
androidDataPath = SuSFSManager.getAndroidDataPath(context) androidDataPath = SuSFSManager.getAndroidDataPath(context)
@@ -550,6 +559,35 @@ fun SuSFSConfigScreen(
initialValue = editingPath ?: "" 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( AddAppPathDialog(
showDialog = showAddAppPathDialog, showDialog = showAddAppPathDialog,
onDismiss = { showAddAppPathDialog = false }, onDismiss = { showAddAppPathDialog = false },
@@ -752,6 +790,27 @@ fun SuSFSConfigScreen(
isDestructive = true 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( ConfirmDialog(
showDialog = showResetMountsDialog, showDialog = showResetMountsDialog,
onDismiss = { showResetMountsDialog = false }, 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 -> { SuSFSTab.SUS_MOUNTS -> {
OutlinedButton( OutlinedButton(
onClick = { showResetMountsDialog = true }, onClick = { showResetMountsDialog = true },
@@ -1181,6 +1262,26 @@ fun SuSFSConfigScreen(
forceRefreshApps = selectedTab == SuSFSTab.SUS_PATHS 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 -> { SuSFSTab.SUS_MOUNTS -> {
val isSusVersion_1_5_8 = remember { isSusVersion_1_5_8() } 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_BUILD_TIME_VALUE = "build_time_value"
private const val KEY_AUTO_START_ENABLED = "auto_start_enabled" private const val KEY_AUTO_START_ENABLED = "auto_start_enabled"
private const val KEY_SUS_PATHS = "sus_paths" 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_SUS_MOUNTS = "sus_mounts"
private const val KEY_TRY_UMOUNTS = "try_umounts" private const val KEY_TRY_UMOUNTS = "try_umounts"
private const val KEY_ANDROID_DATA_PATH = "android_data_path" 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_ID = "susfs_manager"
private const val MODULE_PATH = "/data/adb/modules/$MODULE_ID" 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_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 BACKUP_FILE_EXTENSION = ".susfs_backup"
private const val MEDIA_DATA_PATH = "/data/media/0/Android/data" private const val MEDIA_DATA_PATH = "/data/media/0/Android/data"
@@ -142,6 +144,7 @@ object SuSFSManager {
val buildTimeValue: String, val buildTimeValue: String,
val executeInPostFsData: Boolean, val executeInPostFsData: Boolean,
val susPaths: Set<String>, val susPaths: Set<String>,
val susLoopPaths: Set<String>,
val susMounts: Set<String>, val susMounts: Set<String>,
val tryUmounts: Set<String>, val tryUmounts: Set<String>,
val androidDataPath: String, val androidDataPath: String,
@@ -162,6 +165,7 @@ object SuSFSManager {
return unameValue != DEFAULT_UNAME || return unameValue != DEFAULT_UNAME ||
buildTimeValue != DEFAULT_BUILD_TIME || buildTimeValue != DEFAULT_BUILD_TIME ||
susPaths.isNotEmpty() || susPaths.isNotEmpty() ||
susLoopPaths.isNotEmpty() ||
susMounts.isNotEmpty() || susMounts.isNotEmpty() ||
tryUmounts.isNotEmpty() || tryUmounts.isNotEmpty() ||
kstatConfigs.isNotEmpty() || kstatConfigs.isNotEmpty() ||
@@ -216,14 +220,26 @@ object SuSFSManager {
} }
/** /**
* 版本检查方法 * 检查是否支持设置sdcard路径等功能1.5.8+
*/ */
fun isSusVersion_1_5_8(): Boolean { fun isSusVersion_1_5_8(): Boolean {
return try { return try {
val currentVersion = getSuSFSVersion() val currentVersion = getSuSFSVersion()
compareVersions(currentVersion, MIN_VERSION_FOR_HIDE_MOUNT) >= 0 compareVersions(currentVersion, MIN_VERSION_FOR_HIDE_MOUNT) >= 0
} catch (_: Exception) { } 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), buildTimeValue = getBuildTimeValue(context),
executeInPostFsData = getExecuteInPostFsData(context), executeInPostFsData = getExecuteInPostFsData(context),
susPaths = getSusPaths(context), susPaths = getSusPaths(context),
susLoopPaths = getSusLoopPaths(context),
susMounts = getSusMounts(context), susMounts = getSusMounts(context),
tryUmounts = getTryUmounts(context), tryUmounts = getTryUmounts(context),
androidDataPath = getAndroidDataPath(context), androidDataPath = getAndroidDataPath(context),
@@ -326,6 +343,13 @@ object SuSFSManager {
fun getSusPaths(context: Context): Set<String> = fun getSusPaths(context: Context): Set<String> =
getPrefs(context).getStringSet(KEY_SUS_PATHS, emptySet()) ?: emptySet() 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>) = fun saveSusMounts(context: Context, mounts: Set<String>) =
getPrefs(context).edit { putStringSet(KEY_SUS_MOUNTS, mounts) } getPrefs(context).edit { putStringSet(KEY_SUS_MOUNTS, mounts) }
@@ -465,6 +489,7 @@ object SuSFSManager {
KEY_BUILD_TIME_VALUE to getBuildTimeValue(context), KEY_BUILD_TIME_VALUE to getBuildTimeValue(context),
KEY_AUTO_START_ENABLED to isAutoStartEnabled(context), KEY_AUTO_START_ENABLED to isAutoStartEnabled(context),
KEY_SUS_PATHS to getSusPaths(context), KEY_SUS_PATHS to getSusPaths(context),
KEY_SUS_LOOP_PATHS to getSusLoopPaths(context),
KEY_SUS_MOUNTS to getSusMounts(context), KEY_SUS_MOUNTS to getSusMounts(context),
KEY_TRY_UMOUNTS to getTryUmounts(context), KEY_TRY_UMOUNTS to getTryUmounts(context),
KEY_ANDROID_DATA_PATH to getAndroidDataPath(context), KEY_ANDROID_DATA_PATH to getAndroidDataPath(context),
@@ -771,6 +796,7 @@ object SuSFSManager {
private fun getDefaultDisabledFeatures(context: Context): List<EnabledFeature> { private fun getDefaultDisabledFeatures(context: Context): List<EnabledFeature> {
val defaultFeatures = listOf( val defaultFeatures = listOf(
"sus_path_feature_label" to context.getString(R.string.sus_path_feature_label), "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), "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), "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), "spoof_uname_feature_label" to context.getString(R.string.spoof_uname_feature_label),
@@ -930,6 +956,64 @@ object SuSFSManager {
return false 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挂载 // 添加SUS挂载
suspend fun addSusMount(context: Context, mount: String): Boolean { suspend fun addSusMount(context: Context, mount: String): Boolean {
val success = executeSusfsCommand(context, "add_sus_mount '$mount'") val success = executeSusfsCommand(context, "add_sus_mount '$mount'")

View File

@@ -103,6 +103,7 @@ object ScriptGenerator {
*/ */
private fun shouldConfigureInService(config: SuSFSManager.ModuleConfig): Boolean { private fun shouldConfigureInService(config: SuSFSManager.ModuleConfig): Boolean {
return config.susPaths.isNotEmpty() || return config.susPaths.isNotEmpty() ||
config.susLoopPaths.isNotEmpty() ||
config.kstatConfigs.isNotEmpty() || config.kstatConfigs.isNotEmpty() ||
config.addKstatPaths.isNotEmpty() || config.addKstatPaths.isNotEmpty() ||
(!config.executeInPostFsData && (config.unameValue != DEFAULT_UNAME || config.buildTimeValue != DEFAULT_BUILD_TIME)) (!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") @SuppressLint("SdCardPath")
private fun StringBuilder.generateKstatSection( private fun StringBuilder.generateKstatSection(
kstatConfigs: Set<String>, kstatConfigs: Set<String>,
@@ -461,11 +473,20 @@ object ScriptGenerator {
appendLine() appendLine()
// 路径设置和SUS路径设置 // 路径设置和SUS路径设置
if (config.susPaths.isNotEmpty()) { if (config.susPaths.isNotEmpty() || config.susLoopPaths.isNotEmpty()) {
generatePathSettingSection(config.androidDataPath, config.sdcardPath) generatePathSettingSection(config.androidDataPath, config.sdcardPath)
appendLine() appendLine()
// 添加普通SUS路径
if (config.susPaths.isNotEmpty()) {
generateSusPathsSection(config.susPaths) generateSusPathsSection(config.susPaths)
} }
// 添加循环SUS路径
if (config.susLoopPaths.isNotEmpty()) {
generateSusLoopPathsSection(config.susLoopPaths)
}
}
} }
appendLine("echo \"$(get_current_time): Boot-Completed脚本执行完成\" >> \"${'$'}LOG_FILE\"") appendLine("echo \"$(get_current_time): Boot-Completed脚本执行完成\" >> \"${'$'}LOG_FILE\"")

View File

@@ -586,4 +586,24 @@
<string name="multi_manager_list">活跃管理器</string> <string name="multi_manager_list">活跃管理器</string>
<string name="no_active_manager">无活跃管理器</string> <string name="no_active_manager">无活跃管理器</string>
<string name="home_zygisk_implement">Zygisk 实现</string> <string name="home_zygisk_implement">Zygisk 实现</string>
<!-- 循环路径相关 -->
<string name="susfs_tab_sus_loop_paths">SUS循环路径</string>
<string name="susfs_add_sus_loop_path">添加SUS循环路径</string>
<string name="susfs_edit_sus_loop_path">编辑SUS循环路径</string>
<string name="susfs_loop_path_added_success">SUS循环路径添加成功: %1$s</string>
<string name="susfs_loop_path_removed">SUS循环路径已移除: %1$s</string>
<string name="susfs_loop_path_updated">SUS循环路径已更新: %1$s -> %2$s</string>
<string name="susfs_no_loop_paths_configured">未配置SUS循环路径</string>
<string name="susfs_reset_loop_paths_title">重置循环路径</string>
<string name="susfs_reset_loop_paths_message">确定要清空所有SUS循环路径吗此操作无法撤销。</string>
<string name="susfs_loop_path_label">循环路径</string>
<string name="susfs_loop_path_placeholder">/data/example/path</string>
<string name="susfs_loop_path_restriction_warning">注意:只有不在/storage/和/sdcard/内的路径才能通过循环路径添加。</string>
<string name="susfs_loop_path_invalid_location">错误:循环路径不能位于/storage/或/sdcard/目录内</string>
<string name="loop_paths_section">循环路径</string>
<string name="add_loop_path">添加循环路径</string>
<!-- 循环路径功能描述 -->
<string name="sus_loop_path_feature_label">SUS循环路径</string>
<string name="sus_loop_paths_description_title">循环路径配置</string>
<string name="sus_loop_paths_description_text">循环路径会在每次非root用户应用或隔离服务启动时重新标记为SUS_PATH。这有助于解决添加的路径可能因inode状态重置或内核中inode重新创建而失效的问题</string>
</resources> </resources>

View File

@@ -589,4 +589,24 @@
<string name="no_active_manager">No active manager</string> <string name="no_active_manager">No active manager</string>
<string name="default_signature">SukiSU</string> <string name="default_signature">SukiSU</string>
<string name="home_zygisk_implement">Zygisk implement</string> <string name="home_zygisk_implement">Zygisk implement</string>
<!-- 循环路径相关 -->
<string name="susfs_tab_sus_loop_paths">SUS Loop Paths</string>
<string name="susfs_add_sus_loop_path">Add SUS Loop Path</string>
<string name="susfs_edit_sus_loop_path">Edit SUS Loop Path</string>
<string name="susfs_loop_path_added_success">SUS loop path added successfully: %1$s</string>
<string name="susfs_loop_path_removed">SUS loop path removed: %1$s</string>
<string name="susfs_loop_path_updated">SUS loop path updated: %1$s -> %2$s</string>
<string name="susfs_no_loop_paths_configured">No SUS loop paths configured</string>
<string name="susfs_reset_loop_paths_title">Reset Loop Paths</string>
<string name="susfs_reset_loop_paths_message">Are you sure you want to clear all SUS loop paths? This action cannot be undone.</string>
<string name="susfs_loop_path_label">Loop Path</string>
<string name="susfs_loop_path_placeholder">/data/example/path</string>
<string name="susfs_loop_path_restriction_warning">Note: Only paths NOT inside /storage/ and /sdcard/ can be added via loop paths.</string>
<string name="susfs_loop_path_invalid_location">Error: Loop paths cannot be inside /storage/ or /sdcard/ directories</string>
<string name="loop_paths_section">Loop Paths</string>
<string name="add_loop_path">Add Loop Path</string>
<!-- 循环路径功能描述 -->
<string name="sus_loop_path_feature_label">SUS Loop Path</string>
<string name="sus_loop_paths_description_title">Loop Path Configuration</string>
<string name="sus_loop_paths_description_text">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.</string>
</resources> </resources>