Commit Graph
3310 Commits
Author SHA1 Message Date
birdbird 2efdba5f36 Clean MVC boundary transgressions 2026-10-04 17:39:42 +00:00
birdbird 402e1c4b24 Merge branch 'downloaduifix' into 'develop'
Apply the bind-time download state directly to the track row

See merge request ultrasonic/ultrasonic!1439
2026-10-04 17:31:23 +00:00
birdbird 8ffc97b7d2 Apply the bind-time download state directly to the track row 2026-10-04 17:31:22 +00:00
birdbird eb1d0450af Merge branch 'download' into 'develop'
Fix bugs in download handling and restructure service.

See merge request ultrasonic/ultrasonic!1438
2026-10-02 11:51:58 +00:00
birdbird 048923d674 Fix DownloadService manifest label
The DownloadService was labeled 'Ultrasonic Media Player Service' (copied
from PlaybackService); it now says 'Ultrasonic Download Service'.
2026-10-02 13:46:47 +02:00
birdbird 615a330b62 Extract injectable DownloadManager from the DownloadService companion
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.
2026-10-02 13:46:47 +02:00
birdbird 2c0ce5bb28 Fix failedList removal in deleteAsync and narrow task exception handler
- 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.
2026-10-02 11:21:28 +00:00
birdbird bac11ad2c4 Clamp download progress to 0-100 percent
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.
2026-10-02 11:20:41 +00:00
birdbird de5c89f0a3 Remove no-op let { null } blocks in setSaveFlagForTracks
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.
2026-10-02 11:20:08 +00:00
birdbird cd30bcfd2f Merge branch 'jukeboxstart' into 'develop'
fix: jukebox fails to send start action when starting playback with empty queue

Closes #1266

See merge request ultrasonic/ultrasonic!1437
2026-10-01 22:10:17 +00:00
birdbird e63dfa13e8 fix: jukebox fails to send start action when starting playback with empty queue 2026-10-01 22:10:16 +00:00
birdbird 71f505da76 Merge branch 'coverrace' into 'develop'
Fix infinite preload setting and race condition in imageloader

See merge request ultrasonic/ultrasonic!1436
2026-10-01 21:48:46 +00:00
birdbird 0420a2bf7b Fix infinite preload setting and race condition in imageloader 2026-10-01 21:48:46 +00:00
birdbird c4febfc371 Merge branch 'feature/auto-genres' into 'develop'
feat(auto): add Genres browsing to Android Auto

See merge request ultrasonic/ultrasonic!1382
2026-10-01 21:44:19 +00:00
Sam Mingo 71b50770b6 feat(auto): add Genres browsing to Android Auto 2026-10-01 21:44:19 +00:00
birdbird 5c39ab184c Merge branch 'juke' into 'develop'
Surface jukebox error message before falling back to local playback

See merge request ultrasonic/ultrasonic!1434
2026-10-01 21:39:14 +00:00
birdbird 7772898182 Merge branch 'playbackstops' into 'develop'
Little playback fixes

See merge request ultrasonic/ultrasonic!1435
2026-10-01 21:35:10 +00:00
birdbird de326beaf2 Little playback fixes 2026-10-01 21:35:10 +00:00
birdbird f3dd7808ac Surface jukebox error message before falling back to local playback 2026-10-01 23:33:36 +02:00
birdbird 1ce70142a1 Merge branch 'bug' into 'develop'
fix: protect in-flight downloads from CacheCleaner partial cleanup

See merge request ultrasonic/ultrasonic!1433
2026-10-01 17:24:30 +00:00
birdbird 23c95f2ccc fix: protect in-flight downloads from CacheCleaner partial cleanup 2026-10-01 19:15:45 +02:00
birdbird a58730421f Merge branch 'util' into 'develop'
refactor: split Util.kt into FormatUtil, UiUtil and NotificationUtil

See merge request ultrasonic/ultrasonic!1432
2026-10-01 16:40:03 +00:00
birdbird 4ccd50cf6b refactor: split Util.kt into FormatUtil, UiUtil and NotificationUtil 2026-10-01 16:40:03 +00:00
birdbird 257e398d9f Merge branch 'smoothswitch' into 'develop'
Finetune Offline/Online handling

See merge request ultrasonic/ultrasonic!1431
2026-10-01 11:55:46 +00:00
birdbird 5501c3294b di: stop regenerating the Koin music service module at runtime
The music service module was unloaded and reloaded by MusicServiceFactory on
every library access state change, which forced MusicServiceFactory and
ImageLoaderProvider to inject the Koin instance itself and the fetcher
factories to resolve the SubsonicAPIClient lazily from a volatile module.

- MusicServiceFactory now owns the swappable delegates directly: it builds
  OfflineMusicService or CachedMusicService(RESTMusicService(client)) for the
  current access state, exposes apiClient() alongside getMusicService(), and
  keeps resetMusicService() as the explicit hook for server property edits
- the delegate builders are constructor-injectable, keeping the switch logic
  testable without the REST stack
- ImageLoaderProvider takes a () -> SubsonicAPIClient provider and rebuilds
  the loader when the client instance changes, replacing the ServerID Koin key
- CoverArtFetcher/AvatarFetcher factories resolve the client through the
  factory instead of the module
- musicServiceModule is deleted; the DI graph is static and no module is
  ever unloaded at runtime
2026-10-01 13:33:50 +02:00
birdbird bae723595a PlaybackService: keep playback seamless across offline/online transitions
Switching the library between online and offline rebuilt the player
unconditionally (release -> new ExoPlayer -> setMediaItems -> prepare),
which paused audio and even restarted playback after server property edits.

- entering automatic offline is now a pure state flip: the queue and the
  player are untouched, unplayable entries are skipped by the playback
  failure recovery, and the original order resumes once the server is
  reachable again
- returning online does nothing: stream URLs are re-resolved on the next
  load through the regenerated music service
- engaging manual offline mode prunes the timeline in place instead of
  rebuilding the player: removals never touch the playing item, rebuilt
  URIs are applied via replaceMediaItem at adjusted indexes
- the transition decision is extracted into AccessStateTransitions and the
  prune plan into OfflineQueuePruner, both unit-tested
- the bus subscription is replaced by a single accessState collector, which
  also removes the cross-coroutine serverSelectionChanged flag race and the
  pointless player restart on server property edits
- the access state reaction replaces the racy flag with a previous-state
  comparison; full player rebuilds remain for JUKEBOX switches, server
  switches and the unplayable-current-item fallback
2026-10-01 13:33:50 +02:00
birdbird 304f6b7fd9 PlaybackService: gate automatic offline behind a reachability probe and recover from item failures
A transient streaming error used to flip the whole library into automatic
offline via reportNetworkFailure(playback=true), which pruned every
non-downloaded item from the queue and stopped playback outright.

- absorb transient connectivity hiccups in the loader with a custom
  LoadErrorHandlingPolicy (~45s backoff budget) instead of Media3's default
- on an item failure, recover in place: bounded same-item retries, then skip
  to the next item in play order; the queue is never edited
- automatic offline now requires a failed background ping of the selected
  server; the playback error alone is no longer evidence of an outage
- drop the now-unreachable PLAYBACK_NETWORK_ERROR classification path and
  the playback parameter of reportNetworkFailure
2026-10-01 13:33:50 +02:00
birdbird 250743a30f Merge branch 'further' into 'develop'
UApp: fix Koin readiness flag, first-run race and context accessor

See merge request ultrasonic/ultrasonic!1429
2026-09-30 08:03:03 +00:00
birdbird fbe409b47c di: replace KoinComponent service-locator sprawl with constructor injection
Move ~20 classes from KoinComponent/'by inject()' lookup to constructor
injection, registered in the Koin modules; the dependency graph is now
visible in constructors and these classes are testable without Koin.

- util: CacheCleaner, CommunicationError, DownloadUtil, ContextMenuUtil
  are now plain classes provided as singles.
- adapters: Utils object becomes PopupMenuFactory; TrackViewBinder,
  HeaderViewBinder, ArtistRowBinder, TextRowBinder, ShareRowBinder,
  AlbumRowDelegate/AlbumGridDelegate receive their dependencies (incl.
  PopupMenuFactory) via constructor; FolderSelectorBinder drops a dead
  KoinComponent.
- service: MusicServiceFactory and ImageLoaderProvider receive the Koin
  instance explicitly (they must resolve from the dynamically reloaded
  music service module); PlaybackStateSerializer, DownloadTask,
  MediaLibrarySessionCallback, CachedMusicService and OfflineMusicService
  take their dependencies via constructor.
- subsonic: VideoPlayer becomes an injected class; AvatarFetcher and
  CoverArtFetcher take the SubsonicAPIClient, their Coil factories accept
  a lazy clientProvider so the client stays current across server
  switches.
- models: EditServerModel, GenericListModel and all subclasses get
  constructor injection and are wired in the viewModel registrations.
- imageloader: ArtworkBitmapLoader takes the ImageLoaderProvider.
- view: ChatAdapter takes activeServerProvider/imageLoaderProvider.

Service-locator access is confined to Android framework entry points
(UApp, DownloadService, PlaybackService, UltrasonicIntentReceiver,
AlbumArtContentProvider) and the shared Fragment error-handling
extensions; redundant KoinComponent/KoinScopeComponent declarations were
removed from fragments.

Tests: CommunicationErrorReportingTest, AvatarFetcherTest,
CoverArtFetcherTest and OfflineMusicServiceCatalogTest inject mocks
directly instead of booting Koin.
2026-09-30 09:58:58 +02:00
birdbird caa45c2243 RatingManager: keep failed rating submissions retryable
The dedupe marker (lastUpdate) was recorded even when the submission failed,
so re-submitting the identical rating afterwards was silently suppressed.
lastUpdate is now cleared again when the submission fails (only if no newer
rating was recorded meanwhile), while identical updates arriving while a
submission is in flight are still deduplicated. Also makes lastUpdate
@Volatile: it is written from both bus collectors and rating submissions
on different dispatchers.
2026-09-29 14:12:09 +02:00
birdbird 91b5bc3c74 DownloadService: report DOWNLOADING/RETRYING for active downloads
The early 'activeDownloads.contains -> QUEUED' return made the subsequent
DOWNLOADING/RETRYING branch unreachable: every in-flight download was shown
as queued. QUEUED is now reserved for tracks waiting in the queue.
2026-09-29 14:12:09 +02:00
birdbird 82d5ed569e PlaybackService: drop static actualBackend/instance state
actualBackend becomes an instance field; it was only read inside the service.
The PlaybackService.instance singleton check in UltrasonicIntentReceiver is
replaced by the MediaControllerProvider connection state, which tracks the
service lifecycle (Connected/Connecting while alive, Disconnected/Failed
otherwise) without global mutable state.
2026-09-29 14:12:09 +02:00
birdbird 10cd936fac Standardize coroutine scopes: SupervisorJob + logging exception handler
Introduce CoroutinePatterns.loggingScope() and replace all bare
CoroutineScope(Dispatchers.X) delegations and ad-hoc scope creations
with it. Previously a single failed background task in ActiveServerProvider,
PlaybackService, Scrobbler, CacheCleaner, EqualizerController, FileLoggerTree,
ImageLoader, PlayerFragment or TrackViewHolder propagated to the default
uncaught exception handler and crashed the process; now failures are
supervised and logged.

Also removes the redundant ad-hoc IO scope in ArtistRowBinder: executeOn()
already dispatches off the main thread with its own exception handler.
2026-09-29 14:12:09 +02:00
birdbird a921ff8ea8 UApp: fix Koin readiness flag, first-run race and context accessor
- initiated is now owned by startKoin() alone; setting it before super.onCreate()
  made the NavigationActivity Koin-restart fallback unreachable
- isFirstRun is computed on the main thread: NavigationActivity.onCreate can run
  before the background initialization completed and read a stale 'false',
  silently skipping the welcome dialog on first install
- applicationContext() fails with a descriptive error instead of a bare NPE
  when accessed before Application.onCreate
2026-09-29 14:12:09 +02:00
birdbird 037787819d Merge branch 'refactor2' into 'develop'
Cleanup concerns and mixed scopes.

See merge request ultrasonic/ultrasonic!1424
2026-09-29 11:00:47 +00:00
birdbird d4bc785a46 Cleanup concerns and mixed scopes. 2026-09-29 11:00:46 +00:00
birdbird d84c3a646e Merge branch 'decouple' into 'develop'
Decouple list ViewModels from SwipeRefreshLayout

See merge request ultrasonic/ultrasonic!1428
2026-09-29 09:11:07 +00:00
birdbird c4f4bbd9d6 Decouple list ViewModels from SwipeRefreshLayout
GenericListModel no longer takes a SwipeRefreshLayout: refresh() and
backgroundLoadFromServer(refresh) are View-free, and the loading
indicator is now published via an isRefreshing LiveData that the
observing fragment forwards to its swipe-to-refresh view. Error
handling uses the application context instead of the swipe view's
context. The five list models (Playlist, Podcast, Share, Artist,
Genre) drop the swipe parameter from getItems(); fragments no longer
pass swipeRefresh into model calls and MultiListFragment observes the
model's loading state to drive the indicator.
2026-09-29 11:04:18 +02:00
birdbird d486d26e90 Merge branch 'blocking' into 'develop'
Remove runBlocking from ServerSettingsModel user-facing paths

See merge request ultrasonic/ultrasonic!1426
2026-09-29 08:53:40 +00:00
birdbird 80573c0cae Merge branch 'renovate/gradle-9.x' into 'develop'
Update Gradle to v9.8.0

See merge request ultrasonic/ultrasonic!1425
2026-09-29 08:51:17 +00:00
birdbird 52bd698f8d Merge branch 'renovate/mockitokotlin' into 'develop'
Update dependency org.mockito.kotlin:mockito-kotlin to v6.4.0

See merge request ultrasonic/ultrasonic!1414
2026-09-29 08:50:40 +00:00
birdbird f6cc77e463 Merge branch 'renovate/navigation' into 'develop'
Update navigation to v2.10.2

See merge request ultrasonic/ultrasonic!1403
2026-09-29 08:50:27 +00:00
birdbird 506dbee486 Merge branch 'renovate/kotlin-monorepo' into 'develop'
Update kotlin monorepo to v2.4.20

See merge request ultrasonic/ultrasonic!1401
2026-09-29 08:48:18 +00:00
birdbird d2cc823a0b Merge branch 'renovate/androidxcore' into 'develop'
Update dependency androidx.core:core-ktx to v1.19.1

See merge request ultrasonic/ultrasonic!1415
2026-09-29 08:47:32 +00:00
birdbird 54cb24df8e Remove runBlocking from ServerSettingsModel user-facing paths
getServerList() blocked the calling thread (main) while checking and fixing
server indexes on every resume; it now returns a liveData whose first emission
happens after reindexing. addDemoServer() is now a suspend function and the
welcome dialog launches it in lifecycleScope instead of blocking.
2026-09-29 10:46:04 +02:00
Renovate Bot 38008d5555 Update Gradle to v9.8.0 2026-09-29 08:44:13 +00:00
Renovate Bot d02eb0cf22 Update dependency org.mockito.kotlin:mockito-kotlin to v6.4.0 2026-09-29 08:44:08 +00:00
Renovate Bot 49f50b4e3e Update navigation to v2.10.2 2026-09-29 08:44:05 +00:00
Renovate Bot d4e0318a98 Update kotlin monorepo to v2.4.20 2026-09-29 08:33:17 +00:00
birdbird 2e75d962ae Merge branch 'ci' into 'develop'
Fix duplicate MEDIA_ARTIST_PLAY_ID declaration after merge

See merge request ultrasonic/ultrasonic!1427
2026-09-29 07:53:03 +00:00