From 2278fe49d200ba9cd4936d494032cc36907d4137 Mon Sep 17 00:00:00 2001 From: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com> Date: Tue, 1 Jul 2025 17:29:45 +0800 Subject: [PATCH] manager: Adding optional additions to SUS paths applies functionality corresponding to the package name as well as categorization --- manager/app/build.gradle.kts | 2 + .../ultra/ui/component/SuSFSConfigDialogs.kt | 301 ++++++++++++++++++ .../ultra/ui/component/SuSFSConfigTabs.kt | 109 +++++-- .../com/sukisu/ultra/ui/screen/SuSFSConfig.kt | 38 +++ .../extensions/SuSFSConfigExtensions.kt | 180 +++++++++++ .../com/sukisu/ultra/ui/util/SuSFSManager.kt | 126 ++++++++ .../src/main/res/values-zh-rCN/strings.xml | 9 + manager/app/src/main/res/values/strings.xml | 9 + manager/gradle/libs.versions.toml | 2 + 9 files changed, 757 insertions(+), 19 deletions(-) diff --git a/manager/app/build.gradle.kts b/manager/app/build.gradle.kts index e368d2a2..f09d042c 100644 --- a/manager/app/build.gradle.kts +++ b/manager/app/build.gradle.kts @@ -159,4 +159,6 @@ dependencies { implementation(libs.mmrl.webui) implementation(libs.mmrl.ui) + implementation(libs.accompanist.drawablepainter) + } \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigDialogs.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigDialogs.kt index ffde1db2..258e4ab4 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigDialogs.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigDialogs.kt @@ -1,19 +1,34 @@ package com.sukisu.ultra.ui.component +import android.graphics.drawable.Drawable +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row 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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Apps +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.RadioButtonUnchecked +import androidx.compose.material.icons.filled.Search import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.OutlinedTextField @@ -26,11 +41,16 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.sukisu.ultra.R +import com.sukisu.ultra.ui.util.SuSFSManager /** * 添加路径对话框 @@ -105,6 +125,287 @@ fun AddPathDialog( } } +/** + * 快捷添加应用路径对话框 + */ +@Composable +fun AddAppPathDialog( + showDialog: Boolean, + onDismiss: () -> Unit, + onConfirm: (List) -> Unit, + isLoading: Boolean, + apps: List = emptyList(), + onLoadApps: () -> Unit, + existingSusPaths: Set = emptySet() +) { + var searchText by remember { mutableStateOf("") } + var selectedApps by remember { mutableStateOf(setOf()) } + + // 获取已添加的包名 + val addedPackageNames = remember(existingSusPaths) { + existingSusPaths.mapNotNull { path -> + val regex = Regex(".*/Android/data/([^/]+)/?.*") + regex.find(path)?.groupValues?.get(1) + }.toSet() + } + + // 过滤掉已添加的应用 + val availableApps = remember(apps, addedPackageNames) { + apps.filter { app -> + !addedPackageNames.contains(app.packageName) + } + } + + val filteredApps = remember(availableApps, searchText) { + if (searchText.isBlank()) { + availableApps + } else { + availableApps.filter { app -> + app.appName.contains(searchText, ignoreCase = true) || + app.packageName.contains(searchText, ignoreCase = true) + } + } + } + + LaunchedEffect(showDialog) { + if (showDialog && apps.isEmpty()) { + onLoadApps() + } + // 当对话框显示时清空选择 + if (showDialog) { + selectedApps = setOf() + } + } + + if (showDialog) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text( + text = stringResource(R.string.susfs_add_app_path), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + OutlinedTextField( + value = searchText, + onValueChange = { searchText = it }, + label = { Text(stringResource(R.string.search_apps)) }, + leadingIcon = { + Icon( + imageVector = Icons.Default.Search, + contentDescription = null + ) + }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp) + ) + + // 显示统计信息 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + if (selectedApps.isNotEmpty()) { + Text( + text = stringResource(R.string.selected_apps_count, selectedApps.size), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Medium + ) + } + if (addedPackageNames.isNotEmpty()) { + Text( + text = stringResource(R.string.already_added_apps_count, addedPackageNames.size), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + if (filteredApps.isEmpty()) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ) + ) { + Text( + text = if (availableApps.isEmpty()) { + stringResource(R.string.all_apps_already_added) + } else { + stringResource(R.string.no_apps_found) + }, + modifier = Modifier.padding(16.dp), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else { + LazyColumn( + modifier = Modifier.height(300.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(filteredApps) { app -> + val isSelected = selectedApps.contains(app) + + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = if (isSelected) { + MaterialTheme.colorScheme.primaryContainer + } else { + MaterialTheme.colorScheme.surface + } + ), + onClick = { + selectedApps = if (isSelected) { + selectedApps - app + } else { + selectedApps + app + } + } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 应用图标 + AppIcon( + packageName = app.packageName, + modifier = Modifier.size(40.dp) + ) + + Column( + modifier = Modifier + .weight(1f) + .padding(start = 12.dp) + ) { + Text( + text = app.appName, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + color = if (isSelected) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurface + } + ) + Text( + text = app.packageName, + style = MaterialTheme.typography.bodyMedium, + color = if (isSelected) { + MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + ) + } + + // 选择指示器 + if (isSelected) { + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(24.dp) + ) + } else { + Icon( + imageVector = Icons.Default.RadioButtonUnchecked, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(24.dp) + ) + } + } + } + } + } + } + } + }, + confirmButton = { + Button( + onClick = { + if (selectedApps.isNotEmpty()) { + onConfirm(selectedApps.map { it.packageName }) + } + selectedApps = setOf() + searchText = "" + }, + enabled = selectedApps.isNotEmpty() && !isLoading, + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = stringResource(R.string.add) + ) + } + }, + dismissButton = { + TextButton( + onClick = { + onDismiss() + selectedApps = setOf() + searchText = "" + }, + shape = RoundedCornerShape(8.dp) + ) { + Text(stringResource(R.string.cancel)) + } + }, + shape = RoundedCornerShape(12.dp) + ) + } +} + + +/** + * 应用图标组件 + */ +@Composable +fun AppIcon( + packageName: String, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + var appIcon by remember(packageName) { mutableStateOf(null) } + + LaunchedEffect(packageName) { + try { + val packageManager = context.packageManager + val applicationInfo = packageManager.getApplicationInfo(packageName, 0) + appIcon = packageManager.getApplicationIcon(applicationInfo) + } catch (_: Exception) { + appIcon = null + } + } + + if (appIcon != null) { + Image( + painter = rememberDrawablePainter(appIcon), + contentDescription = null, + modifier = modifier + .clip(RoundedCornerShape(8.dp)) + ) + } else { + // 默认图标 + Icon( + imageVector = Icons.Default.Apps, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = modifier + ) + } +} + + /** * 添加尝试卸载对话框 */ diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigTabs.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigTabs.kt index dd350e3c..07792465 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigTabs.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigTabs.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Apps import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Security @@ -31,6 +32,7 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -39,10 +41,12 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.sukisu.ultra.R import com.sukisu.ultra.ui.screen.extensions.AddKstatPathItemCard +import com.sukisu.ultra.ui.screen.extensions.AppPathGroupCard import com.sukisu.ultra.ui.screen.extensions.EmptyStateCard import com.sukisu.ultra.ui.screen.extensions.FeatureStatusCard import com.sukisu.ultra.ui.screen.extensions.KstatConfigItemCard import com.sukisu.ultra.ui.screen.extensions.PathItemCard +import com.sukisu.ultra.ui.screen.extensions.SectionHeader import com.sukisu.ultra.ui.screen.extensions.SusMountHidingControlCard import com.sukisu.ultra.ui.util.SuSFSManager import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion_1_5_8 @@ -55,22 +59,77 @@ fun SusPathsContent( susPaths: Set, isLoading: Boolean, onAddPath: () -> Unit, + onAddAppPath: () -> Unit, onRemovePath: (String) -> Unit, onEditPath: ((String) -> Unit)? = null ) { + val (appPathGroups, otherPaths) = remember(susPaths) { + val appPathRegex = Regex(".*/Android/data/([^/]+)/?.*") + val appPathMap = mutableMapOf>() + val others = mutableListOf() + + susPaths.forEach { path -> + val matchResult = appPathRegex.find(path) + if (matchResult != null) { + val packageName = matchResult.groupValues[1] + appPathMap.getOrPut(packageName) { mutableListOf() }.add(path) + } else { + others.add(path) + } + } + + val sortedAppGroups = appPathMap.toList() + .sortedBy { it.first } + .map { (packageName, paths) -> packageName to paths.sorted() } + + Pair(sortedAppGroups, others.sorted()) + } + Box(modifier = Modifier.fillMaxSize()) { LazyColumn( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(12.dp) ) { - if (susPaths.isEmpty()) { + // 应用路径分组 + if (appPathGroups.isNotEmpty()) { item { - EmptyStateCard( - message = stringResource(R.string.susfs_no_paths_configured) + SectionHeader( + title = stringResource(R.string.app_paths_section), + subtitle = null, + icon = Icons.Default.Apps, + count = appPathGroups.size ) } - } else { - items(susPaths.toList()) { path -> + + items(appPathGroups) { (packageName, paths) -> + AppPathGroupCard( + packageName = packageName, + paths = paths, + onDeleteGroup = { + paths.forEach { path -> onRemovePath(path) } + }, + onEditGroup = if (onEditPath != null) { + { + onEditPath(paths.first()) + } + } else null, + isLoading = isLoading + ) + } + } + + // 其他路径 + if (otherPaths.isNotEmpty()) { + item { + SectionHeader( + title = stringResource(R.string.other_paths_section), + subtitle = null, + icon = Icons.Default.Folder, + count = otherPaths.size + ) + } + + items(otherPaths) { path -> PathItemCard( path = path, icon = Icons.Default.Folder, @@ -81,7 +140,14 @@ fun SusPathsContent( } } - // 添加普通长按钮 + if (susPaths.isEmpty()) { + item { + EmptyStateCard( + message = stringResource(R.string.susfs_no_paths_configured) + ) + } + } + item { Row( modifier = Modifier @@ -103,7 +169,23 @@ fun SusPathsContent( modifier = Modifier.size(24.dp) ) Spacer(modifier = Modifier.width(8.dp)) - Text(text = stringResource(R.string.add)) + Text(text = stringResource(R.string.add_custom_path)) + } + + Button( + onClick = onAddAppPath, + modifier = Modifier + .weight(1f) + .height(48.dp), + shape = RoundedCornerShape(8.dp) + ) { + Icon( + imageVector = Icons.Default.Apps, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = stringResource(R.string.add_app_path)) } } } @@ -158,7 +240,6 @@ fun SusMountsContent( } } - // 添加普通长按钮 item { Row( modifier = Modifier @@ -204,8 +285,7 @@ fun TryUmountContent( ) { Box(modifier = Modifier.fillMaxSize()) { LazyColumn( - modifier = Modifier - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(12.dp) ) { if (isSusVersion_1_5_8()) { @@ -289,7 +369,6 @@ fun TryUmountContent( } } - // 添加普通长按钮 item { Row( modifier = Modifier @@ -359,7 +438,6 @@ fun KstatConfigContent( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(12.dp) ) { - // 说明卡片 item { Card( modifier = Modifier.fillMaxWidth(), @@ -402,7 +480,6 @@ fun KstatConfigContent( } } - // 静态Kstat配置列表 if (kstatConfigs.isNotEmpty()) { item { Text( @@ -421,7 +498,6 @@ fun KstatConfigContent( } } - // Add Kstat路径列表 if (addKstatPaths.isNotEmpty()) { item { Text( @@ -442,7 +518,6 @@ fun KstatConfigContent( } } - // 空状态显示 if (kstatConfigs.isEmpty() && addKstatPaths.isEmpty()) { item { EmptyStateCard( @@ -451,7 +526,6 @@ fun KstatConfigContent( } } - // 添加普通长按钮 item { Row( modifier = Modifier @@ -515,7 +589,6 @@ fun PathSettingsContent( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - // Android Data路径设置 item { Card( modifier = Modifier.fillMaxWidth(), @@ -550,7 +623,6 @@ fun PathSettingsContent( } } - // SD卡路径设置 item { Card( modifier = Modifier.fillMaxWidth(), @@ -599,7 +671,6 @@ fun EnabledFeaturesContent( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(12.dp) ) { - // 说明卡片 item { Card( modifier = Modifier.fillMaxWidth(), diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt index 7c6575f9..dc025de1 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt @@ -68,6 +68,7 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.sukisu.ultra.R +import com.sukisu.ultra.ui.component.AddAppPathDialog import com.sukisu.ultra.ui.component.AddKstatStaticallyDialog import com.sukisu.ultra.ui.component.AddPathDialog import com.sukisu.ultra.ui.component.AddTryUmountDialog @@ -158,8 +159,12 @@ fun SuSFSConfigScreen( var enabledFeatures by remember { mutableStateOf(emptyList()) } var isLoadingFeatures by remember { mutableStateOf(false) } + // 应用列表相关状态 + var installedApps by remember { mutableStateOf(emptyList()) } + // 对话框状态 var showAddPathDialog by remember { mutableStateOf(false) } + var showAddAppPathDialog by remember { mutableStateOf(false) } var showAddMountDialog by remember { mutableStateOf(false) } var showAddUmountDialog by remember { mutableStateOf(false) } var showRunUmountDialog by remember { mutableStateOf(false) } @@ -263,6 +268,13 @@ fun SuSFSConfigScreen( } } + // 加载应用列表 + fun loadInstalledApps() { + coroutineScope.launch { + installedApps = SuSFSManager.getInstalledApps() + } + } + // 加载槽位信息 fun loadSlotInfo() { coroutineScope.launch { @@ -537,6 +549,31 @@ fun SuSFSConfigScreen( initialValue = editingPath ?: "" ) + AddAppPathDialog( + showDialog = showAddAppPathDialog, + onDismiss = { showAddAppPathDialog = false }, + onConfirm = { packageNames -> + coroutineScope.launch { + isLoading = true + var successCount = 0 + packageNames.forEach { packageName -> + if (SuSFSManager.addAppPaths(context, packageName)) { + successCount++ + } + } + if (successCount > 0) { + susPaths = SuSFSManager.getSusPaths(context) + } + isLoading = false + showAddAppPathDialog = false + } + }, + isLoading = isLoading, + apps = installedApps, + onLoadApps = { loadInstalledApps() }, + existingSusPaths = susPaths + ) + AddPathDialog( showDialog = showAddMountDialog, onDismiss = { @@ -1123,6 +1160,7 @@ fun SuSFSConfigScreen( susPaths = susPaths, isLoading = isLoading, onAddPath = { showAddPathDialog = true }, + onAddAppPath = { showAddAppPathDialog = true }, onRemovePath = { path -> coroutineScope.launch { isLoading = true diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/extensions/SuSFSConfigExtensions.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/extensions/SuSFSConfigExtensions.kt index f92e22e0..d7f405e7 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/extensions/SuSFSConfigExtensions.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/extensions/SuSFSConfigExtensions.kt @@ -1,6 +1,7 @@ package com.sukisu.ultra.ui.screen.extensions import android.annotation.SuppressLint +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -8,6 +9,7 @@ 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 @@ -33,6 +35,7 @@ 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 @@ -49,6 +52,7 @@ 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 kotlinx.coroutines.launch @@ -632,4 +636,180 @@ fun SusMountHidingControlCard( } } } +} + +/** + * 应用路径分组卡片 + */ +@Composable +fun AppPathGroupCard( + packageName: String, + paths: List, + onDeleteGroup: () -> Unit, + onEditGroup: (() -> Unit)? = null, + isLoading: Boolean +) { + val context = LocalContext.current + var appName by remember(packageName) { mutableStateOf("") } + + LaunchedEffect(packageName) { + try { + val packageManager = context.packageManager + val applicationInfo = packageManager.getApplicationInfo(packageName, 0) + appName = packageManager.getApplicationLabel(applicationInfo).toString() + } catch (_: Exception) { + appName = packageName + } + } + + 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, + modifier = Modifier.size(32.dp) + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = appName.ifEmpty { packageName }, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + if (appName.isNotEmpty() && appName != packageName) { + Text( + text = packageName, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (onEditGroup != null) { + IconButton( + onClick = onEditGroup, + enabled = !isLoading + ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = stringResource(R.string.edit), + tint = MaterialTheme.colorScheme.primary + ) + } + } + IconButton( + onClick = onDeleteGroup, + enabled = !isLoading + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource(R.string.delete), + tint = MaterialTheme.colorScheme.error + ) + } + } + } + + // 显示所有路径 + Spacer(modifier = Modifier.height(8.dp)) + + paths.forEach { path -> + Text( + text = path, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .fillMaxWidth() + .background( + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), + RoundedCornerShape(6.dp) + ) + .padding(8.dp) + ) + + if (path != paths.last()) { + Spacer(modifier = Modifier.height(4.dp)) + } + } + } + } +} + +/** + * 分组标题组件 + */ +@Composable +fun SectionHeader( + title: String, + subtitle: String?, + icon: ImageVector, + count: Int +) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) + ), + shape = RoundedCornerShape(12.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + subtitle?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + Surface( + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.primary + ) { + Text( + text = count.toString(), + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onPrimary, + fontWeight = FontWeight.Bold + ) + } + } + } } \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt index 5284b066..74ae628c 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt @@ -3,6 +3,8 @@ package com.sukisu.ultra.ui.util import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager import android.widget.Toast import com.dergoogler.mmrl.platform.Platform.Companion.context import com.sukisu.ultra.Natives @@ -16,6 +18,7 @@ import java.io.File import java.io.FileOutputStream import java.io.IOException import androidx.core.content.edit +import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel import org.json.JSONObject import java.text.SimpleDateFormat import java.util.* @@ -62,6 +65,15 @@ object SuSFSManager { val canConfigure: Boolean = false ) + /** + * 应用信息数据类 + */ + data class AppInfo( + val packageName: String, + val appName: String, + val isSystemApp: Boolean + ) + /** * 备份数据类 */ @@ -349,6 +361,120 @@ object SuSFSManager { fun getSdcardPath(context: Context): String = getPrefs(context).getString(KEY_SDCARD_PATH, "/sdcard") ?: "/sdcard" + /** + * 获取已安装的应用列表 + */ + @SuppressLint("QueryPermissionsNeeded") + suspend fun getInstalledApps(): List = withContext(Dispatchers.IO) { + try { + val pm = context.packageManager + val allApps = mutableMapOf() + + // 从SuperUser中获取应用 + SuperUserViewModel.apps.forEach { superUserApp -> + try { + val isSystemApp = superUserApp.packageInfo.applicationInfo?.let { + (it.flags and ApplicationInfo.FLAG_SYSTEM) != 0 + } ?: false + if (!isSystemApp) { + allApps[superUserApp.packageName] = AppInfo( + packageName = superUserApp.packageName, + appName = superUserApp.label, + isSystemApp = false + ) + } + } catch (_: Exception) { + } + } + + // 从PackageManager获取所有应用 + val installedPackages = pm.getInstalledPackages(PackageManager.GET_META_DATA) + installedPackages.forEach { packageInfo -> + val packageName = packageInfo.packageName + val isSystemApp = packageInfo.applicationInfo?.let { (it.flags and ApplicationInfo.FLAG_SYSTEM) != 0 } + + // 只处理非系统应用且不在SuperUser列表中的应用 + if (!isSystemApp!! && !allApps.containsKey(packageName)) { + try { + val appName = packageInfo.applicationInfo?.loadLabel(pm).toString() + allApps[packageName] = AppInfo( + packageName = packageName, + appName = appName, + isSystemApp = false + ) + } catch (_: Exception) { + allApps[packageName] = AppInfo( + packageName = packageName, + appName = packageName, + isSystemApp = false + ) + } + } + } + + // 添加可能遗漏的当前应用 + val currentPackageName = context.packageName + if (!allApps.containsKey(currentPackageName)) { + try { + val currentAppInfo = pm.getPackageInfo(currentPackageName, 0) + val currentAppName = currentAppInfo.applicationInfo?.loadLabel(pm).toString() + allApps[currentPackageName] = AppInfo( + packageName = currentPackageName, + appName = currentAppName, + isSystemApp = false + ) + } catch (_: Exception) { + allApps[currentPackageName] = AppInfo( + packageName = currentPackageName, + appName = "com.sukisu.ultra", + isSystemApp = false + ) + } + } + + allApps.values.sortedBy { it.appName } + } catch (e: Exception) { + e.printStackTrace() + emptyList() + } + } + + + /** + * 快捷添加应用路径 + */ + suspend fun addAppPaths(context: Context, packageName: String): Boolean { + val androidDataPath = getAndroidDataPath(context) + getSdcardPath(context) + + val path1 = "$androidDataPath/$packageName" + val path2 = "/data/media/0/Android/data/$packageName" + + var successCount = 0 + var totalCount = 0 + + // 添加第一个路径 + totalCount++ + if (addSusPath(context, path1)) { + successCount++ + } + + // 添加第二个路径 + totalCount++ + if (addSusPath(context, path2)) { + successCount++ + } + + val success = successCount > 0 + if (success) { + "" + } else { + "" + } + + return success + } + // 获取所有配置的Map private fun getAllConfigurations(context: Context): Map { return mapOf( diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index bd0b14ae..ecdc0e97 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -557,4 +557,13 @@ 启用此选项将在系统启动时卸载Zygote隔离服务挂载点 Zygote隔离服务卸载已启用 Zygote隔离服务卸载已禁用 + 应用路径 + 其他路径 + 其他 + 应用 + 添加应用路径 + 搜索应用 + %1$d 个已选应用 + %1$d 个已添加应用 + 所有应用均已添加 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 54f0d5de..e99fb62a 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -559,4 +559,13 @@ Enable this option to unmount Zygote isolation service mount points at system startup Zygote isolation service unmount enabled Zygote isolation service unmount disabled + Application Path + Other paths + Other + App + Add App Path + Search Apps + %1$d apps selected + %1$d apps already added + All apps have been added diff --git a/manager/gradle/libs.versions.toml b/manager/gradle/libs.versions.toml index 2e89f3ca..5c2b4fd1 100644 --- a/manager/gradle/libs.versions.toml +++ b/manager/gradle/libs.versions.toml @@ -1,4 +1,5 @@ [versions] +accompanist-drawablepainter = "0.37.3" agp = "8.11.0" gson = "2.11.0" kotlin = "2.1.20" @@ -37,6 +38,7 @@ lsplugin-apksign = { id = "org.lsposed.lsplugin.apksign", version.ref = "apksign lsplugin-cmaker = { id = "org.lsposed.lsplugin.cmaker", version.ref = "cmaker" } [libraries] +accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist-drawablepainter" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } androidx-foundation = { module = "androidx.compose.foundation:foundation" }