[skip ci]manager: Add SUS loop path function
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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挂载内容组件
|
||||
*/
|
||||
|
||||
@@ -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() }
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -586,4 +586,24 @@
|
||||
<string name="multi_manager_list">活跃管理器</string>
|
||||
<string name="no_active_manager">无活跃管理器</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>
|
||||
|
||||
@@ -589,4 +589,24 @@
|
||||
<string name="no_active_manager">No active manager</string>
|
||||
<string name="default_signature">SukiSU</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>
|
||||
|
||||
Reference in New Issue
Block a user