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 ca40a22d..7869f5b3 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 @@ -2,7 +2,6 @@ package com.sukisu.ultra.ui.component import android.annotation.SuppressLint import android.content.pm.PackageInfo -import android.graphics.drawable.Drawable import android.util.Log import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement @@ -55,6 +54,7 @@ import coil.request.ImageRequest import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.sukisu.ultra.R import com.sukisu.ultra.ui.util.SuSFSManager +import com.sukisu.ultra.ui.screen.extensions.AppInfoCache /** * 添加路径对话框 @@ -391,15 +391,28 @@ fun AppIcon( modifier = modifier.clip(RoundedCornerShape(8.dp)) ) } else { - var appIcon by remember(packageName) { mutableStateOf(null) } + var appIcon by remember(packageName) { + mutableStateOf( + AppInfoCache.getAppInfo(packageName)?.drawable + ) + } LaunchedEffect(packageName) { - try { - val packageManager = context.packageManager - val applicationInfo = packageManager.getApplicationInfo(packageName, 0) - appIcon = packageManager.getApplicationIcon(applicationInfo) - } catch (_: Exception) { - Log.d("获取应用图标失败", packageName) + if (appIcon == null && !AppInfoCache.hasCache(packageName)) { + try { + val packageManager = context.packageManager + val applicationInfo = packageManager.getApplicationInfo(packageName, 0) + val drawable = packageManager.getApplicationIcon(applicationInfo) + appIcon = drawable + val cachedInfo = AppInfoCache.CachedAppInfo( + appName = packageName, + packageInfo = null, + drawable = drawable + ) + AppInfoCache.putAppInfo(packageName, cachedInfo) + } catch (_: Exception) { + Log.d("获取应用图标失败", packageName) + } } } Image( 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 07792465..f1ef90e1 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 @@ -32,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.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -41,6 +42,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.sukisu.ultra.R import com.sukisu.ultra.ui.screen.extensions.AddKstatPathItemCard +import com.sukisu.ultra.ui.screen.extensions.AppInfoCache import com.sukisu.ultra.ui.screen.extensions.AppPathGroupCard import com.sukisu.ultra.ui.screen.extensions.EmptyStateCard import com.sukisu.ultra.ui.screen.extensions.FeatureStatusCard @@ -50,6 +52,7 @@ import com.sukisu.ultra.ui.screen.extensions.SectionHeader import com.sukisu.ultra.ui.screen.extensions.SusMountHidingControlCard import com.sukisu.ultra.ui.util.SuSFSManager import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion_1_5_8 +import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel /** * SUS路径内容组件 @@ -61,8 +64,24 @@ fun SusPathsContent( onAddPath: () -> Unit, onAddAppPath: () -> Unit, onRemovePath: (String) -> Unit, - onEditPath: ((String) -> Unit)? = null + onEditPath: ((String) -> Unit)? = null, + forceRefreshApps: Boolean = false ) { + val superUserApps = SuperUserViewModel.apps + val superUserIsRefreshing = remember { SuperUserViewModel().isRefreshing } + + LaunchedEffect(superUserIsRefreshing, superUserApps.size) { + if (!superUserIsRefreshing && superUserApps.isNotEmpty()) { + AppInfoCache.clearCache() + } + } + + LaunchedEffect(forceRefreshApps) { + if (forceRefreshApps) { + AppInfoCache.clearCache() + } + } + val (appPathGroups, otherPaths) = remember(susPaths) { val appPathRegex = Regex(".*/Android/data/([^/]+)/?.*") val appPathMap = mutableMapOf>() 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 dc025de1..cc3386cc 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 @@ -84,6 +84,7 @@ import com.sukisu.ultra.ui.util.SuSFSManager import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion_1_5_8 import com.sukisu.ultra.ui.util.isAbDevice import kotlinx.coroutines.launch +import java.io.File import java.text.SimpleDateFormat import java.util.* @@ -205,21 +206,22 @@ fun SuSFSConfigScreen( contract = ActivityResultContracts.CreateDocument("application/json") ) { uri -> uri?.let { fileUri -> - val fileName = SuSFSManager.getRecommendedBackupPath(context) + val fileName = SuSFSManager.getDefaultBackupFileName() + val tempFile = File(context.cacheDir, fileName) coroutineScope.launch { isLoading = true - val success = SuSFSManager.createBackup(context, fileName) + val success = SuSFSManager.createBackup(context, tempFile.absolutePath) if (success) { - // 复制到用户选择的位置 try { context.contentResolver.openOutputStream(fileUri)?.use { outputStream -> - java.io.File(fileName).inputStream().use { inputStream -> + tempFile.inputStream().use { inputStream -> inputStream.copyTo(outputStream) } } } catch (e: Exception) { e.printStackTrace() } + tempFile.delete() } isLoading = false showBackupDialog = false @@ -233,8 +235,7 @@ fun SuSFSConfigScreen( uri?.let { fileUri -> coroutineScope.launch { try { - // 复制到临时文件 - val tempFile = java.io.File(context.cacheDir, "temp_restore.susfs_backup") + val tempFile = File(context.cacheDir, "temp_restore.susfs_backup") context.contentResolver.openInputStream(fileUri)?.use { inputStream -> tempFile.outputStream().use { outputStream -> inputStream.copyTo(outputStream) @@ -247,8 +248,6 @@ fun SuSFSConfigScreen( selectedBackupFile = tempFile.absolutePath backupInfo = backup showRestoreConfirmDialog = true - } else { - // 显示错误消息 } tempFile.deleteOnExit() } catch (e: Exception) { @@ -1173,7 +1172,8 @@ fun SuSFSConfigScreen( onEditPath = { path -> editingPath = path showAddPathDialog = true - } + }, + forceRefreshApps = selectedTab == SuSFSTab.SUS_PATHS ) } SuSFSTab.SUS_MOUNTS -> { 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 412aa4fd..014edb89 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 @@ -3,6 +3,7 @@ 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 @@ -56,8 +57,47 @@ 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() + + 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 + ) + } + } +} /** * 空状态显示组件 @@ -652,36 +692,78 @@ fun AppPathGroupCard( isLoading: Boolean ) { val context = LocalContext.current - var appName by remember(packageName) { mutableStateOf("") } - var packageInfo by remember(packageName) { mutableStateOf(null) } + 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) { - try { - val packageManager = context.packageManager - val appInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA) - packageInfo = appInfo + LaunchedEffect(packageName, superUserApps.size) { + if (cachedAppInfo == null || superUserApps.isNotEmpty()) { + isLoadingAppInfo = true + coroutineScope.launch { + try { + val superUserAppInfo = AppInfoCache.getAppInfoFromSuperUser(packageName) - appName = try { - appInfo.applicationInfo?.let { - packageManager.getApplicationLabel(it).toString() - } ?: packageName - } catch (_: Exception) { - packageName - } - } catch (_: Exception) { - try { - val installedApps = SuSFSManager.getInstalledApps() - val foundApp = installedApps.find { it.packageName == packageName } - if (foundApp != null) { - appName = foundApp.appName - packageInfo = foundApp.packageInfo - } else { - appName = packageName - packageInfo = null + 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 } - } catch (_: Exception) { - appName = packageName - packageInfo = null } } } @@ -703,20 +785,22 @@ fun AppPathGroupCard( // 应用图标 AppIcon( packageName = packageName, - packageInfo = packageInfo, + 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 = appName.ifEmpty { packageName }, + text = displayName, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurface ) - if (appName.isNotEmpty() && appName != packageName) { + if (!isLoadingAppInfo && cachedAppInfo?.appName?.isNotEmpty() == true && + cachedAppInfo?.appName != packageName) { Text( text = packageName, style = MaterialTheme.typography.bodySmall, @@ -753,7 +837,7 @@ fun AppPathGroupCard( } } - // 显示所有路径 + // 显示所有路径 Spacer(modifier = Modifier.height(8.dp)) paths.forEach { path -> 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 ab12b8ce..d3c4e188 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 @@ -5,7 +5,6 @@ import android.content.Context import android.content.SharedPreferences import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo -import android.content.pm.PackageManager import android.util.Log import android.widget.Toast import com.dergoogler.mmrl.platform.Platform.Companion.context @@ -21,6 +20,8 @@ import java.io.FileOutputStream import java.io.IOException import androidx.core.content.edit import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import org.json.JSONObject import java.text.SimpleDateFormat import java.util.* @@ -57,6 +58,7 @@ object SuSFSManager { private const val MODULE_PATH = "/data/adb/modules/$MODULE_ID" private const val MIN_VERSION_FOR_HIDE_MOUNT = "1.5.8" private const val BACKUP_FILE_EXTENSION = ".susfs_backup" + private const val MEDIA_DATA_PATH = "/data/media/0/Android/data" data class SlotInfo(val slotName: String, val uname: String, val buildTime: String) data class CommandResult(val isSuccess: Boolean, val output: String, val errorOutput: String = "") @@ -370,7 +372,6 @@ object SuSFSManager { @SuppressLint("QueryPermissionsNeeded") suspend fun getInstalledApps(): List = withContext(Dispatchers.IO) { try { - val pm = context.packageManager val allApps = mutableMapOf() // 从SuperUser中获取应用 @@ -391,54 +392,30 @@ object SuSFSManager { } } - try { - // 尝试从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 - } ?: false - // 只处理非系统应用且不在SuperUser列表中的应用 - if (!isSystemApp && !allApps.containsKey(packageName)) { - try { - val appName = packageInfo.applicationInfo?.loadLabel(pm)?.toString() ?: packageName - allApps[packageName] = AppInfo( - packageName = packageName, - appName = appName, - packageInfo = packageInfo, - isSystemApp = false - ) - } catch (_: Exception) { - allApps[packageName] = AppInfo( - packageName = packageName, - appName = packageName, - packageInfo = packageInfo, - isSystemApp = false - ) - } - } - } - } catch (e: Exception) { - Log.e("SuSFSManager", "Error getting installed packages", e) - } - // 添加可能遗漏的当前应用 - val currentPackageName = context.packageName - if (!allApps.containsKey(currentPackageName)) { - try { - val currentAppInfo = pm.getPackageInfo(currentPackageName, 0) - val currentAppName = currentAppInfo.applicationInfo?.loadLabel(pm)?.toString() ?: "com.sukisu.ultra" - allApps[currentPackageName] = AppInfo( - packageName = currentPackageName, - appName = currentAppName, - packageInfo = currentAppInfo, - isSystemApp = false - ) - } catch (_: Exception) { - } - } + // 检查每个应用的数据目录是否存在 + val filteredApps = allApps.values.map { appInfo -> + async(Dispatchers.IO) { + val dataPath = "$MEDIA_DATA_PATH/${appInfo.packageName}" + val exists = try { + val shell = getRootShell() + val outputList = mutableListOf() + val errorList = mutableListOf() - allApps.values.sortedBy { it.appName } + val result = shell.newJob() + .add("[ -d \"$dataPath\" ] && echo 'exists' || echo 'not_exists'") + .to(outputList, errorList) + .exec() + + result.isSuccess && outputList.isNotEmpty() && outputList[0].trim() == "exists" + } catch (e: Exception) { + Log.w("SuSFSManager", "Failed to check directory for ${appInfo.packageName}: ${e.message}") + false + } + if (exists) appInfo else null + } + }.awaitAll().filterNotNull() + + filteredApps.sortedBy { it.appName } } catch (e: Exception) { e.printStackTrace() emptyList() @@ -454,7 +431,7 @@ object SuSFSManager { getSdcardPath(context) val path1 = "$androidDataPath/$packageName" - val path2 = "/data/media/0/Android/data/$packageName" + val path2 = "$MEDIA_DATA_PATH/$packageName" var successCount = 0 var totalCount = 0 @@ -623,13 +600,9 @@ object SuSFSManager { } } - //获取备份文件路径 - fun getRecommendedBackupPath(context: Context): String { - val documentsDir = File(context.getExternalFilesDir(null), "SuSFS_Backups") - if (!documentsDir.exists()) { - documentsDir.mkdirs() - } - return File(documentsDir, generateBackupFileName()).absolutePath + // 获取备份文件路径 + fun getDefaultBackupFileName(): String { + return generateBackupFileName() } // 槽位信息获取