Skip to content

Harden Android sync and download flows#10

Merged
Promises merged 1 commit into
Promises:mainfrom
kvmgithub:fix/android-sync-download-flows
May 10, 2026
Merged

Harden Android sync and download flows#10
Promises merged 1 commit into
Promises:mainfrom
kvmgithub:fix/android-sync-download-flows

Conversation

@kvmgithub
Copy link
Copy Markdown

Context

This PR hardens the Android sync and download flows around a concrete user-facing cleanup problem: when a downloaded audiobook is marked as not downloaded and the user chooses to delete the file, LibriSync should also clean up the files and folders that LibriSync created for that specific book. That includes the Smart Audiobook Player sidecar cover (EmbeddedCover.jpg), an empty per-book folder, and an empty author folder. The cleanup must stay conservative: it must not delete other books, other cover files that still belong to remaining audio files, or an author folder that still contains other content.

While validating that path, this also fixes a related consistency issue in the Android app: different parts of the Android stack were using different database locations. Foreground React Native screens, foreground services, background workers, notification actions, and Rust JNI calls now use the same app database path so account state, library metadata, download tasks, and final file paths stay in one SQLite store.

What changed

Shared Android database path

The app now has a single database path convention for Android:

  • JS uses src/utils/appPaths.ts to derive audible.db from the Expo app document/files directory.
  • Kotlin uses AppPaths.databasePath(context) for Android services, receivers, workers, and bridge code.
  • Library, login, settings, account, and debug screens no longer manually assemble cache-based database paths.
  • DownloadService, DownloadActionReceiver, BackgroundTaskManager, DownloadWorker, LibrarySyncWorker, and TokenRefreshWorker now use the same path helper instead of private cache-dir variants.

This matters because the previous split could leave background workers looking at one database while the UI looked at another. That made downloads, account lookups, token refresh, sync state, and file-path lookup unreliable after app restarts or when background services were involved.

Download deletion and cleanup

clearBookDownloadState(..., deleteFile = true) now performs Android-aware deletion before clearing the Rust download state.

For Android SAF/content URI downloads, the bridge now:

  • reads the stored final file path for the selected ASIN before clearing state;
  • deletes the selected downloaded file through DocumentsContract or the persisted writable document tree;
  • finds the parent book folder from the document id;
  • lists sibling documents after the audiobook file has been removed;
  • deletes EmbeddedCover.jpg only when no other audio file remains in that same book folder;
  • deletes the book folder only when it is empty;
  • deletes the parent author folder only when the book folder was deleted and the author folder is now empty;
  • never deletes the selected tree root;
  • returns structured cleanup metadata to JS.

The returned result now includes cleanup details such as:

  • file_deleted
  • deleted_path
  • cover_deleted
  • book_folder_deleted
  • author_folder_deleted
  • cleanup_error
  • delete_error

The UI uses those fields to distinguish full success from partial cleanup. If the audiobook file was deleted but follow-up folder cleanup failed, the app reports that as partial cleanup instead of pretending everything was removed.

Conservative deletion rules

The cleanup is intentionally defensive:

  • Other audio files in the same book folder prevent EmbeddedCover.jpg from being deleted.
  • Non-empty book folders are kept.
  • Non-empty author folders are kept.
  • The root SAF directory is never deleted.
  • Cleanup errors are captured and surfaced instead of causing broad fallback deletion.
  • Plain file-path deletion remains scoped to the selected file path.

This is the key safety behavior for users with multiple books by the same author or manually managed files in the download directory.

Smart Audiobook Player cover handling

The cover-art path is now handled in both directions:

  • Download/conversion can still create EmbeddedCover.jpg next to the final audiobook file when the Smart Audiobook Player compatibility setting is enabled.
  • Existing EmbeddedCover.jpg files are replaced cleanly when writing a new cover.
  • Deleting a SAF-backed download removes that sidecar cover only when it is safe to do so for the selected book folder.

This keeps Smart Audiobook Player compatibility while avoiding stale cover art after a book is removed.

Download task and final output path consistency

The Android conversion flow now writes the final output path back into the persistent Rust download task after the file is copied to the user-selected destination.

Changes include:

  • DownloadOrchestrator.copyToFinalDestination(...) now returns the actual final path.
  • completed tasks update Rust with the final SAF/file path;
  • JNI nativeUpdateDownloadTaskStatus accepts an optional output_path;
  • PersistentDownloadManager adds update_task_status_with_details(...) to update status, error, and output path in one place.

This is important because deletion depends on the final stored path. Without this, the database could keep a stale cache path while the real audiobook lived in the SAF destination.

Account and sync flow hardening

This PR also tightens account handling for Android background flows:

  • account lookup and token refresh use the shared database path;
  • foreground download start reads the account from SQLite instead of a separate SecureStore-only path;
  • refreshed account tokens are written back through the Rust account storage path;
  • account reconstruction preserves locale details from stored identity data when available;
  • JNI now exposes account deletion so the Android bridge can keep account state consistent.

This makes background sync, manual sync, token refresh, and download enqueueing operate against the same persisted account data.

Background service and manifest setup

The Android manifest/config plugin setup is expanded so generated native projects keep the required service declarations:

  • BackgroundTaskService is declared alongside DownloadService.
  • BootReceiver is declared for boot/package-replaced recovery hooks.
  • RECEIVE_BOOT_COMPLETED is added in app config and the Expo config plugin.
  • The config plugin now updates existing services/receivers idempotently instead of blindly appending duplicates.

This keeps expo prebuild output aligned with the checked-in Android module manifest.

Rust/API safety and test support

The Rust side includes supporting hardening:

  • registration responses are parsed without writing token/customer data to app cache or public download paths;
  • failed registration response logging is shortened to avoid dumping large/sensitive response bodies;
  • interactive OAuth test output avoids printing token material;
  • account storage tests cover preserving richer locale data;
  • the public-download integration test is marked ignored because it requires network access;
  • Cargo.lock is checked in for reproducible Rust dependency resolution.

User-visible behavior

After this change, when a user chooses Delete File from the library screen for a downloaded book:

  1. The selected audiobook file is deleted.
  2. The book is marked as not downloaded in the app database.
  3. The associated EmbeddedCover.jpg is deleted if it belongs only to that now-deleted book folder.
  4. The per-book folder is deleted if it became empty.
  5. The author folder is deleted only if it also became empty.
  6. Other books, other audio files, unrelated covers, and non-empty author folders are left alone.

The app also handles partial cleanup more clearly. For example, if the audiobook file is deleted but Android refuses to remove a sidecar file or folder, the UI can report the cleanup issue without losing the database state reset.

Review focus

The main review areas are:

  • SAF deletion logic in ExpoRustBridgeModule.kt, especially tree-permission lookup and parent-folder cleanup.
  • Shared database path adoption across JS screens, Android services, Android workers, and notification receivers.
  • Download task status updates that persist the final output path after conversion.
  • The conservative cleanup checks that prevent deletion of unrelated content.
  • Manifest/config-plugin changes for background services and boot receiver registration.

Validation

Automated and build validation run for this branch:

  • npm run typecheck
  • cargo test
  • git diff --check
  • npm run build:rust:android
  • ./gradlew :expo-rust-bridge:compileDebugKotlin
  • ./gradlew :app:assembleDebug

Manual Android validation was also performed for the download deletion flow, including SAF-backed output, Smart Audiobook Player cover cleanup, empty book-folder cleanup, and preserving author folders when other books remain.

Use shared app database paths across JS, Kotlin, and workers so sync, account, and download state stay in one SQLite store.

Move SAF deletion into the Android bridge so downloaded books, optional Smart Audiobook Player covers, and empty per-book folders are cleaned up without deleting unrelated files.
@Promises
Copy link
Copy Markdown
Owner

Promises commented May 9, 2026

Thank you, will do a check and test

@Promises Promises merged commit b8004f5 into Promises:main May 10, 2026
@kvmgithub kvmgithub deleted the fix/android-sync-download-flows branch May 14, 2026 16:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants