iOS: native upload and download services (background URLSession)

Swift implementation of the upload_service/download_service channels:
background PUT/GET against the account's WebDAV root, one summary local
notification per batch, Downloads visible in the Files app. Pure helpers
covered by RunnerTests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Ayushya Amitabh
2026-10-06 19:12:15 -04:00
co-authored by Claude Sonnet 5.5
parent d008185f98
commit 469e57cca7
8 changed files with 579 additions and 9 deletions
+37 -6
View File
@@ -816,10 +816,11 @@ files, 15 for folders), keeping the existing reshare bit, plus Remove access.
The five `dev.ayushya.noo/*` channels (`share_intent`, `pick_intent`,
`upload_service`, `download_service`, `sync_service`, plus their status
`EventChannel`s) are implemented in Kotlin only; iOS has no handler for any
of them yet, so every call there throws `MissingPluginException`.
`lib/services/native_channel.dart` makes that safe until each one is built
in Swift:
`EventChannel`s) were written for Android in Kotlin. iOS implements
`upload_service` (+ its status stream) and `download_service` so far
(`ios/Runner/Native/`, see below); for the other three there's no handler, so
calls throw `MissingPluginException`. `lib/services/native_channel.dart`
makes that safe until each one is built in Swift:
- `invokeIfAvailable` - a missing implementation returns null. For cold-start
checks (`getInitialShare`, `getPickRequest`, `getSyncStatus`) and
@@ -839,6 +840,36 @@ side - the helpers only act on `MissingPluginException`.
lives: `getExternalStorageDirectory()` on Android (what `SyncEngine.kt`
writes), the app support directory elsewhere, because that call throws on
iOS. `OfflineController` and `localSyncedFilePath` both go through it.
### iOS upload/download (`ios/Runner/Native/`)
- `NativeServices.swift` registers the channels from
`AppDelegate.didInitializeImplicitFlutterEngine` (via
`engineBridge.applicationRegistrar.messenger()`) and forwards
`startUpload`/`startDownload` to `TransferManager`; a batch's completion
goes back over `upload_service/status` so `FilesController` refreshes the
folder like on Android.
- `TransferManager.swift` runs everything on one *background* `URLSession`
(`dev.ayushya.noo.transfers`, `sessionSendsLaunchEvents`), so transfers
finish after the app is suspended/closed; `AppDelegate` hands the system's
`handleEventsForBackgroundURLSession` completion handler to it and calls
`reconnect()` at launch. Each task carries its info in `taskDescription`
and its batch's running totals live in `UserDefaults`, so the one summary
local notification ("Uploaded 3 files") still fires after a relaunch.
- Uploads: one `PUT` per file to `<server>/remote.php/dav/files/<user>/...`
from a staged copy in Caches (no chunking, so very large files are bound by
the server's single-request limit; an existing file with the same name is
overwritten). Sends `X-OC-Mtime`. Downloads: `GET` into
`Documents/Downloads`, with `UIFileSharingEnabled` +
`LSSupportsOpeningDocumentsInPlace` set so it shows in the Files app under
"On My iPhone > Noo"; a name clash becomes `a (1).txt`.
- No progress notification (iOS can't update one from a background session
the way Android's foreground service does) and no cancel yet - only the
final summary. The Dart snackbar text still says "see the notification for
progress".
- `WebDAV.swift` (URL building) and `LocalFiles.uniqueURL` are pure and
covered by `ios/RunnerTests` (`xcodebuild test -workspace
ios/Runner.xcworkspace -scheme Runner -destination 'platform=iOS
Simulator,id=<udid>'`).
Not built on iOS yet: Share Extension (needs an App Group), the picker
(File Provider), background uploads/downloads (`URLSession`), and sync
(`BGTaskScheduler`) - see the iOS handoff notes.
(File Provider), and sync (`BGTaskScheduler`) - see the iOS handoff notes.