Move the extension post-processing hook stage out of
NativeDownloadFinalizer into a sibling file as an extension function.
The body is verbatim. The shared materializeForFFmpeg and
replaceStatePath helpers stay on the object because every stage uses
them, and no object member needed wider visibility.
Move the metadata embedding stage, ReplayGain tagging and verification,
the album key and duration helpers, and the final audio quality probe
out of NativeDownloadFinalizer into a sibling file of extension
functions. Bodies are verbatim. The moved code reads three JSON helpers
that stay on the object, so parseObject, optPositiveInt and
optPositiveBitrateKbps widen from private to internal.
Move the lossless container conversion and automatic conversion stages,
with their file-local helpers (output extension, M4A sniffing, unique
output path, preserved container rename), out of NativeDownloadFinalizer
into a sibling file of extension functions. Bodies are verbatim except
one moved comment line whose em dash is rewritten as a colon. No object
member needed wider visibility.
NativeDownloadFinalizer.kt is a 1300-line god file. Move the decryption
stage (finalizeDecryption, ISO-BMFF extension normalization and key
candidate expansion) into a sibling file as NativeDownloadFinalizer
extension functions, matching NativeFinalizerMedia/FFmpeg/SafPublish.
Bodies are verbatim; file-local helpers stay private, and no object
member needed wider visibility.
MainActivitySafIo and SafDownloadHandler each defined an identical
errorJson. Keep one internal top-level function in MainActivitySafIo
and drop the private copy. errorJson, isMediaStoreUri, extFromFileName
and extFromMimeType never used their MainActivity receiver, so they
become plain top-level functions; call sites are unchanged.
Rust is the only backend. Rename the iOS invokeGoMethod, which
dispatches the OS-owned channel methods, to invokeOsMethod, and reword
the comments that still described Go calls, the Go progress stream, the
Go metadata reader or a "native migration boundary" so they describe
the current backend. Channel method names and reply keys are unchanged.
Flutter 3.47.7 sets flutter.minSdkVersion to 24, which
android/app/build.gradle.kts uses as minSdk. The N (24) checks around
StorageManager.storageVolumes and the LOLLIPOP (21) check around
finishAndRemoveTask were therefore always true, and their else branches
could never run. Keep the true-branch bodies. Checks for API levels
above 24 are unchanged.
RustCoreBackend.invokeApplication handled readFileMetadata,
editFileMetadata and cancelNativeDataJob, but MainActivity intercepts
all three first: the metadata methods are in nativeBackendMethods and
dispatchBackendApplication answers cancelNativeDataJob before it calls
invokeApplication. No Kotlin code calls invokeApplication with those
names.
Dart never sends these channel methods, so their handlers go on every
platform:
- shareMultipleContentUris (Android) and getAvailableLyricsProviders
(Android and iOS) have no Dart caller.
- scanLibraryFolderIncrementalFromSnapshot and
scanSafTreeIncrementalFromSnapshot are called only from the Dart host
branch of the library scan, which runs only when
supportsCoreBackend is false, that is never on Android or iOS. The
unreachable Dart wrappers are removed separately.
The allowlist and the when cases in MainActivity stay in sync (70
names). The UniFFI exports get_available_lyrics_providers_json and
scan_library_folder_incremental_from_snapshot were used only by the
removed handlers; the backend methods behind them stay.
Rust is the only backend, so several single-backend switches had become
constants and their other branches could never run:
- CoreBackend.supportsOutputDescriptors was always false. The
descriptor branch in SafDownloadHandler and its pfd/detachFd cleanup
never ran, and the Rust planner rejects output_fd anyway.
- routesApplication was always true on Android and iOS. Drop the member,
the "go" value of getBackendImplementations (the reply keys and the
"rust" value Dart receives are unchanged), the iOS routing guard and
the interface defaults that threw for unrouted methods.
- cleanupExtensions(), waitForDownloadProgressDelta and
scanCueForLibrary had no production caller. Dart reaches cleanup
through the cleanupExtensions channel method, and RustCoreBackend
overrides openDownloadProgress, so the polling default goes too.
The UniFFI exports DownloadState.wait_progress_delta and
ExtensionManager.scan_cue_file_for_library were used only by those
Kotlin overrides. Bindings are generated at build time by
scripts/build_rust_backend.sh; no generated binding is tracked.
Honor the requested MIME across publication paths, retain the audio suffix in temporary names, and exclude unfinished documents from full and incremental scans. Verify MIME and byte integrity across four formats and publication modes.
Provider and decryption extensions describe the downloaded container, not
its audio codec. A decrypted M4A/MP4 can still carry FLAC, so a requested
FLAC output now reaches the codec probe instead of being preserved early.
Lossy streams keep their native container and still receive metadata.
Move full and incremental ingestion, source snapshots, path keys, and download exclusion to native transactions. Preserve schema 17, FTS, WAL coordination, cancellation, and Android SAF timestamp backfill.
Port the pinned Material quantizer and Dart RNG compatibility to Rust while keeping Material Score and final color schemes in Dart. Include exact palette fixtures, licenses, and Kotlin worker ABI coverage.
Introduce Android, iOS, and Dart worker dispatch with queued cancellation, binary inputs, and background JSON codecs. Move APK verification to streamed native SHA256 while retaining the isolated fallback.
Open temporary media directory grants for reading, embedding and exporting lyrics on iOS and Android. Resolve native file aliases consistently while retaining extension storage restrictions.
Validate with 4 iOS integration tests, 42 Flutter tests, 6 Rust file capability tests, and Android Kotlin compilation. Refs #631.