manager: Reorganizing the backup and restore functionality of SuSFS configurations
- Add checking for the existence of a data catalog for applications in SUS Path - Modify the loading and caching mechanism of the application information class to avoid repeated refreshes,Finish loading with SuperUser.
This commit is contained in:
@@ -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,17 +391,30 @@ fun AppIcon(
|
||||
modifier = modifier.clip(RoundedCornerShape(8.dp))
|
||||
)
|
||||
} else {
|
||||
var appIcon by remember(packageName) { mutableStateOf<Drawable?>(null) }
|
||||
var appIcon by remember(packageName) {
|
||||
mutableStateOf(
|
||||
AppInfoCache.getAppInfo(packageName)?.drawable
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(packageName) {
|
||||
if (appIcon == null && !AppInfoCache.hasCache(packageName)) {
|
||||
try {
|
||||
val packageManager = context.packageManager
|
||||
val applicationInfo = packageManager.getApplicationInfo(packageName, 0)
|
||||
appIcon = packageManager.getApplicationIcon(applicationInfo)
|
||||
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(
|
||||
painter = rememberDrawablePainter(appIcon),
|
||||
contentDescription = null,
|
||||
|
||||
@@ -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<String, MutableList<String>>()
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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<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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 空状态显示组件
|
||||
@@ -652,36 +692,78 @@ fun AppPathGroupCard(
|
||||
isLoading: Boolean
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var appName by remember(packageName) { mutableStateOf("") }
|
||||
var packageInfo by remember(packageName) { mutableStateOf<PackageInfo?>(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) {
|
||||
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)
|
||||
packageInfo = appInfo
|
||||
|
||||
appName = try {
|
||||
val 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
|
||||
|
||||
val drawable = try {
|
||||
appInfo.applicationInfo?.let {
|
||||
packageManager.getApplicationIcon(it)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
appName = packageName
|
||||
packageInfo = null
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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<AppInfo> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val pm = context.packageManager
|
||||
val allApps = mutableMapOf<String, AppInfo>()
|
||||
|
||||
// 从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<String>()
|
||||
val errorList = mutableListOf<String>()
|
||||
|
||||
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
|
||||
@@ -624,12 +601,8 @@ 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()
|
||||
}
|
||||
|
||||
// 槽位信息获取
|
||||
|
||||
Reference in New Issue
Block a user