Unify the async models: GenericListModel now exposes musicFolders and
isRefreshing as StateFlow like PlayerViewModel does, and the observing
fragments (MultiListFragment, EntryListFragment) collect the flows under
repeatOnLifecycle(STARTED) instead of LiveData observe.
GenericListModel extended AndroidViewModel and exposed context by reaching
into the Application. Replace it with a plain ViewModel that receives the
application context via constructor injection, keeping the model testable
and decoupled from the Application class. Subclasses (list models and
DownloadListModel) now take a Context instead of an Application; Koin
already registers the Android context, so no DI changes were needed.
All queue management and download state moves from the static DownloadService
companion into an injectable DownloadManager singleton (Koin): the queue,
active downloads, failed list, LiveData, state callbacks and retry loop now
live there. DownloadService becomes a thin lifecycle shell that attaches to
the manager on create and detaches on destroy, keeping only the foreground
notification, Wi-Fi lock and storage monitor. Service start/stop is isolated
in a DownloadServiceController so the manager has no Android context handling.
Also included, needed to make the extracted logic correct and testable:
- removeDownloadedTracksFromList and setSaveFlagForTracks now gate on
Storage.isPathExists instead of getFromPath; with the default java.io
storage root getFromPath returns a wrapper for nonexistent paths, which
wrongly filtered out every track (silently disabling preload downloads).
- downloadStateChangedCallback requeues a RETRYING item before triggering
processNextTracks, so a retry is immediately eligible instead of waiting
for the next callback.
- processNextTracks guards against a detached service (null scope/host),
fixing a potential crash when a task callback fires after destruction.
All former DownloadService.xxx call sites (PlaybackService, NavigationActivity,
SettingsFragment, TrackViewHolder/TrackViewBinder, DownloadUtil, TrackCollectionModel,
DownloadListModel and the Koin modules) now use the injected manager.
Adds DownloadManagerTest (Robolectric) covering enqueue/dedup, downloaded-track
filtering, state reporting, clearDownloads, empty-queue draining, retry
requeuing, failure tracking + delete, and post-detach callback safety.
- deleteAsync removed a failedList entry from the downloadQueue (a no-op:
failed items were already polled out of the queue), leaving stale FAILED
state for deleted tracks. Remove the entry from failedList instead.
- The DownloadTask CoroutineExceptionHandler deleted the complete and
pinned files on any uncaught exception. Those files may already hold a
finalized download (exceptions can occur in post-rename steps such as
the catalog write), so this destroyed finished data. Only the partial
file is deleted now: retries do not append to a possibly untrustworthy
stream, while finalized downloads and cancelled (resumable) downloads
are left untouched.
The metadata size may differ from the bytes actually transferred
(transcoded streams) or from the resumed total, so the raw progress
calculation could leave the 0..100 range. Coerce the reported value
into the valid range.
These discarded null expressions did nothing; the filtering they were
originally meant to perform is handled by the rename blocks and by
DownloadTask.checkIfExists. Removes the misleading dead code.