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