Keep the current language pack until the new one is downloaded

The package downloader deleted the installed language pack as soon as a
version difference with the server was detected, before the download had
even started. If the download then failed (connection drop, server
error), the device was left with an empty language directory: on the
next offline launch, the directory check failed and the app was unusable
until internet access was restored.

The early cleanup was also redundant: _copy_data() already removes the
existing directory right before extracting the new archive, i.e. after
the download has succeeded (HTTP 200).

Changes in package_downloader.gd:
- Remove the pre-download cleanup of the current language directory; the
  previous pack now stays usable for offline play until the new one has
  been fully downloaded.
- Check the return value of HTTPRequest.request() and show the download
  error popup when the request cannot be started, instead of leaving the
  screen waiting forever on a request that was never sent.
- Surface extraction failures (missing archive, unzip error) through the
  error popup instead of silently returning from the extraction thread
  and leaving the user stuck on a frozen progress screen.

Changes in login.gd:
- Handle the result of UserDataManager.login_student(), which was
  previously logged and ignored. The synchronization performed right
  before the login can delete or move the student locally (e.g. the
  server requested a local deletion), in which case the child was sent
  to the gardens scene with no student session loaded, ending up on a
  dead screen. On failure, play the wrong-password feedback and reset
  the code keyboard instead of changing scene.

Verified with the GUT test suite (58/58 passing).
This commit is contained in:
Adrien Ufferte
2026-06-11 08:09:56 +02:00
parent c77e9b90ec
commit 921d13b0cb
2 changed files with 17 additions and 8 deletions
@@ -97,16 +97,16 @@ func _ready() -> void:
# Create the language_resources folder
if not DirAccess.dir_exists_absolute(USER_LANGUAGE_RESOURCES_PATH):
DirAccess.make_dir_recursive_absolute(USER_LANGUAGE_RESOURCES_PATH)
# Delete the files from old language pack
if DirAccess.dir_exists_absolute(current_language_path):
Log.trace("PackageDownloader: Cleaning existing language directory at %s" % current_language_path)
Utils.clean_dir(current_language_path)
# Download the pack
# Download the pack. The previous pack is kept on disk so the app can
# still run offline if the download fails; it is only removed during
# extraction, once the new pack has been fully downloaded.
http_request.set_download_file(USER_LANGUAGE_RESOURCES_PATH.path_join(language + ".zip"))
Log.trace("PackageDownloader: Downloading pack from %s" % res.body.url)
http_request.request(res.body.url as String)
var request_error: Error = http_request.request(res.body.url as String)
if request_error != OK:
Log.error("PackageDownloader: Cannot start language pack download: %s" % error_string(request_error))
_show_error(2) # Error downloading
else:
download_bar.value = 1
extract_bar.value = 1
@@ -152,6 +152,7 @@ func _copy_data(this: PackageDownloader) -> void:
# Check if a zip exists for the complete locale
if not FileAccess.file_exists(USER_LANGUAGE_RESOURCES_PATH.path_join(language + ".zip")):
Log.warn("PackageDownloader: No downloaded archive found for %s" % language)
this.call_thread_safe("_show_error", 2) # Error downloading
return
Log.trace("PackageDownloader: Extracting downloaded package")
@@ -184,6 +185,7 @@ func _copy_data(this: PackageDownloader) -> void:
var subfolder: String = unzipper.extract(language_zip_path, USER_LANGUAGE_RESOURCES_PATH, false)
if subfolder == "":
Log.error("PackageDownloader: Extraction failed for %s" % language_zip_path)
this.call_thread_safe("_show_error", 2) # Error downloading
return
# Move the data to the locale folder of the user
+7
View File
@@ -63,6 +63,13 @@ func _on_code_keyboard_password_entered(password: String) -> void:
var login_success: bool = UserDataManager.login_student(password)
Log.info("LoginScreen: Login attempt for student code %s returned %s" % [password, str(login_success)])
kalulu_button.hide()
if not login_success:
# The synchronization above may have deleted or moved the student
Log.warn("LoginScreen: Login failed for student code %s after synchronization" % password)
await kalulu.play_kalulu_speech(wrong_password_speech)
keyboard.reset_password()
kalulu_button.show()
return
await kalulu.play_kalulu_speech(right_password_speech)
await OpeningCurtain.close()
Log.trace("LoginScreen: Start loading next scene")