Skip to content

Fix cloud artwork getting stuck on the placeholder (#138) - #140

Merged
lostf1sh merged 3 commits into
mainfrom
fix/issue-138-cloud-artwork
Oct 4, 2026
Merged

lostf1sh merged 3 commits into
mainfrom
fix/issue-138-cloud-artwork

Conversation

@lostf1sh

@lostf1sh lostf1sh commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #138.

What was wrong

The media session hands every controller (the app's own included) cloud covers as content://<pkg>.artwork/cloud/.... The player, the notification bitmap loader, and the widgets all loaded that URI through SharedArtworkContentProvider. The provider decodes the cover with the same Coil ImageLoader and streams it into a pipe, while the outer decode blocks reading that pipe.

Coil allows four concurrent BitmapFactory decodes. Once four of these outer decodes were in flight (player carousel + mini player + notification + theme extraction is enough), they held every slot. The inner decodes they were waiting on could never start, so every artwork load in the app stayed on its placeholder until restart. That matches both reports: it breaks after a song or two, and re-syncing doesn't help.

Reproduced on an API 36 emulator against Navidrome 0.64.2. A thread dump of the stuck app showed exactly four BitmapFactoryDecoder threads blocked in ParcelFileDescriptor$AutoCloseInputStream.read.

Changes

  • SharedCloudArtworkMappers: Coil mappers turn our own content://…/cloud/… URIs back into navidrome_cover:// / jellyfin_cover:// before fetching. In-process loads no longer go through the provider. External apps still use it.
  • RemoteArtworkCache, shared by both cloud fetchers:
    • Only bodies with an image signature are cached. Subsonic servers report getCoverArt errors with HTTP 200, and those bodies used to be cached as .jpg permanently. Poisoned files cached earlier are evicted.
    • Downloads go to a temp file and are then renamed into place. Concurrent requests for the same cover can no longer truncate a file another request is decoding.
  • System UI artwork: System UI reads the platform session's artwork URI without connecting as a Media3 controller, so it never got the per-item grant and was refused on every track (3 failed opens per song in logcat). The provider now also allows callers holding MEDIA_CONTENT_CONTROL (signature|privileged; such callers can already read and control every media session). Other apps still need the explicit grant. Stock Android fell back to the session bitmap; System UIs without that fallback could show no artwork.

Testing

  • ./gradlew :app:compileDebugKotlin :app:testDebugUnitTest passes.
  • New RemoteArtworkCacheTest covers: error body not cached, atomic replace without leftover temp files, eviction of poisoned cache, format signatures.
  • SharedArtworkContentProviderTest gains a case for privileged media surfaces.
  • Device smoke test (emulator, local Navidrome with 8 albums):
    • Before: artwork stuck after a few skips.
    • After: correct covers and theme through 20 rapid skips. Thread dump shows no blocked decoders.
    • Media controls in the shade: 0 grant failures and the correct album colour across 5 tracks.
  • lintDebug was already failing on main (UnsafeOptInUsageError in FadingPlayer.kt, Russian plural quantities, existing MissingTranslation). This PR adds no new lint errors.

Review follow-up (ae19bb4)

  • Privileged reads are scoped. Callers holding MEDIA_CONTENT_CONTROL may open only the artwork the session currently publishes. MappingPlayer records it, keeping the last three entries, keyed by song id or raw cloud URI. The list is cleared when MusicService is destroyed.
  • No deletion on a failed signature check. A cached body that fails the check is skipped instead of deleted, which removes the race with a concurrent rename. The next successful download replaces it.
  • Verified: unit tests pass (SharedArtworkContentProviderTest now 14 tests). On the emulator, the media controls showed the current cover with no denied opens across 4 tracks.

The media session exposes cloud covers as content://<pkg>.artwork/cloud/... to every controller, including the app's own, so the player, notification bitmap loader, and widgets loaded them through SharedArtworkContentProvider. The provider decodes the cover with the same Coil ImageLoader while the outer decode blocks on the pipe; Coil allows four concurrent BitmapFactory decodes, so four such loads held every slot and the inner decodes never started, leaving all artwork on its placeholder until restart.

Map those URIs back to navidrome_cover:// / jellyfin_cover:// before Coil fetches them. Also stop caching non-image bodies (Subsonic reports errors with HTTP 200), evict ones cached earlier, and write downloads through a temp file so concurrent requests can't truncate a file being decoded.
System UI reads the platform session's artwork URI without connecting as a Media3 controller, so it never received the per-item grant and every open was refused. Allow callers holding MEDIA_CONTENT_CONTROL, which can already read and control every media session; other apps still need the explicit grant.
@greptile-apps

greptile-apps Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Fixes artwork loading deadlock in the image cache layer.

The PR appears safe to merge, though the previously noted cloud-cover sharpness reduction remains.

Fix All in Claude CodeFindings

  1. P2 Cloud covers lose resolution ▶
Fix with agent prompt
### Issue 1
app/src/main/java/com/lostf1sh/pixelplayeross/data/image/SharedCloudArtworkMappers.kt:26-27
The new mappers send in-process cloud artwork straight to the fetchers, but the mapped URIs have no size parameter. Both fetchers therefore request their 500-pixel default instead of the provider’s 1024-pixel artwork. Covers on larger player surfaces may look less sharp.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Summary

The PR routes in-process cloud artwork loads directly to their fetchers, validates and atomically replaces downloaded covers, and limits privileged System UI artwork reads to recently published session artwork.

  • The follow-up removes a cache-eviction race and narrows the privileged artwork exception.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  S[Media session] --> M[Mapped artwork URI]
  M --> A[In-app image loader]
  A --> F[Cloud fetcher]
  F --> C[Validated artwork cache]
  M --> U[System UI]
  U --> P[Artwork provider]
  P --> G{Grant or published privileged artwork?}
  G -->|Yes| C
  G -->|No| D[Access denied]
Loading

Reviews (2) · Last reviewed commit: "Scope System UI artwork reads to the ses..."

Comment on lines +26 to +27
return SharedArtworkContentProvider.parseCloudArtworkUri(data.toString(), packageName)
?.let(Uri::parse)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Cloud covers lose resolution
The new mappers send in-process cloud artwork straight to the fetchers, but the mapped URIs have no size parameter. Both fetchers therefore request their 500-pixel default instead of the provider’s 1024-pixel artwork. Covers on larger player surfaces may look less sharp.

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/main/java/com/lostf1sh/pixelplayeross/data/image/SharedCloudArtworkMappers.kt
Line: 26-27

Comment:
**Cloud covers lose resolution**
The new mappers send in-process cloud artwork straight to the fetchers, but the mapped URIs have no size parameter. Both fetchers therefore request their 500-pixel default instead of the provider’s 1024-pixel artwork. Covers on larger player surfaces may look less sharp.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a regression. Before this PR the provider's inner request also loaded navidrome_cover://<id> with no size parameter, so the fetchers already requested 500 px. CLOUD_ARTWORK_SIZE_PX only set the decode target, not the server request, so the image and its resolution are unchanged. Raising the default request size would be a separate change.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — this is not a regression. Before this change, the provider's inner ImageLoader request already used navidrome_cover:///jellyfin_cover:// without a size parameter, so the fetchers requested their existing 500 px default. CLOUD_ARTWORK_SIZE_PX only controlled the decode target, not the server-requested artwork size. Mapping the URI directly therefore preserves the same source resolution; increasing the request default would be a separate improvement. I'll withdraw this finding.

Comment thread app/src/main/java/com/lostf1sh/pixelplayeross/data/image/RemoteArtworkCache.kt Outdated
Holding MEDIA_CONTENT_CONTROL let a privileged caller open any /song/<id> artwork, not just what the session publishes. MappingPlayer now records the artwork it exposes for the current item, and privileged callers may open only those (the last three, keyed by song id or raw cloud URI so cache-bust tokens don't matter).

Also stop deleting a cached cover that fails the signature check: another request may have renamed a fresh download into place after the check read the old file. Skipping it is enough, since the next successful download replaces it.
@lostf1sh
lostf1sh merged commit 7e79602 into main Oct 4, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Missing album artwork

1 participant