manager: Adding optional additions to SUS paths applies functionality corresponding to the package name as well as categorization

This commit is contained in:
ShirkNeko
2025-07-01 17:29:45 +08:00
parent be14da387e
commit 2278fe49d2
9 changed files with 757 additions and 19 deletions

View File

@@ -159,4 +159,6 @@ dependencies {
implementation(libs.mmrl.webui)
implementation(libs.mmrl.ui)
implementation(libs.accompanist.drawablepainter)
}

View File

@@ -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<String>) -> Unit,
isLoading: Boolean,
apps: List<SuSFSManager.AppInfo> = emptyList(),
onLoadApps: () -> Unit,
existingSusPaths: Set<String> = emptySet()
) {
var searchText by remember { mutableStateOf("") }
var selectedApps by remember { mutableStateOf(setOf<SuSFSManager.AppInfo>()) }
// 获取已添加的包名
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<Drawable?>(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
)
}
}
/**
* 添加尝试卸载对话框
*/

View File

@@ -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<String>,
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<String, MutableList<String>>()
val others = mutableListOf<String>()
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(),

View File

@@ -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<SuSFSManager.EnabledFeature>()) }
var isLoadingFeatures by remember { mutableStateOf(false) }
// 应用列表相关状态
var installedApps by remember { mutableStateOf(emptyList<SuSFSManager.AppInfo>()) }
// 对话框状态
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

View File

@@ -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
@@ -633,3 +637,179 @@ fun SusMountHidingControlCard(
}
}
}
/**
* 应用路径分组卡片
*/
@Composable
fun AppPathGroupCard(
packageName: String,
paths: List<String>,
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
)
}
}
}
}

View File

@@ -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<AppInfo> = withContext(Dispatchers.IO) {
try {
val pm = context.packageManager
val allApps = mutableMapOf<String, AppInfo>()
// 从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<String, Any> {
return mapOf(

View File

@@ -557,4 +557,13 @@
<string name="umount_zygote_iso_service_description">启用此选项将在系统启动时卸载Zygote隔离服务挂载点</string>
<string name="umount_zygote_iso_service_enabled">Zygote隔离服务卸载已启用</string>
<string name="umount_zygote_iso_service_disabled">Zygote隔离服务卸载已禁用</string>
<string name="app_paths_section">应用路径</string>
<string name="other_paths_section">其他路径</string>
<string name="add_custom_path">其他</string>
<string name="add_app_path">应用</string>
<string name="susfs_add_app_path">添加应用路径</string>
<string name="search_apps">搜索应用</string>
<string name="selected_apps_count">%1$d 个已选应用</string>
<string name="already_added_apps_count">%1$d 个已添加应用</string>
<string name="all_apps_already_added">所有应用均已添加</string>
</resources>

View File

@@ -559,4 +559,13 @@
<string name="umount_zygote_iso_service_description">Enable this option to unmount Zygote isolation service mount points at system startup</string>
<string name="umount_zygote_iso_service_enabled">Zygote isolation service unmount enabled</string>
<string name="umount_zygote_iso_service_disabled">Zygote isolation service unmount disabled</string>
<string name="app_paths_section">Application Path</string>
<string name="other_paths_section">Other paths</string>
<string name="add_custom_path">Other</string>
<string name="add_app_path">App</string>
<string name="susfs_add_app_path">Add App Path</string>
<string name="search_apps">Search Apps</string>
<string name="selected_apps_count">%1$d apps selected</string>
<string name="already_added_apps_count">%1$d apps already added</string>
<string name="all_apps_already_added">All apps have been added</string>
</resources>

View File

@@ -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" }