392 Commits
Author SHA1 Message Date
zarzet cb51d08939 feat(crash): show persistent crash reports on Home 2026-10-19 16:17:58 +07:00
zarzet 378c8f3749 fix(library): recover interrupted scans and stale notifications
Serialize scan notifications and surface failures even when database error persistence fails. Recover interrupted scans independently of auto-scan, retain their checkpoints, and collect matching Android process-exit details on restart.
2026-10-19 15:43:00 +07:00
zarzet a70abf8f76 docs(android): document the native channel contract
Add android/README.md: backend channel routing, the 70 nativeBackendMethods with handler file and reply kind, host-to-Dart events, the __json_file spill, progress EventChannels (400 ms Android, 800 ms iOS), the two foreground services, the SAF model, the Rust bridge files, and the iOS routing differences.

Derived from MainActivity.kt and the MainActivity*.kt handler files, MainActivityProgressStreams.kt, SelectedCoreBackend.kt, RustCoreBackendApplication.kt, NativeDataJobs.kt, SpillJsonWriter, DownloadService.kt, BackgroundWorkService.kt, MainActivitySafScan.kt, MainActivitySafIo.kt, android/app/build.gradle.kts, proguard-rules.pro, ios/Runner/AppDelegate.swift, CoreBackend.swift and lib/services/platform_bridge*.dart.
2026-10-19 05:27:36 +07:00
zarzet e80ae1b1da refactor(android): share the SAF scan percent and CUE helpers
The two SAF scans repeated the same progress arithmetic and CUE steps.
Only the runs that are identical are shared:

- safScanProgressPct(done, total) is the Int/Int to Double * 100.0
  expression all six percent sites used. The incremental scan keeps its
  zero-total guard (100.0), the full scan's call sites keep relying on a
  non-zero total as before.
- reportScanned() replaces the two identical scanned/error/percent
  updates in scanSafTree. The CUE-referenced audio skip keeps its own
  update, which never set errorCount.
- reportProcessed() moves above the changed-CUE loop, whose inline copy
  of its body it now replaces.
- resolveCueSheetAudio() covers the FILE line extraction plus sibling
  lookup that both changed-CUE loops and the unchanged-CUE loop ran.
- scanSafCueTracks() covers the audio name, timestamp, cover key and
  scanCueFromUri steps. The scans fall back to different timestamps
  when the audio's own cannot be read, so the fallback is a lambda and
  still runs only on that failure.

Cancellation checks, checkpoint writes, flushes, the copy/delete of the
temporary CUE file and every counter update stay in place and order;
progress values and the scan output are unchanged.
2026-10-19 05:16:33 +07:00
zarzet eeda955363 refactor(android): move SAF batch exists and modtime queries out of MainActivitySafScan.kt
safExistsBatch and getSafFileModTimes answer per-URI document queries
for library and history checks and do not take part in the scan, so
they move verbatim into SafBatchQueries.kt. The file header of
MainActivitySafScan.kt now describes what is left there.
2026-10-19 05:16:33 +07:00
zarzet 671b5a2502 refactor(android): move SAF tree listing and executors out of MainActivitySafScan.kt
Child listing (with the provider-loading retry), readable tree
resolution, the listing prefetcher and the ordered read pool are the
I/O layer under both scans and the history inspection, so they move
verbatim into SafTreeListing.kt together with their pool size
constants (4 listing threads, 6 read workers, unchanged).
SafTreeLister and runSafReadsInOrder become internal because the scans
stay behind. The pause poll constant stays with safScanStopRequested.
2026-10-19 05:16:33 +07:00
zarzet 2af0fa95e5 refactor(android): move SAF CUE helpers out of MainActivitySafScan.kt
The CUE sheet helpers used by both SAF scans (FILE line extraction,
the per-directory child lookup cache and the audio sibling resolver)
move verbatim into SafCueScan.kt. rememberCueDirectoryListing becomes
internal because the scans stay behind; the sibling extension list and
the lookup builder are only used in the new file and stay private.
2026-10-19 05:16:33 +07:00
zarzet bb930827fa refactor(android): move SAF scan checkpointing out of MainActivitySafScan.kt
The resumable-scan checkpoint helpers (state file path, load,
reconcile against written rows, row count and output repair) are a
self-contained unit, so they move verbatim into SafScanCheckpoint.kt.
They were file-private and become internal because scanSafTree stays
in MainActivitySafScan.kt. The checkpoint file name, its line format
and the reconciled NDJSON keys are unchanged.
2026-10-19 05:16:33 +07:00
zarzet 461e449eb2 refactor(android): move SAF file inspection out of MainActivitySafScan.kt
MainActivitySafScan.kt mixes the library scan with unrelated SAF
helpers. resolveSafFile and inspectSafFiles (with its private request
class) serve history file checks, not the scan, so they move verbatim
into SafFileInspection.kt. Signatures and visibility are unchanged.
2026-10-19 05:16:33 +07:00
zarzet 202923fbd9 test(android): pin the extension and MIME tables
ExtensionMimeTablesTest fixes the outputs of extFromFileName,
extFromMimeType, SafDownloadHandler.normalizeExt and mimeTypeForExt, and
NativeFinalizationPolicy.normalizeExtension for every table entry plus
unknown, blank, uppercase and dotted inputs. The expected values come
from reading the code; the tables are not inverse maps, so each keeps
its own entries.

The two normalizers trim, lowercase with Locale.ROOT and add one leading
dot, with "" for blank input; checking the dot before or after
lowercasing cannot differ. normalizeExt now delegates to
normalizeExtension, which becomes internal. The test passed against
both original bodies before the delegation.
2026-10-19 04:58:25 +07:00
zarzet a19a528735 refactor(android): put the SAF IO helpers on Context receivers
The MainActivitySafIo helpers declared a MainActivity receiver although
they only use Context members and the core backend. They now take a
Context receiver, and the ones that reach the backend take it as an
explicit first parameter, so a service can call them with its own
backend. buildLibraryCoverCacheKey used neither and is now a plain
function.

Bodies are unchanged apart from passing the backend through to the
helpers they call. Activity callers pass their own coreBackend, so the
same instances reach the same code. The finalizer keeps using
SafDownloadHandler.copyContentUriToTemp; switching it would add the
MediaStore fallback and MIME-based extension, which is a behavior change.
2026-10-19 04:58:25 +07:00
zarzet 071d72b255 refactor(android): share the foreground wake lock and start helper between the two services
DownloadService and BackgroundWorkService each built the same partial
wake lock (30-minute timeout, 15-minute renew, not reference-counted),
the same dataSync startForeground SDK branch and the same content
PendingIntent to MainActivity. ForegroundWorkLock now holds the lock and
the shared constants, and two small helpers cover the start branch and
the intent.

The renew strategies stay per service: DownloadService renews on its
progress and maintenance calls once 15 minutes have passed, while
BackgroundWorkService re-acquires from a main-thread Handler timer.
Wake-lock tags, channel IDs, notification IDs, intent flags and request
codes are unchanged, and the verification intent keeps its own request
code and extras.
2026-10-19 04:58:25 +07:00
zarzet f88217dde4 refactor(android): move the lifecycle backend calls into MainActivityLifecycleCalls.kt
MainActivity.kt is now the dispatcher only. The screen wake,
verification notification, install marker, runtime state, exit,
backend implementation and memory release cases keep their labels and
order in the backend `when` and now call MainActivity.handle<Name>
functions in MainActivityLifecycleCalls.kt; processPendingExtensionCallbacks
stays inline.

Bodies move verbatim. prepareRuntimeState takes the backend that
configureFlutterEngine selected as a parameter, as the case body
captured it before, and flutterBackCallback is widened to internal for
exitApp.
2026-10-19 01:24:48 +07:00
zarzet 00b661be21 refactor(android): move the download service and worker backend calls into MainActivityServiceCalls.kt
MainActivity.kt should be the dispatcher only. downloadByStrategy, the
download foreground service, background work and native download worker
cases keep their labels and order in the backend `when`; the multi-line
ones now call MainActivity.handle<Name> functions in
MainActivityServiceCalls.kt, and the one-line stop/pause/resume/cancel
cases stay inline.

Bodies move verbatim apart from the receiver label for this@MainActivity.
2026-10-19 01:24:48 +07:00
zarzet cbf41441ff refactor(android): move the library scan backend calls into MainActivityLibraryScanCalls.kt
MainActivity.kt should be the dispatcher only. The cover cache, folder
and SAF tree scan, and scan progress/cancel/pause/resume cases keep
their labels and order in the backend `when` and now call
MainActivity.handle<Name> functions in MainActivityLibraryScanCalls.kt.
Bodies move verbatim. Every handler takes (call, result) so the
dispatch lines stay uniform, even where a body ignores the call.
2026-10-19 01:24:48 +07:00
zarzet 5d0f274414 refactor(android): move the metadata and tag backend calls into MainActivityMetadataCalls.kt
MainActivity.kt should be the dispatcher only. The metadata, tag,
cover, re-enrich, post-processing and CUE cases keep their labels and
order in the backend `when` and now call MainActivity.handle<Name>
functions in MainActivityMetadataCalls.kt.

Bodies move verbatim apart from the receiver label for this@MainActivity
and re-indenting the FLAC copy-back lines of editFileMetadata, which sat
at the wrong depth.
2026-10-19 01:24:48 +07:00
zarzet 01c51bf751 refactor(android): move the SAF backend calls into MainActivitySafCalls.kt
MainActivity.kt should be the dispatcher only. The backend `when` keeps
every case label in the same order, so the routing set check and the
dispatch order are unchanged, and each SAF case now calls a
MainActivity.handle<Name> function in MainActivitySafCalls.kt.

Bodies move verbatim; the only edit is this@MainActivity becoming the
extension function's own receiver label.
2026-10-19 01:24:48 +07:00
zarzet cf4c05cd93 refactor(android): move install state, extension intents, SAF leases and small channels out of MainActivity.kt
MainActivity.kt mixed the Activity lifecycle and the backend dispatcher
with several self-contained helpers. Give each its own file as
MainActivity extension functions, following the W2-N split:

- MainActivityInstallState.kt: ensureInstallMarker, prepareRuntimeState
- MainActivityExtensionIntents.kt: verification and OAuth intent
  handling, processPendingExtensionCallbacks, session-grant events
- MainActivitySafLeases.kt: SAF playback descriptor leases
- MainActivitySmallChannels.kt: the concert_calendar and audio_output
  MethodChannels, still registered at the same point and in the same
  order in configureFlutterEngine

The state these helpers touch stays in MainActivity and is widened from
private to internal. Bodies move verbatim except that companion members
are qualified with MainActivity. outside the class.
2026-10-19 01:24:48 +07:00
zarzet 20b4c73b8d refactor(android): move RustCoreBackend.invokeApplication into its own file
NT-16 Kotlin part. The application method dispatch (about 290 lines) moves
verbatim into RustCoreBackendApplication.kt as an internal extension
`RustCoreBackend.dispatchApplication`. The `invokeApplication` override
stays in SelectedCoreBackend.kt as a one-line delegate. `synchronized(this)`
still locks the RustCoreBackend object.

The object state and helpers the dispatch uses are widened from private to
internal; method names, argument keys, results and error strings are
unchanged.
2026-10-19 00:21:34 +07:00
zarzet b6b609f334 refactor(android): move Impeller helpers, the SAF tree picker and progress streams out of MainActivity.kt
First NT-12 seam group, following the MainActivitySafScan.kt and
MainActivitySafIo.kt receiver-function shape. Bodies are moved verbatim.

- ImpellerCompatibility.kt: the device lists and shouldDisableImpeller /
  getGpuRenderer, now file-level. ImpellerAwareFlutterFragment stays nested
  in MainActivity because proguard-rules.pro keeps it by that name.
- MainActivitySafTreePicker.kt: the pickSafTree launch (the case now calls
  launchSafTreePicker), the activity-result handler, and the storage id and
  display path helpers. The launcher registration stays a MainActivity
  property so it is still registered at construction.
- MainActivityProgressStreams.kt: both progress EventChannels, their
  start/stop/poll helpers and the channel name and polling constants.

Stream state, the picker store, the launcher, the coroutine scope, the
backend channel and the companion's pending picker result are widened from
private to internal so the receiver functions can reach them. Channel
names, payload keys, error codes and dispatch order are unchanged.
2026-10-19 00:21:34 +07:00
zarzet cc9b9f1162 test(android): assert the backend routing set matches the dispatch cases
MainActivity handles a backend call in its own `when` only when the method
name is listed in `nativeBackendMethods`; any other name falls through to the
selected core backend. A name added to only one of the two silently changes
where the call is handled. This JVM test reads MainActivity.kt as source,
extracts the set literal and the top-level string case labels of
`when (call.method)`, and fails with the symmetric difference when they drift.
2026-10-19 00:21:34 +07:00
zarzet b5cf97881f refactor(android): move finalizer history into NativeFinalizerHistory.kt
Move worker request contract validation and the download history row
and JSON mapping out of NativeDownloadFinalizer into a sibling file of
extension functions. Bodies are verbatim. NATIVE_WORKER_CONTRACT_VERSION
stays on the object with its value, so its contract test is unchanged.
The history column contract test now reads buildHistoryRow from the new
file. normalizeOptional moves with the row builder, so
cleanMetadataString widens from private to internal.
2026-10-19 00:14:47 +07:00
zarzet 631b9aa5e8 refactor(android): move finalizer post-processing into NativeFinalizerPostProcess.kt
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.
2026-10-19 00:14:47 +07:00
zarzet 0b4725c2c0 refactor(android): move finalizer metadata into NativeFinalizerMetadata.kt
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.
2026-10-19 00:14:47 +07:00
zarzet ead04c00b4 refactor(android): move finalizer container into NativeFinalizerContainer.kt
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.
2026-10-19 00:14:47 +07:00
zarzet 92af56eea7 refactor(android): move finalizer decrypt into NativeFinalizerDecrypt.kt
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.
2026-10-19 00:14:47 +07:00
zarzet 92adce43ef refactor(native): share errorJson and retire Go-era names in native comments
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.
2026-10-18 23:01:04 +07:00
zarzet fcec0942b9 refactor(android): drop SDK checks below minSdk 24
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.
2026-10-18 23:01:04 +07:00
zarzet 8aca5e4ef2 refactor(android): drop unreachable dispatch cases and dead channel handlers
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.
2026-10-18 23:01:04 +07:00
zarzet ca950dc35a refactor(android): remove backend switches that became constants
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.
2026-10-18 23:01:04 +07:00
zarzet 66d480dbdc fix(storage): retain legacy Android cipher migration support 2026-10-18 04:09:16 +07:00
zarzet 75cd4c39b2 feat(android): refresh launcher widgets with adaptive expressive layouts 2026-10-18 03:10:40 +07:00
zarzet e0dfa7eb69 refactor(android): scope Rust download state handles consistently 2026-10-18 00:46:53 +07:00
zarzet 504c3cd18b refactor(android): share native album artist resolution 2026-10-18 00:46:10 +07:00
zarzet fdcd291c78 refactor(android): remove unreachable backend dispatch fallbacks 2026-10-18 00:46:09 +07:00
zarzet 3d11169f21 fix(network): request Android 17 local network access before NAS connections 2026-10-17 16:18:41 +07:00
zarzet 11c6cf9915 feat(player): synchronize music haptics with local playback 2026-10-17 16:18:37 +07:00
zarzet 40f65d3a4d feat(player): show audio source processing and output details 2026-10-17 16:18:32 +07:00
zarzet 3dc2d69051 fix(audio): preserve FLAC precision in Android bit-perfect playback 2026-10-15 00:28:17 +07:00
zarzet c0fd506c78 fix(storage): preserve audio MIME when publishing SAF downloads
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.
2026-10-13 19:38:11 +07:00
zarzet 186c66aeb8 fix(lyrics): refresh LRC sidecars in their actual SAF folder
Resolve opaque SAF document ancestry before writing adjacent lyrics. Propagate failed requested LRC writes and cover native word-synced refresh in regression tests.
2026-10-13 19:38:00 +07:00
zarzet e3b69a25ef fix(download): probe M4A streams before preserving requested FLAC
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.
2026-10-13 18:27:16 +07:00
zarzet ac8523d789 feat(metadata): add per-field download tag separators 2026-10-12 12:11:06 +07:00
zarzet ebaa6c5c1e test(android): accept validated FLAC fallback across FFmpeg versions 2026-10-11 16:14:01 +07:00
zarzet d611590039 chore(android): update Gradle Kotlin NDK and native build dependencies 2026-10-11 14:33:52 +07:00
zarzet 1b9c35f322 fix(downloads): preserve browser verification and queues across restarts 2026-10-09 18:07:49 +07:00
zarzet 37258a6160 fix(setup): recover folder permissions after Android restarts 2026-10-09 18:07:38 +07:00
zarzet c7b1d0b0ec fix(metadata): preserve original artwork resolution for SAF files 2026-10-09 10:34:49 +07:00
zarzet 2ded1cff68 perf(downloads): batch queue cancellation and progress cleanup 2026-10-08 08:47:47 +07:00
zarzet c67d2a2dd2 perf(downloads): batch cancellation resets for retry 2026-10-08 06:55:26 +07:00