Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ class MainActivity : AppCompatActivity() {
composable(route = MainRoutes.Directory.route) {
PageDirectory()
}
composable(MainRoutes.VerifyBackup.route) { navBackStackEntry ->
val storageMode = navBackStackEntry.arguments?.getString(MainRoutes.ARG_STORAGE_MODE) ?: "Local"
val cloudName = navBackStackEntry.arguments?.getString(MainRoutes.ARG_ACCOUNT_NAME)
val backupDir = navBackStackEntry.arguments?.getString(MainRoutes.ARG_ACCOUNT_REMOTE) ?: ""
com.xayah.feature.main.verify.VerifyBackupPage(storageMode = storageMode, cloudName = cloudName, backupDir = backupDir)
}
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions source/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -420,4 +420,13 @@
<string name="notification_desc">This is needed to post necessary notifications</string>
<string name="permission_is_denied">Permission is denied</string>
<string name="root_denied_msg">Please check your root manager and restart app</string>
<string name="compression_type_title">Compression Type</string> <!-- Used in code as R.string.compression_type -->
<string name="compression_type_description">Select the compression type for backups. TWRP ZIP is compatible with TWRP recovery.</string> <!-- Used in code as R.string.compression_type_desc -->
<string name="verify_backup">Verify Backup</string>
<string name="verify_backup_desc">Check integrity of selected backup</string>
<string name="verify_backup_title">Verify Backup</string>
<string name="verifying_backup_in_progress">Verifying backup...</string>
<string name="verification_results">Verification Results</string>
<string name="verification_success">Backup verification successful. All files are intact.</string>
<string name="verification_failed">Backup verification failed. Some files may be corrupted.</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.map

// -----------------------------------------Keys-----------------------------------------
val KeyBackupSavePath = stringPreferencesKey("backup_save_path")
val KeyCompressionType = stringPreferencesKey("compression_type") // Added key
val KeyAppVersionName = stringPreferencesKey("app_version_name")
val KeyCloudActivatedAccountName = stringPreferencesKey("cloud_activated_account_name")
val KeyLoadedIconMD5 = stringPreferencesKey("loaded_icon_md5")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const val LZ4_SUFFIX = "tar.lz4"
enum class CompressionType(val type: String, val suffix: String, val compressPara: String, val decompressPara: String) {
TAR("tar", TAR_SUFFIX, "", ""),
ZSTD("zstd", ZSTD_SUFFIX, "zstd -r -T0 --ultra -q --priority=rt", "zstd"),
LZ4("lz4", LZ4_SUFFIX, "zstd -r -T0 --ultra -q --priority=rt --format=lz4", "zstd");
LZ4("lz4", LZ4_SUFFIX, "zstd -r -T0 --ultra -q --priority=rt --format=lz4", "zstd"),
TWRP_ZIP("zip", "zip", "", ""); // Added for TWRP backup

companion object
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,20 @@ data class PackageEntity(
val mediaSelected: Boolean
get() = dataStates.mediaState == DataState.Selected

// Added for TWRP backup selection
val backupApk: Boolean
get() = dataStates.apkState == DataState.Selected
val backupUser: Boolean
get() = dataStates.userState == DataState.Selected
val backupUserDe: Boolean
get() = dataStates.userDeState == DataState.Selected
val backupData: Boolean
get() = dataStates.dataState == DataState.Selected
val backupObb: Boolean
get() = dataStates.obbState == DataState.Selected
val backupMedia: Boolean
get() = dataStates.mediaState == DataState.Selected

companion object {
const val FLAG_NONE = 0
const val FLAG_APK = 1 // 000001
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ interface IRemoteRootService {
void setOpsMode(int code, int uid, String packageName, int mode);

String calculateMD5(String src);
ParcelFileDescriptor openFileForStreaming(String path); // New method for streaming
}
Original file line number Diff line number Diff line change
Expand Up @@ -516,4 +516,9 @@ internal class RemoteRootServiceImpl(private val context: Context) : IRemoteRoot
}

override fun calculateMD5(src: String): String = synchronized(lock) { HashUtil.calculateMD5(src) }

override fun openFileForStreaming(path: String): ParcelFileDescriptor = synchronized(lock) {
val file = File(path)
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -377,4 +377,7 @@ class RemoteRootService(private val context: Context) {
val bytes = readBytes(src = src)
ProtoBuf.decodeFromByteArray<T>(bytes)
}.onFailure(onFailure).getOrNull()

suspend fun openFileForStreaming(path: String): ParcelFileDescriptor? =
runCatching { getService().openFileForStreaming(path) }.onFailure(onFailure).getOrNull()
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import com.xayah.core.util.NotificationUtil
import com.xayah.core.util.PathUtil
import com.xayah.core.util.command.PreparationUtil
import kotlinx.coroutines.flow.first
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream

internal abstract class AbstractBackupService : AbstractPackagesService() {
override suspend fun onInitializingPreprocessingEntities(entities: MutableList<ProcessingInfoEntity>) {
Expand Down Expand Up @@ -160,44 +162,105 @@ internal abstract class AbstractBackupService : AbstractPackagesService() {
val dstDir = "${mAppsDir}/${p.archivesRelativeDir}"
var restoreEntity = mPackageDao.query(p.packageName, OpType.RESTORE, p.userId, p.preserveId, p.indexInfo.compressionType, mTaskEntity.cloud, mTaskEntity.backupDir)
mRootService.mkdirs(dstDir)
if (onAppDirCreated(archivesRelativeDir = p.archivesRelativeDir)) {
backup(type = DataType.PACKAGE_APK, p = p, r = restoreEntity, t = pkg, dstDir = dstDir)
backup(type = DataType.PACKAGE_USER, p = p, r = restoreEntity, t = pkg, dstDir = dstDir)
backup(type = DataType.PACKAGE_USER_DE, p = p, r = restoreEntity, t = pkg, dstDir = dstDir)
backup(type = DataType.PACKAGE_DATA, p = p, r = restoreEntity, t = pkg, dstDir = dstDir)
backup(type = DataType.PACKAGE_OBB, p = p, r = restoreEntity, t = pkg, dstDir = dstDir)
backup(type = DataType.PACKAGE_MEDIA, p = p, r = restoreEntity, t = pkg, dstDir = dstDir)
mPackagesBackupUtil.backupPermissions(p = p)
mPackagesBackupUtil.backupSsaid(p = p)

if (pkg.isSuccess) {
// Save config
if (p.indexInfo.compressionType == com.xayah.core.model.CompressionType.TWRP_ZIP) {
// TWRP ZIP Backup Logic
val zipFile = java.io.File(mAppsDir, "${p.packageName}_${p.userId}_${p.preserveId}.zip")
val checksums = mutableMapOf<String, Long>() // Map to store checksums
try {
ZipOutputStream(java.io.FileOutputStream(zipFile)).use { zos ->
val dataTypesToBackup = mutableListOf<DataType>()
if (p.backupApk) dataTypesToBackup.add(DataType.PACKAGE_APK)
if (p.backupUser) dataTypesToBackup.add(DataType.PACKAGE_USER)
if (p.backupUserDe) dataTypesToBackup.add(DataType.PACKAGE_USER_DE)
if (p.backupData) dataTypesToBackup.add(DataType.PACKAGE_DATA)
if (p.backupObb) dataTypesToBackup.add(DataType.PACKAGE_OBB)
if (p.backupMedia) dataTypesToBackup.add(DataType.PACKAGE_MEDIA)

dataTypesToBackup.forEach { dataType ->
val files = mPackagesBackupUtil.getFilesForDataType(p, dataType)
val basePath = when (dataType) {
DataType.PACKAGE_APK -> mPackagesBackupUtil.getPackageSourceDir(p.packageName, p.userId)
else -> mPackagesBackupUtil.packageRepository.getDataSrcDir(dataType, p.userId)
}
files.forEach { file ->
// Ensure basePath is not empty and file path is correctly relativized
val relativePath = if (basePath.isNotEmpty() && file.absolutePath.startsWith(basePath)) {
file.absolutePath.substring(basePath.length).trimStart('/')
} else {
file.name // Fallback if basePath is tricky or not applicable
}
val entryName = "${dataType.type}/$relativePath"
zos.putNextEntry(ZipEntry(entryName))
// Stream file content directly to ZIP
var crcValue: Long = 0
mRootService.openFileForStreaming(file.absolutePath)?.use { pfd ->
ParcelFileDescriptor.AutoCloseInputStream(pfd).use { fis ->
val checkedInputStream = java.util.zip.CheckedInputStream(fis, java.util.zip.CRC32())
checkedInputStream.copyTo(zos)
crcValue = checkedInputStream.checksum.value
}
} ?: log { "Warning: Could not open file ${file.absolutePath} for streaming into TWRP ZIP." }
zos.closeEntry()
checksums[entryName] = crcValue
}
}
// Add checksums.txt to ZIP
zos.putNextEntry(ZipEntry("checksums.txt"))
val checksumContent = checksums.entries.joinToString("\n") { "${it.key}:${it.value}" }
zos.write(checksumContent.toByteArray())
zos.closeEntry()
}
// Update package entity and task entity for success
p.extraInfo.lastBackupTime = DateUtil.getTimestamp()
val id = restoreEntity?.id ?: 0
restoreEntity = p.copy(
id = id,
indexInfo = p.indexInfo.copy(opType = OpType.RESTORE, cloud = mTaskEntity.cloud, backupDir = mTaskEntity.backupDir),
extraInfo = p.extraInfo.copy(activated = false)
)
val configDst = PathUtil.getPackageRestoreConfigDst(dstDir = dstDir)
mRootService.writeJson(data = restoreEntity, dst = configDst)
onConfigSaved(path = configDst, archivesRelativeDir = p.archivesRelativeDir)
mPackageDao.upsert(restoreEntity)
mPackageDao.upsert(p)
pkg.update(packageEntity = p)
pkg.update(packageEntity = p, state = OperationState.DONE)
mTaskEntity.update(successCount = mTaskEntity.successCount + 1)
} else {
} catch (e: Exception) {
log { "Error creating TWRP ZIP for ${p.packageName}: ${e.message}" }
pkg.update(state = OperationState.ERROR)
mTaskEntity.update(failureCount = mTaskEntity.failureCount + 1)
}
} else {
pkg.update(dataType = DataType.PACKAGE_APK, state = OperationState.ERROR)
pkg.update(dataType = DataType.PACKAGE_USER, state = OperationState.ERROR)
pkg.update(dataType = DataType.PACKAGE_USER_DE, state = OperationState.ERROR)
pkg.update(dataType = DataType.PACKAGE_DATA, state = OperationState.ERROR)
pkg.update(dataType = DataType.PACKAGE_OBB, state = OperationState.ERROR)
pkg.update(dataType = DataType.PACKAGE_MEDIA, state = OperationState.ERROR)
// Existing backup logic
if (onAppDirCreated(archivesRelativeDir = p.archivesRelativeDir)) {
backup(type = DataType.PACKAGE_APK, p = p, r = restoreEntity, t = pkg, dstDir = dstDir)
backup(type = DataType.PACKAGE_USER, p = p, r = restoreEntity, t = pkg, dstDir = dstDir)
backup(type = DataType.PACKAGE_USER_DE, p = p, r = restoreEntity, t = pkg, dstDir = dstDir)
backup(type = DataType.PACKAGE_DATA, p = p, r = restoreEntity, t = pkg, dstDir = dstDir)
backup(type = DataType.PACKAGE_OBB, p = p, r = restoreEntity, t = pkg, dstDir = dstDir)
backup(type = DataType.PACKAGE_MEDIA, p = p, r = restoreEntity, t = pkg, dstDir = dstDir)
mPackagesBackupUtil.backupPermissions(p = p)
mPackagesBackupUtil.backupSsaid(p = p)

if (pkg.isSuccess) {
// Save config
p.extraInfo.lastBackupTime = DateUtil.getTimestamp()
val id = restoreEntity?.id ?: 0
restoreEntity = p.copy(
id = id,
indexInfo = p.indexInfo.copy(opType = OpType.RESTORE, cloud = mTaskEntity.cloud, backupDir = mTaskEntity.backupDir),
extraInfo = p.extraInfo.copy(activated = false)
)
val configDst = PathUtil.getPackageRestoreConfigDst(dstDir = dstDir)
mRootService.writeJson(data = restoreEntity, dst = configDst)
onConfigSaved(path = configDst, archivesRelativeDir = p.archivesRelativeDir)
mPackageDao.upsert(restoreEntity)
mPackageDao.upsert(p)
pkg.update(packageEntity = p)
mTaskEntity.update(successCount = mTaskEntity.successCount + 1)
} else {
mTaskEntity.update(failureCount = mTaskEntity.failureCount + 1)
}
} else {
pkg.update(dataType = DataType.PACKAGE_APK, state = OperationState.ERROR)
pkg.update(dataType = DataType.PACKAGE_USER, state = OperationState.ERROR)
pkg.update(dataType = DataType.PACKAGE_USER_DE, state = OperationState.ERROR)
pkg.update(dataType = DataType.PACKAGE_DATA, state = OperationState.ERROR)
pkg.update(dataType = DataType.PACKAGE_OBB, state = OperationState.ERROR)
pkg.update(dataType = DataType.PACKAGE_MEDIA, state = OperationState.ERROR)
}
pkg.update(state = if (pkg.isSuccess) OperationState.DONE else OperationState.ERROR)
}
pkg.update(state = if (pkg.isSuccess) OperationState.DONE else OperationState.ERROR)
}
mTaskEntity.update(processingIndex = mTaskEntity.processingIndex + 1)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import java.io.File
import javax.inject.Inject
import kotlin.coroutines.coroutineContext

Expand Down Expand Up @@ -407,4 +408,65 @@ class PackagesBackupUtil @Inject constructor(
t.updateInfo(dataType = dataType, state = if (isSuccess) OperationState.DONE else OperationState.ERROR, log = t.getLog(dataType) + "\n${outString}", content = "100%")
}
}

// New method to get files for a specific data type
suspend fun getFilesForDataType(p: PackageEntity, dataType: DataType): List<File> {
val files = mutableListOf<File>()
val packageName = p.packageName
val userId = p.userId
val srcDir: String

when (dataType) {
DataType.PACKAGE_APK -> {
srcDir = getPackageSourceDir(packageName, userId)
if (srcDir.isNotEmpty()) {
rootService.listDir(srcDir)?.filter { it.endsWith(".apk") }?.forEach { apkName ->
files.add(File(srcDir, apkName))
}
}
}
DataType.PACKAGE_USER, DataType.PACKAGE_USER_DE, DataType.PACKAGE_DATA, DataType.PACKAGE_OBB, DataType.PACKAGE_MEDIA -> {
srcDir = packageRepository.getDataSrcDir(dataType, userId)
val dataPath = packageRepository.getDataSrc(srcDir, packageName)
if (rootService.exists(dataPath)) {
addFilesRecursively(dataPath, files, dataPath) // Pass dataPath as basePath
}
}
else -> {
// Not handled or not applicable for TWRP backup
}
}
return files
}

// Made public to be accessible from AbstractBackupService
suspend fun getPackageSourceDir(packageName: String, userId: Int) = rootService.getPackageSourceDir(packageName, userId).let { list ->
if (list.isNotEmpty()) PathUtil.getParentPath(list[0]) else ""
}

private suspend fun addFilesRecursively(currentPath: String, fileList: MutableList<File>, basePath: String) {
val items = rootService.listDir(currentPath)
items?.forEach { item ->
val itemFile = File(currentPath, item)
// Check if it's a file or directory using rootService
// This is a simplified check; you might need a more specific way to differentiate
// or rely on `rootService.isDirectory(itemFile.absolutePath)` if available.
// For now, assuming if it's not ending with typical file extensions or if `listDir` on it returns non-null, it's a directory.
// This part needs a reliable way to check if 'itemFile' is a directory.
// Let's assume `rootService.isDirectory(itemFile.absolutePath)` exists for this example.
// if (rootService.isDirectory(itemFile.absolutePath)) { // This is hypothetical
// addFilesRecursively(itemFile.absolutePath, fileList, basePath)
// } else {
// fileList.add(itemFile)
// }
// Fallback: Add the file/dir and let ZIP handling figure it out, or refine this logic.
// The key challenge is determining if 'itemFile' is a directory without direct FS access.
// A common approach is to try listing its contents; if successful, it's a directory.
if (rootService.listDir(itemFile.absolutePath) != null && !rootService.isSymlink(itemFile.absolutePath)) { // Check if it's a directory and not a symlink
addFilesRecursively(itemFile.absolutePath, fileList, basePath)
} else if (rootService.exists(itemFile.absolutePath) && !rootService.isSymlink(itemFile.absolutePath)) { // Check if it's a file and not a symlink
fileList.add(itemFile)
}
}
}
}
Loading
Loading