From e55ce9fd1d2c1b74a88da4b180de5c9f550e2dc2 Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Fri, 24 Jul 2026 16:14:24 +0200 Subject: [PATCH 1/2] Fix Web address lost after Back on the project-name pane Selecting an existing project, then going Back and forward again, left the Web address and options empty while keeping the project name. validatePane() decides whether to load the project's saved profile by comparing the field against the map's fieldProjectName key, treating "the map holds this name" as "the profile is loaded". But savePaneFields() writes that key on any navigation away, including a plain Back, which never unserializes. The guard then saw no change and skipped the load, so the URL was never restored. Track the loaded project in a dedicated loadedProjectName sentinel and reload whenever the selection differs from it. The decision is extracted into a pure shouldReloadProfile() helper covered by ProjectReloadTest. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Xavier Roche --- .../com/httrack/android/HTTrackActivity.java | 28 +++++++++++--- .../httrack/android/ProjectReloadTest.java | 37 +++++++++++++++++++ 2 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 app/src/test/java/com/httrack/android/ProjectReloadTest.java diff --git a/app/src/main/java/com/httrack/android/HTTrackActivity.java b/app/src/main/java/com/httrack/android/HTTrackActivity.java index bad5035..c171b1b 100755 --- a/app/src/main/java/com/httrack/android/HTTrackActivity.java +++ b/app/src/main/java/com/httrack/android/HTTrackActivity.java @@ -198,6 +198,10 @@ protected static class VERSION_CODES { // "Project name" pane is dirty protected boolean dirtyNamePane; + // Project whose saved profile is loaded in the map (reload-guard sentinel); null if none. + // Distinct from the map name key, which plain Back navigation writes before any load. + protected String loadedProjectName; + // Handler to execute code in UI thread private final Handler handlerUI = new Handler(); @@ -699,6 +703,7 @@ protected void onCreate(final Bundle savedInstanceState) { // Clear map (useful to get dynamic fields) mapper.setContext(this); mapper.resetMap(); + loadedProjectName = null; // Go to first pane now setPane(0); @@ -2275,9 +2280,19 @@ public void run() { }); } + /** + * Whether the setup pane must (re)load the selected project's saved profile: the + * selection changed from the profile in the map, or a restore forced it. Kept out of + * the map name key, which plain Back/Next writes before any profile is loaded. + */ + static boolean shouldReloadProfile(final String selectedName, + final String loadedName, final boolean dirty) { + return dirty || loadedName == null || !loadedName.equals(selectedName); + } + /** * Validate the current pane - * + * * @return true if the current pane is valid */ protected boolean validatePane() { @@ -2298,11 +2313,11 @@ protected boolean validatePane() { // Check project name final String name = getFieldText(R.id.fieldProjectName); if (OptionsMapper.isStringNonEmpty(name)) { - // Changed name ? - final String prevName = getMap(R.id.fieldProjectName); - if (prevName == null || !prevName.equals(name) || dirtyNamePane) { - // We need to put immediately the name in the map to be able to - // unserialize. + // Load the selected project's profile when the selection differs from what is + // loaded. Compares loadedProjectName, not the map name key: Back navigation + // writes that key (savePaneFields) before any profile is read. + if (shouldReloadProfile(name, loadedProjectName, dirtyNamePane)) { + // Put the name in the map first so unserialize() can resolve the profile. try { mapper.resetMap(); setMap(R.id.fieldProjectName, name); @@ -2311,6 +2326,7 @@ protected boolean validatePane() { // Ignore (if not found) } finally { dirtyNamePane = false; + loadedProjectName = name; } } // A crafted winprofile.ini can overwrite the name with a path that escapes the root. diff --git a/app/src/test/java/com/httrack/android/ProjectReloadTest.java b/app/src/test/java/com/httrack/android/ProjectReloadTest.java new file mode 100644 index 0000000..44d9888 --- /dev/null +++ b/app/src/test/java/com/httrack/android/ProjectReloadTest.java @@ -0,0 +1,37 @@ +package com.httrack.android; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +/** + * Guards HTTrackActivity.shouldReloadProfile, the decision that loads a selected project's + * saved URL/options. Regression: Select project -> Back -> Next -> Next left the URL blank + * because the guard trusted the map name key, which Back had already written. + */ +public class ProjectReloadTest { + @Test + public void reloadsWhenNothingLoadedYet() { + // Fresh selection, no profile loaded: must load, even though the map name key already + // holds the name (written by Back navigation). This is the reported bug. + assertTrue(HTTrackActivity.shouldReloadProfile("Foo", null, false)); + } + + @Test + public void reloadsWhenSelectionChanged() { + assertTrue(HTTrackActivity.shouldReloadProfile("Bar", "Foo", false)); + } + + @Test + public void reloadsWhenDirtyAfterRestore() { + assertTrue(HTTrackActivity.shouldReloadProfile("Foo", "Foo", true)); + } + + @Test + public void skipsWhenSameProfileAlreadyLoaded() { + // The one case that must NOT reload: reloading here would wipe URL/options the user + // typed on the setup pane then navigated back past. + assertFalse(HTTrackActivity.shouldReloadProfile("Foo", "Foo", false)); + } +} From 59294e3ff10bdb93023e992f8e5dd494f36298d5 Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Fri, 24 Jul 2026 16:20:42 +0200 Subject: [PATCH 2/2] Persist the reload sentinel across recreation; trim repeated comments Review caught a data-loss regression from the previous commit: loadedProjectName was not saved in the instance state, so after a rotation (or process death) on the setup pane it came back null while the map was restored with the user's live edits. The next Back/Next then saw a "changed" project and reset the map, wiping those edits. Persist the sentinel next to the map it tracks. Also fold the reload-guard rationale down to a single copy at the field declaration; the javadoc and call site now just point at it. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Xavier Roche --- .../com/httrack/android/HTTrackActivity.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/httrack/android/HTTrackActivity.java b/app/src/main/java/com/httrack/android/HTTrackActivity.java index c171b1b..19686ad 100755 --- a/app/src/main/java/com/httrack/android/HTTrackActivity.java +++ b/app/src/main/java/com/httrack/android/HTTrackActivity.java @@ -2280,11 +2280,7 @@ public void run() { }); } - /** - * Whether the setup pane must (re)load the selected project's saved profile: the - * selection changed from the profile in the map, or a restore forced it. Kept out of - * the map name key, which plain Back/Next writes before any profile is loaded. - */ + /** Whether the setup pane should (re)load the selected project's saved profile; see loadedProjectName. */ static boolean shouldReloadProfile(final String selectedName, final String loadedName, final boolean dirty) { return dirty || loadedName == null || !loadedName.equals(selectedName); @@ -2313,9 +2309,7 @@ protected boolean validatePane() { // Check project name final String name = getFieldText(R.id.fieldProjectName); if (OptionsMapper.isStringNonEmpty(name)) { - // Load the selected project's profile when the selection differs from what is - // loaded. Compares loadedProjectName, not the map name key: Back navigation - // writes that key (savePaneFields) before any profile is read. + // Reload when the selection differs from the loaded profile (see shouldReloadProfile). if (shouldReloadProfile(name, loadedProjectName, dirtyNamePane)) { // Put the name in the map first so unserialize() can resolve the profile. try { @@ -2966,6 +2960,9 @@ protected void saveInstanceState(final Bundle outState) { // Map keys outState.putParcelable("com.httrack.android.map", mapper.serialize()); + // Which project's profile the map holds, so the reload guard survives recreation. + outState.putString("com.httrack.android.loadedProjectName", loadedProjectName); + // Current pane outState.putInt("com.httrack.android.pane_id", pane_id); @@ -3106,6 +3103,11 @@ protected void restoreInstanceState(final Bundle savedInstanceState) { // Load map settings loadParcelable(data); + // Restore the reload-guard sentinel alongside the map it tracks; else a post-restore + // Back/Next would treat the live map as stale and wipe the user's edits. + loadedProjectName = savedInstanceState + .getString("com.httrack.android.loadedProjectName"); + // The progress pane means a live crawl; after process death none exists, so land on the // setup pane instead, where isInterruptedProfile() defaults the action to Continue. final int paneId = (id == LAYOUT_MIRROR_PROGRESS && !hasLiveRunner())