diff --git a/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt b/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt index a95c8308..41d48374 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt @@ -74,7 +74,7 @@ fun AlbumDetailRoute( album = album, songs = songs, coverArt = viewModel::coverArt, - artwork = rememberArtworkColors(viewModel.coverArt(album?.coverArt, false)), + artwork = rememberArtworkColors(viewModel.coverArt(album?.coverArt, true)), onBack = onBack, onSongClick = viewModel::play, playingSongId = playback.item?.id.takeIf { context?.kind == QueueKind.Album && context.refId == albumId }, diff --git a/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkTheme.kt b/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkTheme.kt index 05b11b2e..ad96a413 100644 --- a/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkTheme.kt +++ b/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkTheme.kt @@ -19,11 +19,15 @@ import coil3.SingletonImageLoader import coil3.compose.LocalPlatformContext import coil3.request.ImageRequest import coil3.request.SuccessResult +import coil3.request.allowHardware import coil3.toBitmap import com.subtracks.data.model.CoverArtRef +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +private const val SEED_ART_SIZE_PX = 128 + data class ArtworkColors( val scheme: ColorScheme, val gradientHigh: Color, @@ -97,16 +101,24 @@ private fun rememberArtworkSeed(ref: CoverArtRef?): State { value = null val art = ref ?: return@produceState value = - withContext(Dispatchers.Default) { - val request = - ImageRequest - .Builder(context) - .data(art.url) - .memoryCacheKey(art.cacheKey) - .diskCacheKey(art.cacheKey) - .build() - val image = (SingletonImageLoader.get(context).execute(request) as? SuccessResult)?.image - image?.let { seedFrom(it.toBitmap()) } + withContext(Dispatchers.IO) { + try { + val request = + ImageRequest + .Builder(context) + .data(art.url) + .memoryCacheKey(art.cacheKey) + .diskCacheKey(art.cacheKey) + .size(SEED_ART_SIZE_PX) + .allowHardware(false) + .build() + val image = (SingletonImageLoader.get(context).execute(request) as? SuccessResult)?.image + image?.let { seedFrom(it.toBitmap()) } + } catch (e: CancellationException) { + throw e + } catch (_: Exception) { + null + } } } } diff --git a/app/src/test/kotlin/com/subtracks/ui/theme/ArtworkExtractionTest.kt b/app/src/test/kotlin/com/subtracks/ui/theme/ArtworkExtractionTest.kt new file mode 100644 index 00000000..78f4a527 --- /dev/null +++ b/app/src/test/kotlin/com/subtracks/ui/theme/ArtworkExtractionTest.kt @@ -0,0 +1,64 @@ +package com.subtracks.ui.theme + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Color +import androidx.activity.ComponentActivity +import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import coil3.ImageLoader +import coil3.SingletonImageLoader +import coil3.annotation.DelicateCoilApi +import coil3.asImage +import coil3.test.FakeImageLoaderEngine +import com.subtracks.data.model.CoverArtRef +import org.junit.After +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.GraphicsMode + +@OptIn(DelicateCoilApi::class) +@RunWith(AndroidJUnit4::class) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +class ArtworkExtractionTest { + @get:Rule + val composeRule = createAndroidComposeRule() + + private val bitmap = + Bitmap.createBitmap(64, 64, Bitmap.Config.ARGB_8888).apply { + eraseColor(Color.rgb(30, 120, 200)) + } + + @Before + fun installImageLoader() { + SingletonImageLoader.reset() + SingletonImageLoader.setUnsafe( + ImageLoader + .Builder(ApplicationProvider.getApplicationContext()) + .components { add(FakeImageLoaderEngine.Builder().intercept("art", bitmap.asImage()).build()) } + .build(), + ) + } + + @After + fun resetImageLoader() { + SingletonImageLoader.reset() + } + + @Test + fun resolvesColorsFromCoverArt() { + var colors: ArtworkColors? = null + composeRule.setContent { + colors = rememberArtworkColors(CoverArtRef(url = "art", cacheKey = "art")) + } + composeRule.waitUntil(timeoutMillis = 5_000) { colors != null } + assertNotNull(colors) + assertTrue(colors!!.scheme.background.luminance() < 0.1f) + } +}