EARHORNMUSIC UPnP · Android TV · Phones · Android Auto · GNU/Linux
Release 0.5.0

CoilBitmapLoader.kt

86 lines · 3.1 KB

Changed in: 0.5.0

1package dev.stave.playback
2
3import android.content.Context
4import android.graphics.Bitmap
5import android.graphics.BitmapFactory
6import android.net.Uri
7import androidx.media3.common.util.UnstableApi
8import androidx.media3.session.BitmapLoader
9import coil3.ImageLoader
10import coil3.SingletonImageLoader
11import coil3.request.ImageRequest
12import coil3.request.SuccessResult
13import coil3.request.allowHardware
14import coil3.size.Size
15import coil3.toBitmap
16import com.google.common.util.concurrent.Futures
17import com.google.common.util.concurrent.ListenableFuture
18import com.google.common.util.concurrent.SettableFuture
19import kotlinx.coroutines.CoroutineScope
20import kotlinx.coroutines.Dispatchers
21import kotlinx.coroutines.SupervisorJob
22import kotlinx.coroutines.launch
23
24/**
25 * BitmapLoader that uses Coil to fetch artwork at a defined target
26 * resolution. Replacing the default DataSourceBitmapLoader produces
27 * consistently large bitmaps for Auto's now-playing card and the
28 * system media notification — the default decodes whatever resolution
29 * the URI happens to return, which for local content://albumart can
30 * be a 256-px thumbnail and for UPnP varies wildly by server.
31 *
32 * Target [TARGET_SIZE_PX] is 1024x1024 — the largest size Auto's
33 * MediaPlaybackTemplate will display before downscaling, and a sane
34 * upper bound for memory usage when caching.
35 */
36@UnstableApi
37class CoilBitmapLoader(
38 private val context: Context,
39) : BitmapLoader {
40
41 private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
42 private val loader: ImageLoader
43 get() = SingletonImageLoader.get(context)
44
45 override fun supportsMimeType(mimeType: String): Boolean {
46 return mimeType.startsWith("image/")
47 }
48
49 override fun decodeBitmap(data: ByteArray): ListenableFuture<Bitmap> {
50 val future = SettableFuture.create<Bitmap>()
51 scope.launch {
52 try {
53 val bmp = BitmapFactory.decodeByteArray(data, 0, data.size)
54 if (bmp != null) future.set(bmp)
55 else future.setException(IllegalArgumentException("Failed to decode bitmap from ${data.size} bytes"))
56 } catch (t: Throwable) {
57 future.setException(t)
58 }
59 }
60 return future
61 }
62
63 override fun loadBitmap(uri: Uri): ListenableFuture<Bitmap> {
64 val future = SettableFuture.create<Bitmap>()
65 scope.launch {
66 try {
67 val request = ImageRequest.Builder(context)
68 .data(uri)
69 .size(Size(TARGET_SIZE_PX, TARGET_SIZE_PX))
70 .allowHardware(false)
71 .build()
72 when (val result = loader.execute(request)) {
73 is SuccessResult -> future.set(result.image.toBitmap())
74 else -> future.setException(IllegalStateException("Coil failed for $uri"))
75 }
76 } catch (t: Throwable) {
77 future.setException(t)
78 }
79 }
80 return future
81 }
82
83 companion object {
84 const val TARGET_SIZE_PX = 1024
85 }
86}