package dev.stave.playback import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import androidx.media3.common.util.UnstableApi import androidx.media3.session.BitmapLoader import coil3.ImageLoader import coil3.SingletonImageLoader import coil3.request.ImageRequest import coil3.request.SuccessResult import coil3.request.allowHardware import coil3.size.Size import coil3.toBitmap import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch /** * BitmapLoader that uses Coil to fetch artwork at a defined target * resolution. Replacing the default DataSourceBitmapLoader produces * consistently large bitmaps for Auto's now-playing card and the * system media notification — the default decodes whatever resolution * the URI happens to return, which for local content://albumart can * be a 256-px thumbnail and for UPnP varies wildly by server. * * Target [TARGET_SIZE_PX] is 1024x1024 — the largest size Auto's * MediaPlaybackTemplate will display before downscaling, and a sane * upper bound for memory usage when caching. */ @UnstableApi class CoilBitmapLoader( private val context: Context, ) : BitmapLoader { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val loader: ImageLoader get() = SingletonImageLoader.get(context) override fun supportsMimeType(mimeType: String): Boolean { return mimeType.startsWith("image/") } override fun decodeBitmap(data: ByteArray): ListenableFuture { val future = SettableFuture.create() scope.launch { try { val bmp = BitmapFactory.decodeByteArray(data, 0, data.size) if (bmp != null) future.set(bmp) else future.setException(IllegalArgumentException("Failed to decode bitmap from ${data.size} bytes")) } catch (t: Throwable) { future.setException(t) } } return future } override fun loadBitmap(uri: Uri): ListenableFuture { val future = SettableFuture.create() scope.launch { try { val request = ImageRequest.Builder(context) .data(uri) .size(Size(TARGET_SIZE_PX, TARGET_SIZE_PX)) .allowHardware(false) .build() when (val result = loader.execute(request)) { is SuccessResult -> future.set(result.image.toBitmap()) else -> future.setException(IllegalStateException("Coil failed for $uri")) } } catch (t: Throwable) { future.setException(t) } } return future } companion object { const val TARGET_SIZE_PX = 1024 } }