From 70bdafab488c4967cdc882083cd98f5824154ba1 Mon Sep 17 00:00:00 2001 From: KhooLy <73142442+KhooLy@users.noreply.github.com> Date: Sun, 19 Jul 2026 23:38:13 +0300 Subject: [PATCH] fix: recycled bitmap crash in Now Playing metadata --- .../PlayerNowPlayingController.android.kt | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerNowPlayingController.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerNowPlayingController.android.kt index b13954a0..c5fdf7ca 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerNowPlayingController.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerNowPlayingController.android.kt @@ -90,7 +90,10 @@ internal class AndroidPlayerNowPlayingController( private var metadata: AndroidNowPlayingMetadata? = null private var snapshot = PlayerPlaybackSnapshot() - private var artworkBitmap: Bitmap? = null + private var artworkArt: Bitmap? = null + private var artworkAlbumArt: Bitmap? = null + private var artworkDisplayIcon: Bitmap? = null + private var artworkNotificationIcon: Bitmap? = null private var released = false private var lastPublishedPositionMs = Long.MIN_VALUE private var lastPublishedDurationMs = Long.MIN_VALUE @@ -126,7 +129,10 @@ internal class AndroidPlayerNowPlayingController( mediaSession.isActive = true if (artworkChanged) { - artworkBitmap = null + artworkArt = null + artworkAlbumArt = null + artworkDisplayIcon = null + artworkNotificationIcon = null loadArtwork(normalized.artworkUrl) } @@ -184,7 +190,10 @@ internal class AndroidPlayerNowPlayingController( artworkGeneration.incrementAndGet() metadata = null snapshot = PlayerPlaybackSnapshot() - artworkBitmap = null + artworkArt = null + artworkAlbumArt = null + artworkDisplayIcon = null + artworkNotificationIcon = null resetPublishedPlaybackState() mediaSession.setMetadata(null) mediaSession.setPlaybackState( @@ -209,13 +218,23 @@ internal class AndroidPlayerNowPlayingController( snapshot.durationMs.takeIf { it > 0L }?.let { durationMs -> builder.putLong(MediaMetadata.METADATA_KEY_DURATION, durationMs) } - artworkBitmap?.let { artwork -> - builder.putBitmap(MediaMetadata.METADATA_KEY_ART, artwork) - builder.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, artwork) - builder.putBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON, artwork) + + val base = builder.build() + val withArtwork = artworkArt?.takeIf { !it.isRecycled }?.let { art -> + MediaMetadata.Builder(base) + .putBitmap(MediaMetadata.METADATA_KEY_ART, art) + .apply { + artworkAlbumArt?.takeIf { !it.isRecycled } + ?.let { putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, it) } + artworkDisplayIcon?.takeIf { !it.isRecycled } + ?.let { putBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON, it) } + } + .build() } - mediaSession.setMetadata(builder.build()) + if (withArtwork == null || runCatching { mediaSession.setMetadata(withArtwork) }.isFailure) { + runCatching { mediaSession.setMetadata(base) } + } } private fun publishPlaybackState(force: Boolean) { @@ -275,7 +294,7 @@ internal class AndroidPlayerNowPlayingController( sessionToken = mediaSession.sessionToken, metadata = currentMetadata, snapshot = snapshot, - artwork = artworkBitmap, + artwork = artworkNotificationIcon?.takeIf { !it.isRecycled }, ) PlayerNowPlayingService.publish(appContext, notification) } @@ -291,10 +310,12 @@ internal class AndroidPlayerNowPlayingController( mainHandler.post { if (released || generation != artworkGeneration.get() || metadata?.artworkUrl != urlString) { - bitmap?.recycle() return@post } - artworkBitmap = bitmap + artworkArt = bitmap + artworkAlbumArt = bitmap?.let(::copyArtwork) + artworkDisplayIcon = bitmap?.let(::copyArtwork) + artworkNotificationIcon = bitmap?.let(::copyArtwork) publishMetadata() publishNotification() } @@ -533,6 +554,9 @@ private fun downloadArtwork(urlString: String): Bitmap? { } } +private fun copyArtwork(bitmap: Bitmap): Bitmap? = + if (bitmap.isRecycled) null else runCatching { bitmap.copy(bitmap.config ?: Bitmap.Config.ARGB_8888, false) }.getOrNull() + private fun decodeSampledBitmap(bytes: ByteArray, maxEdgePx: Int): Bitmap? { val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true } BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bounds)