From 2042269df7952c3b58b42d9a44e1e7da7952e617 Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Sat, 18 Jul 2026 18:53:59 +0200 Subject: [PATCH 1/2] app: make the import re-entry guard survive activity recreation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit importInProgress was a per-instance field, so a rotation mid-import reset it while the detached worker kept copying, and the always-live Import menu let a second worker start and race the first onto the same .part file — leaving a short file that dest.exists() then treats as complete. It is now a static AtomicBoolean, claimed with compareAndSet and cleared in the worker's finally, so a recreated activity sees the in-flight import and a failing import cannot wedge the guard. The claim moved after the idempotent SAF setup so a throw there cannot wedge it either. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Xavier Roche --- .../com/httrack/android/HTTrackActivity.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/httrack/android/HTTrackActivity.java b/app/src/main/java/com/httrack/android/HTTrackActivity.java index 3e138ec..b0d1a17 100755 --- a/app/src/main/java/com/httrack/android/HTTrackActivity.java +++ b/app/src/main/java/com/httrack/android/HTTrackActivity.java @@ -42,6 +42,7 @@ import java.util.Enumeration; import java.util.HashSet; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -2250,7 +2251,8 @@ private void startLegacyMirrorImport() { } } - private volatile boolean importInProgress; + // Process-wide: a rotation recreates the activity, but the detached worker keeps copying. + private static final AtomicBoolean importInProgress = new AtomicBoolean(); /** * Copy the picked tree into our Websites directory, entirely off the UI thread: even walking @@ -2259,26 +2261,24 @@ private void startLegacyMirrorImport() { * threads would race on the same temp files. */ private void importMirrorsFrom(final Uri treeUri) { - if (importInProgress) { - showNotification(getString(R.string.import_mirrors_running)); - return; - } final ContentResolver resolver = getContentResolver(); resolver.takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); final Context appContext = getApplicationContext(); final File dest = getProjectRootFile(); - importInProgress = true; + // Claim the guard last, so a synchronous throw above cannot wedge it. + if (!importInProgress.compareAndSet(false, true)) { + showNotification(getString(R.string.import_mirrors_running)); + return; + } showNotification(getString(R.string.import_mirrors_running)); new Thread(new Runnable() { @Override public void run() { - String outcome; try { - outcome = runImport(appContext, resolver, treeUri, dest); + deliverImportOutcome(appContext, runImport(appContext, resolver, treeUri, dest)); } finally { - importInProgress = false; + importInProgress.set(false); } - deliverImportOutcome(appContext, outcome); } }, "legacy-import").start(); } From 9cb53504f97c8b5a673f49caf273ea5c3b360c39 Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Sat, 18 Jul 2026 23:36:01 +0200 Subject: [PATCH 2/2] app: release the import guard if the worker thread never starts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-up. The CAS claimed the process-wide guard, but showNotification and Thread.start() ran after it and outside the worker's try/finally. A throw there — chiefly OutOfMemoryError from thread creation under pressure — left the static guard true for the rest of the process, so every later import showed "already running" with no worker alive. The old per-instance field self-healed on the next activity; the static one does not, so the launch is now wrapped to release the guard and rethrow. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Xavier Roche --- .../com/httrack/android/HTTrackActivity.java | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/httrack/android/HTTrackActivity.java b/app/src/main/java/com/httrack/android/HTTrackActivity.java index b0d1a17..95c41cd 100755 --- a/app/src/main/java/com/httrack/android/HTTrackActivity.java +++ b/app/src/main/java/com/httrack/android/HTTrackActivity.java @@ -2270,17 +2270,24 @@ private void importMirrorsFrom(final Uri treeUri) { showNotification(getString(R.string.import_mirrors_running)); return; } - showNotification(getString(R.string.import_mirrors_running)); - new Thread(new Runnable() { - @Override - public void run() { - try { - deliverImportOutcome(appContext, runImport(appContext, resolver, treeUri, dest)); - } finally { - importInProgress.set(false); + // Release the process-wide guard if the worker never starts (e.g. OOM at thread creation); + // otherwise a failed launch would wedge it true for the rest of the process. + try { + showNotification(getString(R.string.import_mirrors_running)); + new Thread(new Runnable() { + @Override + public void run() { + try { + deliverImportOutcome(appContext, runImport(appContext, resolver, treeUri, dest)); + } finally { + importInProgress.set(false); + } } - } - }, "legacy-import").start(); + }, "legacy-import").start(); + } catch (final Throwable t) { + importInProgress.set(false); + throw t; + } } /** The copy itself, on the worker thread; returns the message to show. **/