address review: offload artwork decode, drop the redundant metadata override, cancel the loader

This commit is contained in:
austinried
2026-10-04 00:29:58 +00:00
parent 4095d038a9
commit 97ca4230da
3 changed files with 35 additions and 24 deletions
@@ -5,9 +5,9 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.media3.common.MediaMetadata
import androidx.media3.common.util.BitmapLoader import androidx.media3.common.util.BitmapLoader
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util
import coil3.ImageLoader import coil3.ImageLoader
import coil3.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.SuccessResult import coil3.request.SuccessResult
@@ -20,6 +20,7 @@ import com.subtracks.data.repo.SourceRepository
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
private const val ARTWORK_SIZE = 512 private const val ARTWORK_SIZE = 512
@@ -46,7 +47,11 @@ class CoverArtBitmapLoader(
) : BitmapLoader { ) : BitmapLoader {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun supportsMimeType(mimeType: String): Boolean = mimeType.startsWith("image/") override fun supportsMimeType(mimeType: String): Boolean = Util.isBitmapFactorySupportedMimeType(mimeType)
fun shutdown() {
scope.cancel()
}
override fun loadBitmap(uri: Uri): ListenableFuture<Bitmap> { override fun loadBitmap(uri: Uri): ListenableFuture<Bitmap> {
val coverArtId = val coverArtId =
@@ -80,19 +85,20 @@ class CoverArtBitmapLoader(
return future return future
} }
override fun loadBitmapFromMetadata(metadata: MediaMetadata): ListenableFuture<Bitmap> { override fun decodeBitmap(data: ByteArray): ListenableFuture<Bitmap> {
metadata.artworkData?.let { return decodeBitmap(it) } val future = SettableFuture.create<Bitmap>()
metadata.artworkUri?.let { return loadBitmap(it) } scope.launch {
return Futures.immediateFailedFuture(IllegalArgumentException("Artwork has no data or URI"))
}
override fun decodeBitmap(data: ByteArray): ListenableFuture<Bitmap> =
try { try {
BitmapFactory val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
.decodeByteArray(data, 0, data.size) if (bitmap == null) {
?.let { Futures.immediateFuture(it) } future.setException(IllegalArgumentException("Could not decode artwork"))
?: Futures.immediateFailedFuture(IllegalArgumentException("Could not decode artwork")) } else {
future.set(bitmap)
}
} catch (e: Exception) { } catch (e: Exception) {
Futures.immediateFailedFuture(e) future.setException(e)
}
}
return future
} }
} }
@@ -132,7 +132,7 @@ class Media3PlayerHandle(
.setTitle(item.title) .setTitle(item.title)
.setArtist(item.artist) .setArtist(item.artist)
.setAlbumTitle(item.album) .setAlbumTitle(item.album)
.setArtworkUri(item.coverArtId?.let(CoverArtArtwork::uri)) .setArtworkUri(item.coverArtId?.takeIf { it.isNotEmpty() }?.let(CoverArtArtwork::uri))
.setIsBrowsable(false) .setIsBrowsable(false)
.setIsPlayable(true) .setIsPlayable(true)
.setExtras( .setExtras(
@@ -21,6 +21,7 @@ import org.koin.core.context.GlobalContext
class PlaybackService : MediaSessionService() { class PlaybackService : MediaSessionService() {
private var session: MediaSession? = null private var session: MediaSession? = null
private var artworkLoader: CoverArtBitmapLoader? = null
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
override fun onCreate() { override fun onCreate() {
@@ -39,6 +40,13 @@ class PlaybackService : MediaSessionService() {
.setMediaSourceFactory( .setMediaSourceFactory(
DefaultMediaSourceFactory(this).setDataSourceFactory(OkHttpDataSource.Factory(streamingClient())), DefaultMediaSourceFactory(this).setDataSourceFactory(OkHttpDataSource.Factory(streamingClient())),
).build() ).build()
val loader =
CoverArtBitmapLoader(
applicationContext,
GlobalContext.get().get<SourceRepository>(),
SingletonImageLoader.get(this),
)
artworkLoader = loader
session = session =
MediaSession MediaSession
.Builder(this, player) .Builder(this, player)
@@ -49,13 +57,8 @@ class PlaybackService : MediaSessionService() {
Intent(this, MainActivity::class.java), Intent(this, MainActivity::class.java),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
), ),
).setBitmapLoader( ).setBitmapLoader(loader)
CoverArtBitmapLoader( .build()
this,
GlobalContext.get().get<SourceRepository>(),
SingletonImageLoader.get(this),
),
).build()
} }
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? = session override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? = session
@@ -85,6 +88,8 @@ class PlaybackService : MediaSessionService() {
} }
override fun onDestroy() { override fun onDestroy() {
artworkLoader?.shutdown()
artworkLoader = null
session?.run { session?.run {
player.release() player.release()
release() release()