From 42187bbf28dd3d27d51a202d7376cd13037d3c63 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 21:27:36 +0000 Subject: [PATCH] fix(playback): cap media-session artwork to platform bitmap limit Some OEM ROMs (e.g. LineageOS/peridot on Android 15) crash with "cannot use a recycled source in createBitmap" inside MediaMetadata.Builder.scaleBitmap() when the legacy MediaSession path sets metadata artwork. media3 size-limits artwork using Resources.getSystem()'s config_mediaMetadataBitmapMaxSize, which is unresolvable on these ROMs and falls back to the full screen width. The over-sized bitmap is then re-scaled by android.media.session.MediaSession.setMetadata(), and those ROMs recycle the source bitmap during scaling. media3's CacheBitmapLoader caches the now-recycled bitmap and reuses it on the next metadata update, hitting createBitmap() on a recycled source. Cap decoded artwork via DataSourceBitmapLoader.setMaximumOutputDimension to the same framework limit the platform compares against (resolved from the app context, 320dp default), so build() never re-scales the bitmap. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01D2raH4U7FCpK99YMQfGVQS --- .../playback/playerPool/MediaSessionPool.kt | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt index 06170fcee4..92b6fa2300 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt @@ -93,8 +93,32 @@ class MediaSessionPool( .Builder(appContext) .setExecutorService(DataSourceBitmapLoader.DEFAULT_EXECUTOR_SERVICE.get()) .setDataSourceFactory(dataSourceFactory) + // Cap decoded artwork to the platform's own metadata bitmap limit so the legacy + // MediaSession path never has to re-scale it. media3 would otherwise size-limit the + // bitmap using Resources.getSystem()'s config_mediaMetadataBitmapMaxSize, which on + // several OEM ROMs (e.g. LineageOS/peridot) is unresolvable and falls back to the full + // screen width. That over-sized bitmap then gets re-scaled by the framework inside + // android.media.session.MediaSession.setMetadata(), and some of those ROMs recycle the + // *source* bitmap during MediaMetadata.Builder.scaleBitmap(). media3's CacheBitmapLoader + // caches that now-recycled bitmap and hands it back on the next metadata update, which + // crashes with "cannot use a recycled source in createBitmap". Pre-sizing to the same + // limit the framework uses keeps build() from ever calling scaleBitmap(). + .setMaximumOutputDimension(mediaMetadataBitmapMaxSize()) .build() + /** + * Mirrors how android.media.session.MediaSession derives its metadata bitmap ceiling: the + * framework dimension config_mediaMetadataBitmapMaxSize, resolved from the app context so it + * matches the value the platform compares against. Falls back to AOSP's 320dp default when the + * (hidden, framework-internal) resource can't be resolved by name. + */ + private fun mediaMetadataBitmapMaxSize(): Int { + val resources = appContext.resources + val id = resources.getIdentifier("config_mediaMetadataBitmapMaxSize", "dimen", "android") + val resolved = if (id != 0) resources.getDimensionPixelSize(id) else 0 + return if (resolved > 0) resolved else (DEFAULT_METADATA_BITMAP_DP * resources.displayMetrics.density).toInt() + } + // protects from LruCache killing playing sessions private val playingMap = mutableMapOf() @@ -271,5 +295,9 @@ class MediaSessionPool( companion object { private val CLEANUP_INTERVAL_NS = TimeUnit.MINUTES.toNanos(1) + + // AOSP default for config_mediaMetadataBitmapMaxSize, used when the framework resource + // can't be resolved by name on a given ROM. + private const val DEFAULT_METADATA_BITMAP_DP = 320 } }