airo_background_downloads 1.2.1
airo_background_downloads: ^1.2.1 copied to clipboard
Cross-platform transfer engine with intent policies, native WorkManager & URLSession background execution, checksum processors, state machines, and telemetry.
airo_background_downloads #
Progressive background-downloader contracts for Flutter apps with WorkManager resume policies, background URLSession lifecycle handling, and checksum integrity verification.
Features #
- Progressive Background Download: Pause, resume, and track progress across app restarts.
- Integrity Verification: Built-in sha256/md5 integrity verification.
- Native Android & iOS Engines: WorkManager worker on Android and URLSession background task on iOS.
- Desktop HTTP engine (Linux, macOS, Windows):
HttpBackgroundDownloadswith partial-file safety and resume. Not for direct use on Android/iOS — usecreateBackgroundDownloads()instead.
Desktop download limits #
When using HttpBackgroundDownloads, there is no default byte cap on response bodies. Set DownloadArtifactRequest.expectedBytes and/or pass maxDownloadBytes to bound size; otherwise downloads are limited only by disk and server behavior.
macOS coverage #
CI runs on macOS arm64 (macos-latest). An optional Intel macOS (macos-13) job is non-blocking when runners are available. Intel macOS support is not claimed — behavior on Intel Macs is best-effort only until that job runs reliably. Darwin stat parsing uses lstat$INODE64 / fstat$INODE64 on 64-bit macOS (fail closed on Intel when those symbols are missing).
Partial file layout (desktop HTTP) #
Partial downloads and .meta sidecars live under a dedicated .airo_partials/ directory next to the destination file. That leaf is created as 0700 and verified; parent directories (including /tmp or Directory.systemTemp) are never chmodded.
Which API runs on Android #
On Android and iOS, use createBackgroundDownloads() (or MethodChannelBackgroundDownloads directly) with DownloadArtifactRequest. That is the path the native engines understand: artifactId, source (HTTPS only), destinationPath (inside the app sandbox), expectedBytes, expectedSha256, displayName.
AiroDownloadEngine / AiroPlatformBridge with AiroDownloadRequest does not work against the native engines in this version: it sends id/url/destination keys, which the Android and iOS plugins reject (invalid_request), and its AiroDownloadPolicy (for example NetworkPolicy.unmetered) is not read by native code. See Known gaps.
Google Play foreground service declaration #
The plugin's manifest adds three permissions to your app: INTERNET, FOREGROUND_SERVICE and FOREGROUND_SERVICE_DATA_SYNC. Google Play asks you to declare and justify the data sync foreground service.
Which component uses it #
- Downloads run in
ProgressiveDownloadWorker, a WorkManagerCoroutineWorker. The plugin enqueues one unique FIFO chain, so one download runs at a time. The only WorkManager constraint isNetworkType.CONNECTED. - The worker calls
setForeground(...)withServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC(Android 10+). WorkManager then runs it inside its own service,androidx.work.impl.foreground.SystemForegroundService. That is the component Play sees as the data sync foreground service. The plugin has no service of its own.
When it starts and stops #
- Starts: at the start of every worker run, right before the HTTP request, after WorkManager schedules the work (the app called
enqueue,resumeorretry, and the device has a network connection). It is refreshed about every 500 ms while bytes arrive, to update the notification. - Stops: when the worker returns: the file is downloaded, checked (size and SHA-256 when given) and moved into place; the download fails; the app calls
pauseorcancel(the transfer loop sees the change on its next read and returns); or Android/WorkManager stops the worker.
Your app's manifest (Android 14+) #
If your app targets Android 14 (API 34) or higher, Android requires a foreground service type on the service in the manifest. This plugin does not declare it for you. Add this inside <application> in your app's AndroidManifest.xml (the root <manifest> element needs xmlns:tools="http://schemas.android.com/tools"):
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
tools:node="merge" />
This is the declaration the WorkManager long-running workers guide asks for. If other workers in your app need more types, list them all (for example dataSync|location). Without it, setForeground fails, the worker throws before downloading, and the download does not run.
Ready-to-paste Play Console answers #
Replace <App> and the size with your own. Upload the video yourself; the text below only says what it must show.
Foreground service type: Data sync
Describe the task that uses this foreground service type:
<App>downloads an on-device AI model file (about 1 to 2.6 GB) after the user taps to download it. The download runs in a WorkManager worker that is promoted to a data sync foreground service (androidx.work.impl.foreground.SystemForegroundService) only while the file is being transferred and checked, and shows an ongoing "Downloading N%" notification. The service starts when the download starts and stops as soon as the file is downloaded and verified, or when the user pauses or cancels the download in the app. Nothing is uploaded; the only network request is the download of the model file.
User impact if the task is deferred or interrupted:
The model file is large and can take from several minutes to over an hour to download. Until it finishes, the app's on-device AI features cannot be used. If the system defers or stops the download when the user leaves the app, the user returns to an unfinished download and has to wait again. The download resumes from the bytes already saved when the server supports it, but it does not progress while deferred.
What the video should show:
- Open
<App>and tap the control that starts the model download (the download only starts from this user action).- Pull down the notification shade: the "Downloading N%" notification with a progress bar is visible.
- Press Home to leave the app and show the notification still updating.
- Return to the app, show the progress, then let the download finish (or pause or cancel it in the app) and show that the notification goes away.
Record with notifications allowed for the app. On Android 13+ with notifications denied, the service still runs but the notification only appears in the Task Manager in quick settings.
Android 14 and 15 notes #
- Android 14 (API 34): the manifest service type above is required. Users can swipe away the foreground service notification; the download keeps running.
- Android 15 (API 35):
dataSyncforeground services may run for 6 hours in total per 24 hours. Then Android callsService.onTimeout, and the timer resets only when the user brings the app to the foreground. After the limit, a newdataSyncforeground service cannot be started from the background (ForegroundServiceStartNotAllowedException). Apps targeting Android 15 also cannot start adataSyncforeground service from aBOOT_COMPLETEDreceiver; this plugin has no boot receiver. - What this package does at the 6 hour limit: it has no handling of its own (no
onTimeout,onStoppedorstopReasoncode, and no dedicated failure code). WorkManager'sSystemForegroundServicereceives the timeout and stops the worker. From the worker code, the expected result is: the coroutine is cancelled at its next progress update, the genericcatchrecordsstatus: failed,failureCode: transport("Download was interrupted."), withcanResume: trueand the.partfile kept. While an entry isfailed, laterenqueuecalls queue but do not start. To continue, callresume(artifactId)(orretry) when the app is in the foreground; the worker resumes withRange. This path has not been reproduced on a device. - Starting from the background: on Android 12+ Android can refuse to start a foreground service from the background. The first
setForegroundcall is outside the worker'stryblock, so if it throws, the worker fails while the stored status staysdownloading. Callpausethenresume(orcancelandenqueueagain) to recover. See Known gaps.
Notifications #
What the Android notification shows #
The Android engine builds the foreground service notification itself (ProgressiveDownloadWorker.createForegroundInfo):
- Channel
airo_platform_downloads, named "Airo downloads", importance low (no sound). - Title: the request's
displayName(first 80 characters), or theartifactIdwhen there is none. - Text: "Downloading N%" when the size is known (
expectedBytesorContent-Length), otherwise "Downloading", with a matching progress bar (indeterminate when the size is unknown). - Small icon
android.R.drawable.stat_sys_download, ongoing, alerts once. - No action buttons and no tap action. Pause, resume and cancel are only available through the Dart API.
The only thing an app controls is the title, through displayName.
DownloadNotificationConfig #
DownloadNotificationConfig (titlePattern, showProgressInNotification, enablePauseResumeActions, enableCancelAction) and AiroDownloadNotificationController are Dart-only helpers. They are not sent to the Android or iOS engine and do not change the notification above. AiroDownloadNotificationController keeps a map of active downloads and formats titlePattern (replacing {name} with the artifact id); it posts nothing, and the package never calls onNotificationAction. Use them if you draw your own notification with another plugin.
Android 13+ and POST_NOTIFICATIONS #
- The plugin does not declare or request
POST_NOTIFICATIONS. - Android does not require it to start a foreground service. The download still runs without it.
- Without it (app targeting Android 13+, user has not granted it), the notification does not appear in the notification drawer; the running service is only listed in the Task Manager in quick settings.
- If you want users to see the progress notification, your app must declare
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>and request it at runtime (for example withpermission_handler) before starting a download.
Privacy and telemetry #
- No analytics dependency. The package's runtime dependencies are
flutter,cryptoandffi(seepubspec.yaml). The Android engine addsandroidx.work:work-runtime-ktxandcom.squareup.okhttp3:okhttp(seeandroid/build.gradle). The iOS pod depends only on Flutter. - The only network request is the download itself. Android makes one
GETto the request'ssourceper attempt (OkHttp, inProgressiveDownloadWorker), iOS uses a backgroundURLSessiondownload task forsource, and the desktop engine usesHttpClient.getUrl(source). There is no other HTTP client code in the package. - Telemetry stays in memory on the device.
AiroDownloadTelemetryLoggerkeeps counters in memory only: no files, no network, reset when the process ends orreset()is called.AiroDownloadTelemetrySnapshot.toMap()returns a plainMapfor local diagnostics. The package never sends it anywhere. Sending it off the device is the app's choice. test/telemetry_stays_local_test.dartchecks the dependency list, the imports underlib/, and that the telemetry and notification helpers import nothing but the package's own models.
Removing the foreground service permission #
An app could try to drop the permission with:
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"
tools:node="remove" />
Do not do this with this version if you use the Android engine. The worker always calls setForeground with the data sync type before it downloads, and there is no option to skip it. On apps targeting Android 14+ that call is refused without FOREGROUND_SERVICE_DATA_SYNC, so every Android download fails before the first byte (and the stored status stays downloading). Removing FOREGROUND_SERVICE breaks it on every supported Android version. There is also no "in-app only" Android path: HttpBackgroundDownloads throws on Android and iOS. An opt-out is listed under Known gaps.
Large files and resume #
Android #
- Size: byte counts are 64-bit (
Long) throughout the worker, so files over 2 GB (for example 2.6 GB) are handled. IfexpectedBytesis set, the worker checks free space for the remaining bytes first and fails withinsufficient_storagewhen it does not fit. - Resume: partial data is kept in
<destinationPath>.part. On the next attempt, if the.partfile has bytes, the worker sendsRange: bytes=<size>-. A206response is appended; any other success code (for example200) fails withresume_not_supported, andretrythen deletes the.partfile and starts again from byte 0. - Integrity: the Android engine does not check
Content-Rangeand does not sendIf-Rangeor an ETag, so a file that changed on the server between attempts is only caught by the final checks. Always setexpectedSha256(andexpectedBytes) for large models. On a mismatch the.partfile is deleted and the download fails withintegrity_mismatch. - Redirects (Hugging Face
resolve/mainURLs): OkHttp follows redirects (followRedirects(true)), including HTTPS to HTTPS hops such as Hugging Face to its CDN; it does not follow redirects that switch between HTTP and HTTPS (followSslRedirects(false)). OkHttp builds the follow-up request from the original one, so theRangeheader goes to the CDN too. This is not covered by an automated test here (no Android SDK in CI) and has not been checked live against Hugging Face from this repo. - Headers: the Android engine ignores
DownloadArtifactRequest.headers, so anAuthorizationtoken for a gated Hugging Face repo is not sent. Use public files on Android.
Desktop (HttpBackgroundDownloads) #
- Resume with
Rangeis only attempted when the request hasexpectedSha256or a strong validator (ETag) was stored; otherwise the partial file is discarded. - A fresh download follows up to 8 redirects. A resume request does not follow redirects. Against a Hugging Face
resolve/mainURL (which answers with a redirect), the resume attempt gets the redirect, the partial file is deleted, and the download restarts from byte 0 and follows the redirect. The file still completes and is verified, but the bytes already on disk are downloaded again.test/redirect_resume_test.dartpins both behaviours.
Known gaps #
- No Android 15
dataSynctimeout handling. NoonTimeout/stopReasonhandling and no dedicated failure code; a timeout looks liketransport"Download was interrupted.", and the app must callresumewhen it is in the foreground again. - First
setForegroundcall is outside the worker'stryblock. If Android refuses the foreground service (missing manifest type, missing permission, background start limits), the worker fails and the stored status staysdownloading. - The plugin manifest does not declare
foregroundServiceType="dataSync"onSystemForegroundService. Apps targeting Android 14+ must add it themselves (see above). - No opt-out of the foreground service for apps that only download while in the app.
DownloadNotificationConfigis not applied to the native notification: no pause, resume or cancel actions, no tap action, no option to hide progress.AiroDownloadEngine/AiroPlatformBridgedo not match the native method channel (wrong argument keys and event shape), andAiroDownloadPolicy(unmetered, charging, retries) is not applied on Android, which always usesNetworkType.CONNECTED. UsecreateBackgroundDownloads()withDownloadArtifactRequest; "Wi-Fi only" must be enforced by the app before callingenqueue.- Android ignores
DownloadArtifactRequest.headers(no auth tokens for gated downloads). - Android resume does not validate
Content-Rangeor useIf-Range; it relies on the final size and SHA-256 checks. - Desktop resume restarts from byte 0 when the source URL redirects (for example Hugging Face
resolve/main).
Installation #
dependencies:
airo_background_downloads: ^1.2.1
License #
MIT License - see LICENSE for details.