Skip to content
Merged
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 @@ -26,6 +26,7 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.GZIPOutputStream;

public final class BackupEngine {
Expand Down Expand Up @@ -63,10 +64,13 @@ public static String suggestedFileName(Context ctx) {
* (over {@code dest}), so the bytes tar produces are observable and metered against the tree's total size
* ({@code du -sb}). Byte progress tracks the gzip+write time far better than a member count, which
* front-loads on the many small files and then stalls on the few large ones. When {@code listener} is
* null or the pre-count fails, it streams without progress (indeterminate). This Java loop is also the
* seam a later backup-cancel interrupts.
* null or the pre-count fails, it streams without progress (indeterminate).
*
* <p>K2GO-384: {@code cancelled} (may be null) aborts the stream — the read loop kills tar and stops. The
* caller distinguishes a cancel from a failure by that same flag and removes the incomplete SAF file.
*/
public static boolean streamBackup(Context ctx, OutputStream dest, ProgressListener listener) {
public static boolean streamBackup(Context ctx, OutputStream dest, ProgressListener listener,
AtomicBoolean cancelled) {
File iiabRootDir = new File(ctx.getFilesDir(), "rootfs");
File nativeDir = new File(ctx.getApplicationInfo().nativeLibraryDir);
File staticTar = new File(nativeDir, "libtar.so");
Expand Down Expand Up @@ -126,6 +130,8 @@ public static boolean streamBackup(Context ctx, OutputStream dest, ProgressListe
final long startNs = System.nanoTime();
int n;
while ((n = in.read(buf)) > 0) {
// K2GO-384: cancel stops tar mid-stream (backup is read-only, so this is always safe).
if (cancelled != null && cancelled.get()) { p.destroy(); break; }
gz.write(buf, 0, n);
processed += n;
if (listener != null && totalBytes > 0L) {
Expand All @@ -143,9 +149,10 @@ public static boolean streamBackup(Context ctx, OutputStream dest, ProgressListe
int exit = p.waitFor();
if (errDrain != null) { try { errDrain.join(1500); } catch (InterruptedException ignored) { } }
String tail; synchronized (errTail) { tail = errTail.toString().trim(); }
if (exit != 0) {
final boolean userCancelled = cancelled != null && cancelled.get();
if (exit != 0 && !userCancelled) { // a cancel kills tar (non-zero) on purpose -- not a failure to log
Log.w(TAG, "backup tar exited " + exit + (tail.isEmpty() ? "" : "; stderr tail:\n" + tail));
} else if (!tail.isEmpty()) {
} else if (exit == 0 && !tail.isEmpty()) {
// K2GO-384: succeeded, but --ignore-failed-read let tar skip unreadable entries. Expected ones
// are proot mount stubs (iiab/sdcard, ...); RECORD them so a genuinely dropped file is never
// silent (the honesty this slice restored -- the old tar|gzip pipe masked all of this).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ public final class DeepOpService extends Service {
* pass outcome (complete / error / cancelled / held). Guards the re-callable attemptVerifyAndExtract so a
* "Keep restoring" cannot start a second pass over the same temp while one is running. */
private volatile boolean passRunning = false;
/** K2GO-384: cancel for the (read-only, single-pass) BACKUP. streamBackup's read loop reads it and kills
* tar. Always safe (nothing on the device is touched); the terminal removes the incomplete SAF file. */
private final java.util.concurrent.atomic.AtomicBoolean backupCancelled = new java.util.concurrent.atomic.AtomicBoolean(false);
/** K2GO-384: true while the user is deciding on a paused COPY (Cancel pressed). The copy loop blocks on
* it; ACTION_RESUME clears it (continue), ACTION_CANCEL_CONFIRM sets cancelBeforeExtract and clears it
* (abort). Only the copy is pausable -- verify/extract are external tar processes. */
Expand Down Expand Up @@ -201,25 +204,44 @@ public int onStartCommand(Intent intent, int flags, int startId) {
// ---- BACKUP (read-only) ----
private void runBackup(final String uriStr) {
if (done) return;
currentCancelKind = DeepOpState.CancelKind.CANCELLABLE; // K2GO-384: backup is cancellable throughout (read-only)
setStep(getString(R.string.k2go_br_status_backing), -1);
AppExecutors.get().io().execute(() -> {
boolean ok;
try (OutputStream os = getContentResolver().openOutputStream(Uri.parse(uriStr))) {
// K2GO-384: byte-accurate progress + ETA; streamBackup reports from its tar-stdout read loop.
// post() is thread-safe and carries cancelKind=NONE (backup's Cancel stays on the notification,
// so no on-screen Cancel appears).
// post() is thread-safe. backupCancelled lets the on-screen (or notification) Cancel stop tar.
ok = os != null && BackupEngine.streamBackup(this, os,
(percent, etaSeconds) -> post(getString(R.string.k2go_br_status_backing), percent, etaSeconds));
(percent, etaSeconds) -> post(getString(R.string.k2go_br_status_backing), percent, etaSeconds),
backupCancelled);
} catch (Exception e) {
Log.e(TAG, "backup failed", e);
ok = false;
}
final boolean success = ok;
main.post(() -> finishJob(success,
getString(R.string.k2go_br_backup_done), getString(R.string.k2go_br_backup_failed)));
main.post(() -> {
if (done) return;
if (!success) {
// K2GO-384: an unfinished backup left an incomplete/damaged .tar.gz at the SAF destination.
// Removing it is the default (no prompt) -- a partial gzip'd tar is useless and unresumable.
deleteBackupDoc(uriStr);
if (backupCancelled.get()) { finishCancelled(); return; } // user cancel -> bifurcation
}
finishJob(success, getString(R.string.k2go_br_backup_done), getString(R.string.k2go_br_backup_failed));
});
});
}

/** K2GO-384: remove the incomplete/damaged backup file a cancelled or failed run left at the SAF
* destination. Best-effort -- a document picked via CREATE_DOCUMENT supports delete; if not, leave it. */
private void deleteBackupDoc(String uriStr) {
try {
android.provider.DocumentsContract.deleteDocument(getContentResolver(), Uri.parse(uriStr));
} catch (Exception e) {
Log.w(TAG, "could not delete incomplete backup doc: " + e.getMessage());
}
}

// ---- RESTORE (destructive) ----

/**
Expand Down Expand Up @@ -550,13 +572,14 @@ private void finishCancelled() {
}

/**
* Notification "Cancel" — offered only for BACKUP (read-only, safe to abandon). Restore has no
* Cancel action (destructive, hard gate). The in-flight backup stream sees {@code done} and no-ops
* on completion. Runs the same cleanup as a failure so the lock is released and the op ends.
* Cancel (from the on-screen button's confirm, or the notification action — BACKUP only). Restore has no
* Cancel action here (it takes the CANCELLABLE hold / DESTRUCTIVE force-cancel paths below).
*/
private void cancel() {
if (owner == EnvironmentLock.Owner.BACKUP) {
finishJob(false, "", getString(R.string.k2go_br_backup_failed));
// K2GO-384: stop the backup for real -- the read loop sees this, kills tar, and the terminal
// removes the incomplete file (was: mark done + FAILED, which left tar running and the file behind).
backupCancelled.set(true);
return;
}
// K2GO-384: a Cancel on any pre-destructive pass (CANCELLABLE) HOLDS the run and waits for the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,9 @@ private void setStatusAnimated(String text) {
*/
private void onCancelTapped() {
if (!isAdded()) return;
// K2GO-384: backup is read-only + single-pass, so its cancel is trivially safe -- a light guard dialog
// (against an accidental tap), then stop tar + remove the incomplete file. No hold/pause, no phases.
if (!isRestore()) { showBackupCancelDialog(); return; }
if (lastCancelKind == DeepOpState.CancelKind.DESTRUCTIVE) { showDestructiveCancelDialog(); return; }
if (lastCancelKind != DeepOpState.CancelKind.CANCELLABLE) return;
sendToService(DeepOpService.ACTION_CANCEL); // hold the run while the user decides (no race)
Expand All @@ -323,6 +326,23 @@ private void sendToService(String action) {
new android.content.Intent(requireContext(), DeepOpService.class).setAction(action));
}

/**
* K2GO-384: backup is read-only, so cancelling is always safe -- this dialog only guards an accidental tap.
* Confirming stops the backup and removes the incomplete file (the default; a partial archive is useless).
* Cancelable: nothing is paused, so a scrim/Back dismiss simply keeps the backup running.
*/
private void showBackupCancelDialog() {
new BrandDialog(requireContext())
.setTitle(getString(R.string.k2go_br_cancel_backup_title))
.setMessage(getString(R.string.k2go_br_cancel_backup_body))
.setPositive(R.string.k2go_br_cancel_backup_confirm, () -> {
if (cancel != null) cancel.setEnabled(false);
sendToService(DeepOpService.ACTION_CANCEL); // stop tar -> incomplete file removed -> CANCELLED
})
.setNegative(R.string.k2go_br_cancel_backup_keep, null) // dismiss = keep backing up
.show();
}

/**
* K2GO-384: cancelling DURING the extract is destructive -- the rootfs is being overwritten. A strong
* M3 dialog: RED confirm (colorError) gated by an acknowledgement checkbox. Confirming force-cancels the
Expand Down
4 changes: 4 additions & 0 deletions controller/app/src/main/res/values-ar/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,10 @@
<string name="k2go_br_cancel_extract_ack">أفهم أن الإلغاء الآن قد يترك نظامي تالفًا أو غير قابل للاستخدام.</string>
<string name="k2go_br_restore_damaged">تم إلغاء الاستعادة. سيبدأ نظامك في وضع الاسترداد عند التشغيل التالي.</string>
<string name="k2go_br_cancel_extract_need_ack">حدد المربع لتأكيد فهمك.</string>
<string name="k2go_br_cancel_backup_title">إلغاء النسخة الاحتياطية؟</string>
<string name="k2go_br_cancel_backup_body">لم تكتمل هذه النسخة الاحتياطية. يؤدي الإلغاء إلى إزالة الملف غير المكتمل — يمكنك بدء نسخة احتياطية جديدة في أي وقت.</string>
<string name="k2go_br_cancel_backup_confirm">إلغاء النسخة الاحتياطية</string>
<string name="k2go_br_cancel_backup_keep">متابعة النسخ الاحتياطي</string>
<string name="k2go_br_restore_unreadable">تعذّرت قراءة الملف المحدد، لذا لم يتغيّر أي شيء.</string>
<string name="deepop_channel_name">النسخ الاحتياطي والاستعادة</string>
<string name="deepop_channel_desc">يُبقي عملية نسخ احتياطي أو استعادة قيد التشغيل بأمان في الخلفية</string>
Expand Down
4 changes: 4 additions & 0 deletions controller/app/src/main/res/values-az/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,10 @@
<string name="k2go_br_cancel_extract_ack">İndi ləğv etməyin sistemimi zədələnmiş və ya yararsız qoya biləcəyini başa düşürəm.</string>
<string name="k2go_br_restore_damaged">Bərpa ləğv edildi. Sisteminiz növbəti işə salınmada bərpa rejimində başlayacaq.</string>
<string name="k2go_br_cancel_extract_need_ack">Başa düşdüyünüzü təsdiqləmək üçün qutunu işarələyin.</string>
<string name="k2go_br_cancel_backup_title">Ehtiyat nüsxə ləğv edilsin?</string>
<string name="k2go_br_cancel_backup_body">Bu ehtiyat nüsxə tamamlanmayıb. Ləğv etmək yarımçıq faylı silir — istənilən vaxt yeni ehtiyat nüsxə başlada bilərsiniz.</string>
<string name="k2go_br_cancel_backup_confirm">Ehtiyat nüsxəni ləğv et</string>
<string name="k2go_br_cancel_backup_keep">Ehtiyatlamağa davam et</string>
<string name="k2go_br_restore_unreadable">Seçilmiş fayl oxuna bilmədi, ona görə heç nə dəyişdirilmədi.</string>
<string name="deepop_channel_name">Ehtiyat nüsxə &amp; bərpa</string>
<string name="deepop_channel_desc">Ehtiyat nüsxə və ya bərpanı arxa planda təhlükəsiz işlədir</string>
Expand Down
4 changes: 4 additions & 0 deletions controller/app/src/main/res/values-bg/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,10 @@
<string name="k2go_br_cancel_extract_ack">Разбирам, че отказът сега може да остави системата ми повредена или неизползваема.</string>
<string name="k2go_br_restore_damaged">Възстановяването е отказано. Системата ви ще стартира в режим на възстановяване при следващото стартиране.</string>
<string name="k2go_br_cancel_extract_need_ack">Отметнете полето, за да потвърдите, че разбирате.</string>
<string name="k2go_br_cancel_backup_title">Отказ от резервното копие?</string>
<string name="k2go_br_cancel_backup_body">Това резервно копие не е завършено. Отказът премахва незавършения файл — можете да започнете ново резервно копие по всяко време.</string>
<string name="k2go_br_cancel_backup_confirm">Отказ от резервното копие</string>
<string name="k2go_br_cancel_backup_keep">Продължи архивирането</string>
<string name="k2go_br_restore_unreadable">Избраният файл не можа да бъде прочетен, затова нищо не беше променено.</string>
<string name="deepop_channel_name">Резервно копие и възстановяване</string>
<string name="deepop_channel_desc">Поддържа безопасното изпълнение на резервно копие или възстановяване във фонов режим</string>
Expand Down
4 changes: 4 additions & 0 deletions controller/app/src/main/res/values-bn/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,10 @@
<string name="k2go_br_cancel_extract_ack">আমি বুঝি যে এখন বাতিল করলে আমার সিস্টেম ক্ষতিগ্রস্ত বা ব্যবহারের অযোগ্য হয়ে যেতে পারে।</string>
<string name="k2go_br_restore_damaged">পুনরুদ্ধার বাতিল করা হয়েছে। পরবর্তী চালুতে আপনার সিস্টেম রিকভারি মোডে শুরু হবে।</string>
<string name="k2go_br_cancel_extract_need_ack">আপনি বুঝেছেন তা নিশ্চিত করতে বাক্সটি চেক করুন।</string>
<string name="k2go_br_cancel_backup_title">ব্যাকআপ বাতিল করবেন?</string>
<string name="k2go_br_cancel_backup_body">এই ব্যাকআপ শেষ হয়নি। বাতিল করলে অসম্পূর্ণ ফাইলটি সরানো হবে — আপনি যেকোনো সময় নতুন ব্যাকআপ শুরু করতে পারেন।</string>
<string name="k2go_br_cancel_backup_confirm">ব্যাকআপ বাতিল করুন</string>
<string name="k2go_br_cancel_backup_keep">ব্যাকআপ চালিয়ে যান</string>
<string name="k2go_br_restore_unreadable">নির্বাচিত ফাইলটি পড়া যায়নি, তাই কিছুই পরিবর্তন করা হয়নি।</string>
<string name="deepop_channel_name">ব্যাকআপ ও পুনরুদ্ধার</string>
<string name="deepop_channel_desc">ব্যাকআপ বা পুনরুদ্ধার ব্যাকগ্রাউন্ডে নিরাপদে চালু রাখে</string>
Expand Down
4 changes: 4 additions & 0 deletions controller/app/src/main/res/values-cs/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,10 @@
<string name="k2go_br_cancel_extract_ack">Rozumím, že zrušení nyní může můj systém poškodit nebo učinit nepoužitelným.</string>
<string name="k2go_br_restore_damaged">Obnovení zrušeno. Váš systém se při příštím spuštění spustí v režimu obnovy.</string>
<string name="k2go_br_cancel_extract_need_ack">Zaškrtnutím políčka potvrďte, že rozumíte.</string>
<string name="k2go_br_cancel_backup_title">Zrušit zálohu?</string>
<string name="k2go_br_cancel_backup_body">Tato záloha není dokončena. Zrušením se odstraní neúplný soubor — novou zálohu můžete kdykoli spustit.</string>
<string name="k2go_br_cancel_backup_confirm">Zrušit zálohu</string>
<string name="k2go_br_cancel_backup_keep">Pokračovat v zálohování</string>
<string name="k2go_br_restore_unreadable">Vybraný soubor se nepodařilo přečíst, takže se nic nezměnilo.</string>
<string name="deepop_channel_name">Záloha &amp; obnova</string>
<string name="deepop_channel_desc">Udržuje zálohu nebo obnovu bezpečně spuštěnou na pozadí</string>
Expand Down
4 changes: 4 additions & 0 deletions controller/app/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,10 @@
<string name="k2go_br_cancel_extract_ack">Ich verstehe, dass ein Abbruch jetzt mein System beschädigen oder unbrauchbar machen kann.</string>
<string name="k2go_br_restore_damaged">Wiederherstellung abgebrochen. Ihr System startet beim nächsten Start im Wiederherstellungsmodus.</string>
<string name="k2go_br_cancel_extract_need_ack">Aktivieren Sie das Kontrollkästchen, um zu bestätigen, dass Sie es verstanden haben.</string>
<string name="k2go_br_cancel_backup_title">Sicherung abbrechen?</string>
<string name="k2go_br_cancel_backup_body">Diese Sicherung ist nicht abgeschlossen. Beim Abbrechen wird die unvollständige Datei entfernt — Sie können jederzeit eine neue Sicherung starten.</string>
<string name="k2go_br_cancel_backup_confirm">Sicherung abbrechen</string>
<string name="k2go_br_cancel_backup_keep">Sicherung fortsetzen</string>
<string name="k2go_br_restore_unreadable">Die ausgewählte Datei konnte nicht gelesen werden, daher wurde nichts geändert.</string>
<string name="deepop_channel_name">Sichern &amp; wiederherstellen</string>
<string name="deepop_channel_desc">Hält ein Backup oder eine Wiederherstellung sicher im Hintergrund am Laufen</string>
Expand Down
Loading
Loading