Skip to content

Offline downloads, from first principles

An eight-part guide to how this app downloads a film or an episode for offline viewing, and why it is built the way it is. Written for someone who has never worked on a background downloader: each doc builds on the last, and each ties the concept back to the real files so you can read theory and code side by side.

Read them in order the first time through:

  1. What a Download Is: why “download a film” means copying thousands of small files, what ends up on disk, and the components that share the work.
  2. Starting a Download: the path from a tap on the Download button to thousands of queued native tasks. Covers gating, the manifest plan, the quality choice, and how each task is built.
  3. Progress and Completion: how a finished segment becomes a moving progress bar, and how the app proves a download is really complete before it writes the playlists that make it playable.
  4. When Things Go Wrong: the layers of recovery (package retries, settlement, the stall watchdog, cold-start reconciliation) and the user actions pause, resume, retry and cancel.
  5. Android: Keeping Downloads Alive: WorkManager, the holding queue, the app’s own foreground service, Android’s background job quotas and 6-hour dataSync limit, and the Back button.
  6. iOS: The Background URLSession: how iOS keeps transferring while the app is suspended, the resource timeout, and what changes when Dart is not running.
  7. State, Storage and Startup: every place download state is stored (Hive, disk, the package database, native preferences), the status state machine, and the startup sequence.
  8. Operating and Extending: logs, adb recipes, tests, tuning knobs, known limitations, and how to change the system safely.

A Chorki title is an HLS stream: a playlist that points at a few thousand four-second .ts segments, plus one 16-byte AES key. Downloading it means copying every one of those files to disk and rewriting the playlists to point at the copies. The app hands each file to the background_downloader package as its own task. On Android that becomes a WorkManager job, fed about eight at a time; on iOS it becomes a background URLSession download. The app’s DownloadManager watches the tasks finish and turns that into progress. When something is stuck it re-queues exactly the files missing on disk. Only once the disk proves every file is present does it write the local playlists and mark the title completed. Almost all of the complexity exists because phones do not want apps running in the background, and a two-hour film on a shaky mobile connection takes longer than either operating system will normally let an app keep working.

The working tree on main as of 16 September 2026, including the download throughput and reliability fixes made that day:

  • the Android holding queue of 8
  • the dataSync time-limit handling
  • Back-to-minimize while downloading
  • the iOS 7-day resource timeout
  • a settlement budget that refills on progress
  • disk-verified completion
  • Wi-Fi-only auto-resume
  • honoring the download quality setting

and the follow-up fixes made from these docs:

  • unique audio task IDs, with re-queues matched by destination file
  • a “Pause download” action in the download sheet
  • a free-space check before a download starts
  • the 50-download limit
  • downloads scoped to the signed-in account and the profile’s maturity level

The package version is background_downloader 9.5.5. Package behaviour described here was checked against its source in ~/.pub-cache/hosted/pub.dev/background_downloader-9.5.5/.

Code is referenced by file and symbol name (for example DownloadManagerImpl._settleImpl) rather than by line number, because line numbers in these files move often.

Doc Primary files
1. What a Download Is lib/data/services/hls/ (all), lib/data/services/download_manager_impl.dart, lib/domain/services/download_manager.dart
2. Starting a Download lib/presentation/widgets/content_page_widget/content_page_button_widget.dart, lib/presentation/pages/content_pages/single_content_screen.dart, lib/presentation/bloc/download_screen/download_screen_bloc.dart, lib/data/helpers/content_helper.dart, lib/data/services/hls/download_orchestrator.dart, hls_manifest_parser.dart, download_task_factory.dart
3. Progress and Completion lib/data/services/hls/asset_progress_tracker.dart, local_playlist_writer.dart, lib/presentation/widgets/download/download_row_watcher.dart, lib/data/services/content_entity_serde.dart
4. When Things Go Wrong lib/data/services/download_manager_impl.dart (_settleImpl, _runStallWatchdog, _coldStartReconcile, _safeFinalize), lib/presentation/bloc/connectivity/connectivity_cubit.dart
5. Android android/app/src/main/kotlin/com/prothomalo/DownloadForegroundService.kt, MainActivity.kt, android/app/src/main/AndroidManifest.xml, lib/data/services/download_progress_notifier.dart, lib/presentation/widgets/download/download_protection_tip.dart, lib/presentation/pages/home_pages/main_scaffold.dart
6. iOS lib/main.dart (_initBackgroundDownloader), DownloadManagerImpl._configureGroupNotification, the package’s ios/…/BDPlugin.swift and UrlSessionDelegate.swift
7. State, Storage and Startup lib/domain/entities/cache_content_entity.dart, cache_subtitle_entity.dart, download_record.dart, download_status.dart, lib/main.dart, lib/injection_container.dart, lib/data/services/app_bootstrap_impl.dart
8. Operating and Extending test/data/services/hls/, test/presentation/bloc/download_screen/download_screen_bloc_test.dart, plus everything above
  • ../playback/03-offline-downloads.md: the playback side of the same feature. It covers what the rewritten playlists look like, why a tiny web server on 127.0.0.1:8080 plays the download back, and why the key file needs its own MIME type. This series stops where that doc starts.
  • ../playback/06-the-new-kms-authorization.md: section 3 explains why short-lived key tokens would break the key download task if that backend change ever ships.