From e7ca8d26af314f19a8a586fc5229ad35c535b9de Mon Sep 17 00:00:00 2001 From: CAtlas0784 Date: Wed, 7 Oct 2026 23:30:40 +0700 Subject: [PATCH] release: v1.0.1 - add restore backup button and bump version --- app/build.gradle.kts | 4 +- .../escapepenacony/data/StorageManager.kt | 63 ++++++++++++++- .../catlas/escapepenacony/ui/MainScreen.kt | 80 ++++++++++++------- 3 files changed, 114 insertions(+), 33 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3a8f1be..26552d3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "com.catlas.escapepenacony" minSdk = 26 targetSdk = 35 - versionCode = 1 - versionName = "v1.0.0" + versionCode = 2 + versionName = "v1.0.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/catlas/escapepenacony/data/StorageManager.kt b/app/src/main/java/com/catlas/escapepenacony/data/StorageManager.kt index 8795c87..cde65f3 100644 --- a/app/src/main/java/com/catlas/escapepenacony/data/StorageManager.kt +++ b/app/src/main/java/com/catlas/escapepenacony/data/StorageManager.kt @@ -14,11 +14,12 @@ object StorageManager { private const val PREFS_NAME = "escape_penacony_prefs" private const val KEY_PERSISTENT_URI = "persistent_uri" private const val KEY_SELECTED_LANG = "selected_language" - private const val KEY_PREFER_ROOT = "prefer_root_mode" const val PPSR_PACKAGE_NAME = "com.afn.ppSR" const val PPSR_INTERNAL_PERSISTENT_PATH = "/data/data/com.afn.ppSR/files/server/build/build/misc/persistent.json" + const val PPSR_INTERNAL_BACKUP_PATH = + "/data/data/com.afn.ppSR/files/server/build/build/misc/persistent.json.bak" fun savePersistentUri(context: Context, uri: Uri) { context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) @@ -101,6 +102,16 @@ object StorageManager { val originalContent = readProcess.inputStream.bufferedReader().use { it.readText().trim() } readProcess.waitFor() + // Save backup both inside ppSR folder and in local app cache + if (originalContent.isNotEmpty()) { + try { + val bakCmd = "cp \"$PPSR_INTERNAL_PERSISTENT_PATH\" \"$PPSR_INTERNAL_BACKUP_PATH\"" + Runtime.getRuntime().exec(arrayOf("su", "-c", bakCmd)).waitFor() + val cacheBackup = File(context.cacheDir, "persistent_backup.json") + cacheBackup.writeText(originalContent, Charsets.UTF_8) + } catch (_: Exception) {} + } + val updatedJson = PersistentHelper.updateJsonString( if (originalContent.isNotEmpty()) originalContent else "{}", newConfig @@ -127,9 +138,55 @@ object StorageManager { } } + suspend fun restoreBackup(context: Context, isRootMode: Boolean, uri: Uri?): Result = + withContext(Dispatchers.IO) { + try { + if (isRootMode) { + // Try root backup file first + val checkCmd = "[ -f \"$PPSR_INTERNAL_BACKUP_PATH\" ] && echo 'BAK_EXISTS'" + val checkP = Runtime.getRuntime().exec(arrayOf("su", "-c", checkCmd)) + val checkOut = checkP.inputStream.bufferedReader().use { it.readText().trim() } + checkP.waitFor() + + if (checkOut == "BAK_EXISTS") { + val restoreCmd = "cp \"$PPSR_INTERNAL_BACKUP_PATH\" \"$PPSR_INTERNAL_PERSISTENT_PATH\" && chmod 666 \"$PPSR_INTERNAL_PERSISTENT_PATH\"" + val p = Runtime.getRuntime().exec(arrayOf("su", "-c", restoreCmd)) + val exit = p.waitFor() + if (exit == 0) return@withContext Result.success(Unit) + } + + // Fallback to cache backup + val cacheBackup = File(context.cacheDir, "persistent_backup.json") + if (cacheBackup.exists()) { + val copyCmd = "cp \"${cacheBackup.absolutePath}\" \"$PPSR_INTERNAL_PERSISTENT_PATH\" && chmod 666 \"$PPSR_INTERNAL_PERSISTENT_PATH\"" + val p = Runtime.getRuntime().exec(arrayOf("su", "-c", copyCmd)) + val exit = p.waitFor() + if (exit == 0) return@withContext Result.success(Unit) + } + + Result.failure(Exception("No backup file found to restore")) + } else { + // SAF fallback + if (uri == null) return@withContext Result.failure(Exception("No persistent.json selected")) + val cacheBackup = File(context.cacheDir, "persistent_backup.json") + if (!cacheBackup.exists()) return@withContext Result.failure(Exception("No backup file found")) + + val backupContent = cacheBackup.readText(Charsets.UTF_8) + context.contentResolver.openOutputStream(uri, "wt")?.use { output -> + output.write(backupContent.toByteArray(Charsets.UTF_8)) + output.flush() + } ?: return@withContext Result.failure(Exception("Failed to open output stream")) + + Result.success(Unit) + } + } catch (e: Exception) { + e.printStackTrace() + Result.failure(e) + } + } + suspend fun restartPpsrServerViaRoot(): Result = withContext(Dispatchers.IO) { try { - // Force-stop then relaunch MainActivity val restartCmd = "am force-stop $PPSR_PACKAGE_NAME && sleep 0.5 && am start -n $PPSR_PACKAGE_NAME/.MainActivity" val process = Runtime.getRuntime().exec(arrayOf("su", "-c", restartCmd)) val exit = process.waitFor() @@ -162,7 +219,7 @@ object StorageManager { try { val backupFile = File(context.cacheDir, "persistent_backup.json") - backupFile.writeText(originalContent) + backupFile.writeText(originalContent, Charsets.UTF_8) } catch (_: Exception) {} val updatedJson = PersistentHelper.updateJsonString(originalContent, newConfig) diff --git a/app/src/main/java/com/catlas/escapepenacony/ui/MainScreen.kt b/app/src/main/java/com/catlas/escapepenacony/ui/MainScreen.kt index 8912ed4..917ecbf 100644 --- a/app/src/main/java/com/catlas/escapepenacony/ui/MainScreen.kt +++ b/app/src/main/java/com/catlas/escapepenacony/ui/MainScreen.kt @@ -303,42 +303,66 @@ fun MainScreen( } } - if (isRootMode) { - // Restart ppSR Server button - OutlinedButton( + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) { + // Restore / Revert Backup Button + IconButton( onClick = { coroutineScope.launch { - Toast.makeText(context, "Restarting ppSR Server...", Toast.LENGTH_SHORT).show() - val res = StorageManager.restartPpsrServerViaRoot() + val res = StorageManager.restoreBackup(context, isRootMode, currentPersistentUri) res.onSuccess { - Toast.makeText(context, "ppSR Server restarted successfully!", Toast.LENGTH_SHORT).show() - }.onFailure { - Toast.makeText(context, "Restart failed: ${it.message}", Toast.LENGTH_SHORT).show() + if (isRootMode) { + currentActiveConfig = StorageManager.readConfigViaRoot() + } else if (currentPersistentUri != null) { + currentActiveConfig = StorageManager.readCurrentConfig(context, currentPersistentUri) + } + Toast.makeText(context, "Restored backup persistent.json successfully!", Toast.LENGTH_SHORT).show() + }.onFailure { err -> + Toast.makeText(context, "Restore failed: ${err.message}", Toast.LENGTH_SHORT).show() } } }, - shape = RoundedCornerShape(8.dp), - contentPadding = PaddingValues(horizontal = 8.dp, vertical = 2.dp), - modifier = Modifier.height(32.dp) + modifier = Modifier.size(34.dp) ) { - Icon(Icons.Default.Refresh, contentDescription = "Restart", modifier = Modifier.size(13.dp), tint = NeonCyan) - Spacer(modifier = Modifier.width(4.dp)) - Text("Restart ppSR", fontSize = 11.sp, color = NeonCyan) + Icon(Icons.Default.Restore, contentDescription = "Restore Backup", tint = StarGold, modifier = Modifier.size(20.dp)) } - } else { - Button( - onClick = onPickPersistentFile, - shape = RoundedCornerShape(8.dp), - colors = ButtonDefaults.buttonColors( - containerColor = if (currentPersistentUri != null) DarkSurfaceElevated else PurplePrimary - ), - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), - modifier = Modifier.height(32.dp) - ) { - Text( - text = if (currentPersistentUri != null) "Change" else "Choose File", - fontSize = 11.sp - ) + + if (isRootMode) { + // Restart ppSR Server button + OutlinedButton( + onClick = { + coroutineScope.launch { + Toast.makeText(context, "Restarting ppSR Server...", Toast.LENGTH_SHORT).show() + val res = StorageManager.restartPpsrServerViaRoot() + res.onSuccess { + Toast.makeText(context, "ppSR Server restarted successfully!", Toast.LENGTH_SHORT).show() + }.onFailure { + Toast.makeText(context, "Restart failed: ${it.message}", Toast.LENGTH_SHORT).show() + } + } + }, + shape = RoundedCornerShape(8.dp), + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 2.dp), + modifier = Modifier.height(32.dp) + ) { + Icon(Icons.Default.Refresh, contentDescription = "Restart", modifier = Modifier.size(13.dp), tint = NeonCyan) + Spacer(modifier = Modifier.width(4.dp)) + Text("Restart ppSR", fontSize = 11.sp, color = NeonCyan) + } + } else { + Button( + onClick = onPickPersistentFile, + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = if (currentPersistentUri != null) DarkSurfaceElevated else PurplePrimary + ), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), + modifier = Modifier.height(32.dp) + ) { + Text( + text = if (currentPersistentUri != null) "Change" else "Choose File", + fontSize = 11.sp + ) + } } } }