Migrate to coil3

This commit is contained in:
Benoit Marty 2025-03-03 12:30:17 +01:00
parent 491bb1d8fe
commit a70249769d
43 changed files with 148 additions and 117 deletions

View file

@ -9,7 +9,6 @@ package io.element.android.libraries.designsystem.components
import android.graphics.Bitmap
import android.graphics.Typeface
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import android.text.TextPaint
import androidx.annotation.FloatRange
@ -85,9 +84,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isSpecified
import androidx.compose.ui.unit.toOffset
import androidx.compose.ui.unit.toSize
import coil.imageLoader
import coil.request.DefaultRequestOptions
import coil.request.ImageRequest
import coil3.SingletonImageLoader
import coil3.request.ImageRequest
import coil3.request.allowHardware
import coil3.toBitmap
import com.airbnb.android.showkase.annotation.ShowkaseComposable
import com.vanniktech.blurhash.BlurHash
import io.element.android.compound.theme.ElementTheme
@ -328,7 +328,7 @@ fun Modifier.avatarBloom(
ImageRequest.Builder(context)
.data(avatarData)
// Allow cache and default dispatchers
.defaults(DefaultRequestOptions())
.defaults(ImageRequest.Defaults())
// Needed to be able to read pixels from the Bitmap for the hash
.allowHardware(false)
// Reduce size so it loads faster for large avatars
@ -340,9 +340,9 @@ fun Modifier.avatarBloom(
var blurHash by rememberSaveable(avatarData) { mutableStateOf<String?>(null) }
LaunchedEffect(avatarData) {
withContext(Dispatchers.IO) {
val drawable =
context.imageLoader.execute(painterRequest).drawable ?: return@withContext
val bitmap = (drawable as? BitmapDrawable)?.bitmap ?: return@withContext
val bitmap = SingletonImageLoader.get(context)
.execute(painterRequest)
.image?.toBitmap() ?: return@withContext
blurHash = BlurHash.encode(
bitmap = bitmap,
componentX = BloomDefaults.HASH_COMPONENTS,

View file

@ -15,6 +15,8 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -26,10 +28,10 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import coil.compose.SubcomposeAsyncImage
import coil.compose.SubcomposeAsyncImageContent
import coil3.compose.AsyncImage
import coil3.compose.AsyncImagePainter
import coil3.compose.SubcomposeAsyncImage
import coil3.compose.SubcomposeAsyncImageContent
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
@ -89,7 +91,8 @@ private fun ImageAvatar(
contentScale = ContentScale.Crop,
modifier = modifier
) {
when (val state = painter.state) {
val collectedState by painter.state.collectAsState()
when (val state = collectedState) {
is AsyncImagePainter.State.Success -> SubcomposeAsyncImageContent()
is AsyncImagePainter.State.Error -> {
SideEffect {

View file

@ -20,7 +20,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import coil.compose.AsyncImage
import coil3.compose.AsyncImage
@Composable
fun BlurHashAsyncImage(

View file

@ -34,6 +34,7 @@ dependencies {
implementation(projects.libraries.testtags)
implementation(libs.coil.compose)
implementation(libs.coil.gif)
implementation(libs.coil.network.okhttp)
implementation(libs.jsoup)
testImplementation(libs.coroutines.test)

View file

@ -24,8 +24,8 @@ import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.request.ImageRequest
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight

View file

@ -7,9 +7,9 @@
package io.element.android.libraries.matrix.ui.media
import coil.ImageLoader
import coil.fetch.Fetcher
import coil.request.Options
import coil3.ImageLoader
import coil3.fetch.Fetcher
import coil3.request.Options
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.MatrixClient
@ -24,7 +24,6 @@ internal class AvatarDataFetcherFactory(
return CoilMediaFetcher(
mediaLoader = client.mediaLoader,
mediaData = data.toMediaRequestData(),
options = options
)
}
}

View file

@ -7,16 +7,16 @@
package io.element.android.libraries.matrix.ui.media
import coil.decode.DataSource
import coil.decode.ImageSource
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.request.Options
import coil3.decode.DataSource
import coil3.decode.ImageSource
import coil3.fetch.FetchResult
import coil3.fetch.Fetcher
import coil3.fetch.SourceFetchResult
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.media.toFile
import okio.Buffer
import okio.FileSystem
import okio.Path.Companion.toOkioPath
import timber.log.Timber
import java.nio.ByteBuffer
@ -24,7 +24,6 @@ import java.nio.ByteBuffer
internal class CoilMediaFetcher(
private val mediaLoader: MatrixMediaLoader,
private val mediaData: MediaRequestData,
private val options: Options
) : Fetcher {
override suspend fun fetch(): FetchResult? {
if (mediaData.source == null) {
@ -32,8 +31,8 @@ internal class CoilMediaFetcher(
return null
}
return when (mediaData.kind) {
is MediaRequestData.Kind.Content -> fetchContent(mediaData.source, options)
is MediaRequestData.Kind.Thumbnail -> fetchThumbnail(mediaData.source, mediaData.kind, options)
is MediaRequestData.Kind.Content -> fetchContent(mediaData.source)
is MediaRequestData.Kind.Thumbnail -> fetchThumbnail(mediaData.source, mediaData.kind)
is MediaRequestData.Kind.File -> fetchFile(mediaData.source, mediaData.kind)
}
}
@ -47,8 +46,12 @@ internal class CoilMediaFetcher(
return mediaLoader.downloadMediaFile(mediaSource, kind.mimeType, kind.fileName)
.map { mediaFile ->
val file = mediaFile.toFile()
SourceResult(
source = ImageSource(file = file.toOkioPath(), closeable = mediaFile),
SourceFetchResult(
source = ImageSource(
file = file.toOkioPath(),
fileSystem = FileSystem.SYSTEM,
closeable = mediaFile,
),
mimeType = null,
dataSource = DataSource.DISK
)
@ -59,37 +62,40 @@ internal class CoilMediaFetcher(
.getOrNull()
}
private suspend fun fetchContent(mediaSource: MediaSource, options: Options): FetchResult? {
private suspend fun fetchContent(mediaSource: MediaSource): FetchResult? {
return mediaLoader.loadMediaContent(
source = mediaSource,
).map { byteArray ->
byteArray.asSourceResult(options)
byteArray.asSourceResult()
}.onFailure {
Timber.e(it)
}.getOrNull()
}
private suspend fun fetchThumbnail(mediaSource: MediaSource, kind: MediaRequestData.Kind.Thumbnail, options: Options): FetchResult? {
private suspend fun fetchThumbnail(mediaSource: MediaSource, kind: MediaRequestData.Kind.Thumbnail): FetchResult? {
return mediaLoader.loadMediaThumbnail(
source = mediaSource,
width = kind.width,
height = kind.height,
).map { byteArray ->
byteArray.asSourceResult(options)
byteArray.asSourceResult()
}.onFailure {
Timber.e(it)
}.getOrNull()
}
private fun ByteArray.asSourceResult(options: Options): SourceResult {
private fun ByteArray.asSourceResult(): SourceFetchResult {
val byteBuffer = ByteBuffer.wrap(this)
val bufferedSource = try {
Buffer().apply { write(byteBuffer) }
} finally {
byteBuffer.position(0)
}
return SourceResult(
source = ImageSource(bufferedSource, options.context),
return SourceFetchResult(
source = ImageSource(
source = bufferedSource,
fileSystem = FileSystem.SYSTEM,
),
mimeType = null,
dataSource = DataSource.MEMORY
)

View file

@ -9,10 +9,10 @@ package io.element.android.libraries.matrix.ui.media
import android.content.Context
import android.os.Build
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil3.ImageLoader
import coil3.gif.AnimatedImageDecoder
import coil3.gif.GifDecoder
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
@ -33,11 +33,17 @@ class DefaultLoggedInImageLoaderFactory @Inject constructor(
override fun newImageLoader(matrixClient: MatrixClient): ImageLoader {
return ImageLoader
.Builder(context)
.okHttpClient { okHttpClient.get() }
.components {
add(
OkHttpNetworkFetcherFactory(
callFactory = {
okHttpClient.get()
}
)
)
// Add gif support
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
add(ImageDecoderDecoder.Factory())
add(AnimatedImageDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
@ -53,11 +59,19 @@ class DefaultLoggedInImageLoaderFactory @Inject constructor(
class NotLoggedInImageLoaderFactory @Inject constructor(
@ApplicationContext private val context: Context,
private val okHttpClient: Provider<OkHttpClient>,
) : ImageLoaderFactory {
override fun newImageLoader(): ImageLoader {
) {
fun newImageLoader(): ImageLoader {
return ImageLoader
.Builder(context)
.okHttpClient { okHttpClient.get() }
.components {
add(
OkHttpNetworkFetcherFactory(
callFactory = {
okHttpClient.get()
}
)
)
}
.build()
}
}

View file

@ -7,7 +7,7 @@
package io.element.android.libraries.matrix.ui.media
import coil.ImageLoader
import coil3.ImageLoader
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn

View file

@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.ui.media
import io.element.android.libraries.matrix.api.media.MediaSource
/**
* Can be use with [coil.compose.AsyncImage] to load a [MediaSource].
* Can be use with [coil3.compose.AsyncImage] to load a [MediaSource].
* This will go internally through our [CoilMediaFetcher].
*
* Example of usage:

View file

@ -7,9 +7,9 @@
package io.element.android.libraries.matrix.ui.media
import coil.ImageLoader
import coil.fetch.Fetcher
import coil.request.Options
import coil3.ImageLoader
import coil3.fetch.Fetcher
import coil3.request.Options
import io.element.android.libraries.matrix.api.MatrixClient
internal class MediaRequestDataFetcherFactory(
@ -23,7 +23,6 @@ internal class MediaRequestDataFetcherFactory(
return CoilMediaFetcher(
mediaLoader = client.mediaLoader,
mediaData = data,
options = options
)
}
}

View file

@ -7,8 +7,8 @@
package io.element.android.libraries.matrix.ui.media
import coil.key.Keyer
import coil.request.Options
import coil3.key.Keyer
import coil3.request.Options
import io.element.android.libraries.designsystem.components.avatar.AvatarData
internal class AvatarDataKeyer : Keyer<AvatarData> {

View file

@ -8,7 +8,7 @@
package io.element.android.libraries.matrix.ui.media
import androidx.test.platform.app.InstrumentationRegistry
import coil.ImageLoader
import coil3.ImageLoader
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.test.A_SESSION_ID

View file

@ -7,7 +7,7 @@
package io.element.android.libraries.matrix.ui.media
import coil.ImageLoader
import coil3.ImageLoader
import io.element.android.libraries.matrix.api.MatrixClient
class FakeLoggedInImageLoaderFactory(

View file

@ -25,7 +25,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil3.compose.AsyncImage
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.components.BigIcon

View file

@ -23,8 +23,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalInspectionMode
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import coil3.compose.AsyncImage
import coil3.compose.AsyncImagePainter
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight

View file

@ -30,8 +30,8 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import coil3.compose.AsyncImage
import coil3.compose.AsyncImagePainter
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.preview.ElementPreview

View file

@ -46,7 +46,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil3.compose.AsyncImage
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.architecture.AsyncData

View file

@ -9,7 +9,7 @@ package io.element.android.libraries.push.api.notifications
import android.graphics.Bitmap
import androidx.core.graphics.drawable.IconCompat
import coil.ImageLoader
import coil3.ImageLoader
interface NotificationBitmapLoader {
/**

View file

@ -11,10 +11,11 @@ import android.content.Context
import android.graphics.Bitmap
import android.os.Build
import androidx.core.graphics.drawable.IconCompat
import androidx.core.graphics.drawable.toBitmap
import coil.ImageLoader
import coil.request.ImageRequest
import coil.transform.CircleCropTransformation
import coil3.ImageLoader
import coil3.request.ImageRequest
import coil3.request.transformations
import coil3.toBitmap
import coil3.transform.CircleCropTransformation
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
@ -50,7 +51,7 @@ class DefaultNotificationBitmapLoader @Inject constructor(
.transformations(CircleCropTransformation())
.build()
val result = imageLoader.execute(imageRequest)
result.drawable?.toBitmap()
result.image?.toBitmap()
} catch (e: Throwable) {
Timber.e(e, "Unable to load room bitmap")
null
@ -78,7 +79,7 @@ class DefaultNotificationBitmapLoader @Inject constructor(
.transformations(CircleCropTransformation())
.build()
val result = imageLoader.execute(imageRequest)
val bitmap = result.drawable?.toBitmap()
val bitmap = result.image?.toBitmap()
return bitmap?.let { IconCompat.createWithBitmap(it) }
} catch (e: Throwable) {
Timber.e(e, "Unable to load user bitmap")

View file

@ -12,7 +12,7 @@ import android.graphics.Typeface
import android.text.style.StyleSpan
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import coil.ImageLoader
import coil3.ImageLoader
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.RoomId

View file

@ -7,7 +7,7 @@
package io.element.android.libraries.push.impl.notifications
import coil.ImageLoader
import coil3.ImageLoader
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.api.notifications.NotificationIdProvider

View file

@ -9,7 +9,7 @@ package io.element.android.libraries.push.impl.notifications
import android.app.Notification
import android.graphics.Bitmap
import coil.ImageLoader
import coil3.ImageLoader
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.RoomId

View file

@ -16,7 +16,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.MessagingStyle
import androidx.core.app.Person
import androidx.core.content.res.ResourcesCompat
import coil.ImageLoader
import coil3.ImageLoader
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.NotificationConfig
import io.element.android.libraries.core.meta.BuildMeta

View file

@ -9,7 +9,7 @@ package io.element.android.libraries.push.impl.notifications.fake
import android.app.Notification
import android.graphics.Bitmap
import coil.ImageLoader
import coil3.ImageLoader
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo

View file

@ -7,7 +7,7 @@
package io.element.android.libraries.push.impl.notifications.fake
import coil.ImageLoader
import coil3.ImageLoader
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.NotificationDataFactory
import io.element.android.libraries.push.impl.notifications.OneShotNotification

View file

@ -8,7 +8,7 @@
package io.element.android.libraries.push.impl.notifications.fake
import android.app.Notification
import coil.ImageLoader
import coil3.ImageLoader
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.RoomGroupMessageCreator

View file

@ -9,12 +9,11 @@ package io.element.android.libraries.push.test.notifications
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import coil.ImageLoader
import coil.annotation.ExperimentalCoilApi
import coil.test.FakeImageLoaderEngine
import coil3.ImageLoader
import coil3.test.FakeImageLoaderEngine
import coil3.test.intercept
import org.robolectric.RuntimeEnvironment
@OptIn(ExperimentalCoilApi::class)
class FakeImageLoader {
private val coilRequests = mutableListOf<Any>()

View file

@ -7,7 +7,7 @@
package io.element.android.libraries.push.test.notifications
import coil.ImageLoader
import coil3.ImageLoader
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder

View file

@ -9,7 +9,7 @@ package io.element.android.libraries.push.test.notifications.push
import android.graphics.Bitmap
import androidx.core.graphics.drawable.IconCompat
import coil.ImageLoader
import coil3.ImageLoader
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
class FakeNotificationBitmapLoader(