diff --git a/android/app/src/main/java/tv/orbit/classic/InstallSnapshot.kt b/android/app/src/main/java/tv/orbit/classic/InstallSnapshot.kt index d55df810..115f7b7f 100644 --- a/android/app/src/main/java/tv/orbit/classic/InstallSnapshot.kt +++ b/android/app/src/main/java/tv/orbit/classic/InstallSnapshot.kt @@ -15,18 +15,29 @@ import java.security.MessageDigest * - `rollback `: copies the schedules aside as schedule-rollback-, as every install does. * - `catalogue`: a read-only copy of the library catalogue beside it, for checking new channels' titles against the library. * - `clear`: removes the copy once it's been pulled. + * - `restore` (8 November 2026, audit U12): puts a pulled snapshot back. See [InstallRestore]: only with `confirm RESTORE`, + * only while Orbit's screen isn't open, only files a snapshot takes, each checked against its checksum first, and the + * current data is kept aside before anything is replaced. * - * It only ever copies; it never changes the schedules, the library or the sign-in. + * Apart from `restore`, it only ever copies; it never changes the schedules, the library or the sign-in. */ class InstallSnapshotReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - val result = runCatching { InstallSnapshot.handle(context, intent.getStringExtra("do").orEmpty(), intent.getStringExtra("version")) } + val result = runCatching { + if (intent.getStringExtra("do") == "restore") { + if (InstallSnapshot.screenOpen) "refused: Orbit is open; force-stop it first" + else InstallRestore.apply(context.filesDir.parentFile!!, File(context.getExternalFilesDir(null), "install-restore"), + intent.getStringExtra("confirm"), System.currentTimeMillis()) + } else InstallSnapshot.handle(context, intent.getStringExtra("do").orEmpty(), intent.getStringExtra("version")) + } resultData = result.fold({ it }, { "failed" }) android.util.Log.i("OrbitInstall", "${intent.getStringExtra("do")}: $resultData") } } internal object InstallSnapshot { + /** Orbit's screen is open in this process (set by MainActivity); a restore waits until it isn't. */ + @Volatile var screenOpen = false private val versionShape = Regex("""\d+\.\d+\.\d+""") fun handle(context: Context, action: String, version: String?): String { @@ -68,3 +79,57 @@ internal object InstallSnapshot { digest.digest().joinToString("") { "%02x".format(it) } } } + +/** + * Putting a snapshot back (8 November 2026, audit U12: Android won't install an older Orbit over a newer one, so without + * this a bad install could only be fixed forward). adb pushes a pulled snapshot to Orbit's shared folder as + * install-restore/, with restore.txt listing each file's SHA-256 (`sha256sum` format). Then, with Orbit force-stopped: + * + * - nothing happens without `confirm RESTORE`; + * - only what a snapshot takes can come back: no_backup/schedule/... and the settings and profiles prefs. Never the + * library catalogue, the sign-in or the PIN; + * - every listed file is checked against its checksum before anything is touched, and any problem refuses the lot; + * - the current schedules and prefs are kept aside as no_backup/before-restore-