[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.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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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挂载内容组件
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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() }
|
||||||
|
|
||||||
|
|||||||
@@ -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_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'")
|
||||||
|
|||||||
@@ -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\"")
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user