diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 266c7904be..dc5d5b7a21 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { implementation(projects.libraries.pushproviders.api) implementation(projects.libraries.designsystem) implementation(projects.libraries.matrixui) + implementation(projects.libraries.matrixmedia.api) implementation(projects.libraries.uiCommon) implementation(projects.libraries.uiStrings) implementation(projects.features.login.api) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt index 4e3412afc8..2544abbfb3 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt @@ -33,7 +33,7 @@ import io.element.android.libraries.architecture.callback import io.element.android.libraries.architecture.inputs import io.element.android.libraries.designsystem.utils.ForceOrientationInMobileDevices import io.element.android.libraries.designsystem.utils.ScreenOrientation -import io.element.android.libraries.matrix.ui.media.NotLoggedInImageLoaderFactory +import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder import kotlinx.parcelize.Parcelize @ContributesNode(AppScope::class) @@ -42,7 +42,7 @@ class NotLoggedInFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val loginEntryPoint: LoginEntryPoint, - private val notLoggedInImageLoaderFactory: NotLoggedInImageLoaderFactory, + private val imageLoaderHolder: ImageLoaderHolder, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.Root, @@ -66,7 +66,7 @@ class NotLoggedInFlowNode( super.onBuilt() lifecycle.subscribe( onResume = { - SingletonImageLoader.setUnsafe(notLoggedInImageLoaderFactory.newImageLoader()) + SingletonImageLoader.setUnsafe(imageLoaderHolder.get()) }, ) } diff --git a/features/call/impl/build.gradle.kts b/features/call/impl/build.gradle.kts index c3bf2fb6dc..48c6fb5cc8 100644 --- a/features/call/impl/build.gradle.kts +++ b/features/call/impl/build.gradle.kts @@ -74,7 +74,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.featureflag.api) implementation(projects.libraries.matrix.impl) - implementation(projects.libraries.matrixui) + implementation(projects.libraries.matrixmedia.api) implementation(projects.libraries.network) implementation(projects.libraries.preferences.api) implementation(projects.libraries.push.api) @@ -94,7 +94,7 @@ dependencies { testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.libraries.matrixuiTest) + testImplementation(projects.libraries.matrixmedia.test) testImplementation(projects.libraries.push.test) testImplementation(projects.services.analytics.test) testImplementation(projects.services.appnavstate.test) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt index c185ebacd0..d1575c7a97 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt @@ -22,6 +22,8 @@ import io.element.android.features.call.api.CallType import io.element.android.features.call.impl.receivers.DeclineCallBroadcastReceiver import io.element.android.features.call.impl.ui.IncomingCallActivity import io.element.android.features.call.impl.utils.IntentProvider +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.matrix.api.MatrixClientProvider @@ -70,7 +72,15 @@ class RingingCallNotificationCreator( ): Notification? { val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null val imageLoader = imageLoaderHolder.get(matrixClient) - val largeIcon = notificationBitmapLoader.getUserIcon(roomAvatarUrl, imageLoader) + val largeIcon = notificationBitmapLoader.getUserIcon( + avatarData = AvatarData( + id = roomId.value, + name = roomName, + url = roomAvatarUrl, + size = AvatarSize.RoomDetailsHeader, + ), + imageLoader = imageLoader, + ) val caller = Person.Builder() .setName(senderDisplayName) diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt index 4d9b493feb..5bf5d6bacb 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt @@ -13,6 +13,7 @@ import androidx.test.platform.app.InstrumentationRegistry import coil3.ImageLoader import com.google.common.truth.Truth.assertThat import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator +import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID @@ -53,7 +54,7 @@ class RingingCallNotificationCreatorTest { @Test fun `createNotification - tries to load the avatar URL`() = runTest { - val getUserIconLambda = lambdaRecorder { _, _ -> null } + val getUserIconLambda = lambdaRecorder { _, _ -> null } val notificationCreator = createRingingCallNotificationCreator( matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(FakeMatrixClient()) }), notificationBitmapLoader = FakeNotificationBitmapLoader(getUserIconResult = getUserIconLambda) diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index ea7ad34b08..f240df0ef4 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) + implementation(projects.libraries.matrixmedia.api) implementation(projects.libraries.designsystem) implementation(projects.libraries.textcomposer.impl) implementation(projects.libraries.uiStrings) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/ImageAvatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/ImageAvatar.kt index 3ccc20cfdd..ebd81f61b1 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/ImageAvatar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/ImageAvatar.kt @@ -46,7 +46,7 @@ internal fun ImageAvatar( is AsyncImagePainter.State.Success -> SubcomposeAsyncImageContent() is AsyncImagePainter.State.Error -> { SideEffect { - Timber.Forest.e( + Timber.e( state.result.throwable, "Error loading avatar $state\n${state.result}" ) diff --git a/libraries/matrixui-test/build.gradle.kts b/libraries/matrixmedia/api/build.gradle.kts similarity index 77% rename from libraries/matrixui-test/build.gradle.kts rename to libraries/matrixmedia/api/build.gradle.kts index f2ea3c459a..90c7ee635e 100644 --- a/libraries/matrixui-test/build.gradle.kts +++ b/libraries/matrixmedia/api/build.gradle.kts @@ -11,11 +11,11 @@ plugins { } android { - namespace = "io.element.android.libraries.matrix.ui.test" + namespace = "io.element.android.libraries.matrix.ui.media.api" } dependencies { + implementation(projects.libraries.designsystem) implementation(projects.libraries.matrix.api) - implementation(projects.libraries.matrixui) implementation(libs.coil.compose) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatatarDataExt.kt b/libraries/matrixmedia/api/src/main/kotlin/io/element/android/libraries/matrix/ui/media/Avatar.kt similarity index 66% rename from libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatatarDataExt.kt rename to libraries/matrixmedia/api/src/main/kotlin/io/element/android/libraries/matrix/ui/media/Avatar.kt index aa8192faed..4d4dd402d6 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatatarDataExt.kt +++ b/libraries/matrixmedia/api/src/main/kotlin/io/element/android/libraries/matrix/ui/media/Avatar.kt @@ -8,9 +8,6 @@ package io.element.android.libraries.matrix.ui.media -import io.element.android.libraries.designsystem.components.avatar.AvatarData -import io.element.android.libraries.matrix.api.media.MediaSource - /** * The size in pixel of the thumbnail to generate for the avatar. * This is not the size of the avatar displayed in the UI but the size to get from the servers. @@ -25,10 +22,3 @@ import io.element.android.libraries.matrix.api.media.MediaSource * Let's always use the same size so coil caching works properly. */ const val AVATAR_THUMBNAIL_SIZE_IN_PIXEL = 240L - -internal fun AvatarData.toMediaRequestData(): MediaRequestData { - return MediaRequestData( - source = url?.let { MediaSource(it) }, - kind = MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL) - ) -} diff --git a/libraries/matrixmedia/api/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderHolder.kt b/libraries/matrixmedia/api/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderHolder.kt new file mode 100644 index 0000000000..62b169744b --- /dev/null +++ b/libraries/matrixmedia/api/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderHolder.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.ui.media + +import coil3.ImageLoader +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.SessionId + +interface ImageLoaderHolder { + fun get(): ImageLoader + fun get(client: MatrixClient): ImageLoader + fun remove(sessionId: SessionId) +} diff --git a/libraries/matrixmedia/api/src/main/kotlin/io/element/android/libraries/matrix/ui/media/InitialsAvatarBitmapGenerator.kt b/libraries/matrixmedia/api/src/main/kotlin/io/element/android/libraries/matrix/ui/media/InitialsAvatarBitmapGenerator.kt new file mode 100644 index 0000000000..e6ff5d762d --- /dev/null +++ b/libraries/matrixmedia/api/src/main/kotlin/io/element/android/libraries/matrix/ui/media/InitialsAvatarBitmapGenerator.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.ui.media + +import android.graphics.Bitmap +import io.element.android.libraries.designsystem.components.avatar.AvatarData + +/** + * Generates a bitmap for an initials avatar based on the provided [io.element.android.libraries.designsystem.components.avatar.AvatarData]. + */ +interface InitialsAvatarBitmapGenerator { + fun generateBitmap( + size: Int, + avatarData: AvatarData, + useDarkTheme: Boolean, + fontSizePercentage: Float = 0.5f, + ): Bitmap? +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt b/libraries/matrixmedia/api/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt similarity index 95% rename from libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt rename to libraries/matrixmedia/api/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt index 462711cc9c..47841a573b 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt +++ b/libraries/matrixmedia/api/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt @@ -12,7 +12,7 @@ import io.element.android.libraries.matrix.api.media.MediaSource /** * Can be use with [coil3.compose.AsyncImage] to load a [MediaSource]. - * This will go internally through our [CoilMediaFetcher]. + * This will go internally through our CoilMediaFetcher. * * Example of usage: * AsyncImage( diff --git a/libraries/matrixmedia/impl/build.gradle.kts b/libraries/matrixmedia/impl/build.gradle.kts new file mode 100644 index 0000000000..82afc2f62c --- /dev/null +++ b/libraries/matrixmedia/impl/build.gradle.kts @@ -0,0 +1,33 @@ +import extension.setupDependencyInjection +import extension.testCommonDependencies + +/* + * Copyright (c) 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.libraries.matrix.ui.media.impl" +} + +setupDependencyInjection() + +dependencies { + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixmedia.api) + implementation(projects.libraries.designsystem) + implementation(libs.coil.compose) + implementation(libs.coil.gif) + implementation(libs.coil.network.okhttp) + + testCommonDependencies(libs, true) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.dateformatter.test) + testImplementation(projects.libraries.sessionStorage.test) +} diff --git a/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatarDataExt.kt b/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatarDataExt.kt new file mode 100644 index 0000000000..0b1a09bdf9 --- /dev/null +++ b/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatarDataExt.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.ui.media + +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.matrix.api.media.MediaSource + +internal fun AvatarData.toMediaRequestData(): MediaRequestData { + return MediaRequestData( + source = url?.let { MediaSource(it) }, + kind = MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL) + ) +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatarDataFetcherFactory.kt b/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatarDataFetcherFactory.kt similarity index 100% rename from libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatarDataFetcherFactory.kt rename to libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatarDataFetcherFactory.kt diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt b/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt similarity index 92% rename from libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt rename to libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt index bc23b0c525..06321f64a9 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt +++ b/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt @@ -27,14 +27,15 @@ internal class CoilMediaFetcher( private val mediaData: MediaRequestData, ) : Fetcher { override suspend fun fetch(): FetchResult? { - if (mediaData.source == null) { + val source = mediaData.source + if (source == null) { Timber.e("MediaData source is null") return null } - return when (mediaData.kind) { - 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) + return when (val kind = mediaData.kind) { + is MediaRequestData.Kind.Content -> fetchContent(source) + is MediaRequestData.Kind.Thumbnail -> fetchThumbnail(source, kind) + is MediaRequestData.Kind.File -> fetchFile(source, kind) } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderHolder.kt b/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/DefaultImageLoaderHolder.kt similarity index 85% rename from libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderHolder.kt rename to libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/DefaultImageLoaderHolder.kt index a0db54410b..a0957a3b13 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderHolder.kt +++ b/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/DefaultImageLoaderHolder.kt @@ -17,18 +17,16 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.sessionstorage.api.observer.SessionListener import io.element.android.libraries.sessionstorage.api.observer.SessionObserver -interface ImageLoaderHolder { - fun get(client: MatrixClient): ImageLoader - fun remove(sessionId: SessionId) -} - @ContributesBinding(AppScope::class) @SingleIn(AppScope::class) class DefaultImageLoaderHolder( - private val loggedInImageLoaderFactory: LoggedInImageLoaderFactory, + private val imageLoaderFactory: ImageLoaderFactory, private val sessionObserver: SessionObserver, ) : ImageLoaderHolder { private val map = mutableMapOf() + private val notLoggedInImageLoader by lazy { + imageLoaderFactory.newImageLoader() + } init { observeSessions() @@ -42,10 +40,14 @@ class DefaultImageLoaderHolder( }) } + override fun get(): ImageLoader { + return notLoggedInImageLoader + } + override fun get(client: MatrixClient): ImageLoader { return synchronized(map) { map.getOrPut(client.sessionId) { - loggedInImageLoaderFactory + imageLoaderFactory .newImageLoader(client.matrixMediaLoader) } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/InitialsAvatarBitmapGenerator.kt b/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/DefaultInitialsAvatarBitmapGenerator.kt similarity index 68% rename from libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/InitialsAvatarBitmapGenerator.kt rename to libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/DefaultInitialsAvatarBitmapGenerator.kt index 4ab0b0d78a..71c894f305 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/InitialsAvatarBitmapGenerator.kt +++ b/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/DefaultInitialsAvatarBitmapGenerator.kt @@ -24,6 +24,8 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.unit.dp import androidx.core.graphics.createBitmap import coil3.Bitmap +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding import io.element.android.compound.theme.AvatarColors import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.SemanticColors @@ -35,60 +37,35 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Text -/** - * Generates a bitmap for an initials avatar based on the provided [AvatarData]. - */ -class InitialsAvatarBitmapGenerator( - useDarkTheme: Boolean = false, - private val fontSizePercentage: Float = 0.5f, -) { - private val compoundColors: SemanticColors = if (useDarkTheme) { - compoundColorsDark - } else { - compoundColorsLight - } - +@ContributesBinding(AppScope::class) +class DefaultInitialsAvatarBitmapGenerator : InitialsAvatarBitmapGenerator { // List of predefined avatar colors to use for initials avatars, in light mode - private val allAvatarColors: List = listOf( - AvatarColors( - background = compoundColors.bgDecorative1, - foreground = compoundColors.textDecorative1, - ), - AvatarColors( - background = compoundColors.bgDecorative2, - foreground = compoundColors.textDecorative2, - ), - AvatarColors( - background = compoundColors.bgDecorative3, - foreground = compoundColors.textDecorative3, - ), - AvatarColors( - background = compoundColors.bgDecorative4, - foreground = compoundColors.textDecorative4, - ), - AvatarColors( - background = compoundColors.bgDecorative5, - foreground = compoundColors.textDecorative5, - ), - AvatarColors( - background = compoundColors.bgDecorative6, - foreground = compoundColors.textDecorative6, - ), - ) + private val lightAvatarColors: List = compoundColorsLight.buildAvatarColors() + + // List of predefined avatar colors to use for initials avatars, in dark mode + private val darkAvatarColors: List = compoundColorsDark.buildAvatarColors() /** * Generates a bitmap for an avatar with no URL, using the initials from the [AvatarData]. * @param size The size of the bitmap to generate, in pixels. * @param avatarData The [AvatarData] containing the initials and other information. + * @param useDarkTheme Whether the theme is dark. + * @param fontSizePercentage The percentage of the avatar size to use for the font size. */ - fun generateBitmap(size: Int, avatarData: AvatarData): Bitmap? { + override fun generateBitmap( + size: Int, + avatarData: AvatarData, + useDarkTheme: Boolean, + fontSizePercentage: Float, + ): Bitmap? { if (avatarData.url != null) { // This generator is only for initials avatars, not for avatars with URLs return null } // Get the color pair to use for the initials avatar - val avatarColors = allAvatarColors[avatarData.id.sumOf { it.code } % allAvatarColors.size] + val colors = if (useDarkTheme) darkAvatarColors else lightAvatarColors + val avatarColors = colors[avatarData.id.sumOf { it.code } % colors.size] val bitmap = createBitmap(size, size) Canvas(bitmap).run { @@ -116,20 +93,32 @@ class InitialsAvatarBitmapGenerator( } } +private fun SemanticColors.buildAvatarColors(): List = listOf( + AvatarColors(background = bgDecorative1, foreground = textDecorative1), + AvatarColors(background = bgDecorative2, foreground = textDecorative2), + AvatarColors(background = bgDecorative3, foreground = textDecorative3), + AvatarColors(background = bgDecorative4, foreground = textDecorative4), + AvatarColors(background = bgDecorative5, foreground = textDecorative5), + AvatarColors(background = bgDecorative6, foreground = textDecorative6), +) + @Composable @PreviewsDayNight internal fun InitialsAvatarBitmapGeneratorPreview() = ElementPreview { Column( verticalArrangement = Arrangement.spacedBy(4.dp) ) { + val generator = remember { DefaultInitialsAvatarBitmapGenerator() } repeat(6) { index -> val avatarData = remember { AvatarData(id = index.toString(), name = Char('0'.code + index).toString(), size = AvatarSize.IncomingCall) } val isLightTheme = ElementTheme.isLightTheme val bitmap = remember(isLightTheme) { - val generator = InitialsAvatarBitmapGenerator(useDarkTheme = !isLightTheme) - generator.generateBitmap(512, avatarData)?.asImageBitmap() + generator.generateBitmap( + size = 512, + avatarData = avatarData, + useDarkTheme = !isLightTheme, + )?.asImageBitmap() } - bitmap?.let { Image(bitmap = it, contentDescription = null, modifier = Modifier.size(48.dp)) } ?: Text("No avatar generated") diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt b/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt similarity index 62% rename from libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt rename to libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt index db51b3453e..e67e630e97 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt +++ b/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt @@ -16,32 +16,40 @@ import coil3.gif.GifDecoder import coil3.network.okhttp.OkHttpNetworkFetcherFactory import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding -import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Provider import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import okhttp3.OkHttpClient -interface LoggedInImageLoaderFactory { +interface ImageLoaderFactory { + fun newImageLoader(): ImageLoader fun newImageLoader(matrixMediaLoader: MatrixMediaLoader): ImageLoader } @ContributesBinding(AppScope::class) -class DefaultLoggedInImageLoaderFactory( +class DefaultImageLoaderFactory( @ApplicationContext private val context: Context, private val okHttpClient: Provider, -) : LoggedInImageLoaderFactory { +) : ImageLoaderFactory { + private val okHttpNetworkFetcherFactory = OkHttpNetworkFetcherFactory( + callFactory = { + // Use newBuilder, see https://coil-kt.github.io/coil/network/#using-a-custom-okhttpclient + okHttpClient().newBuilder().build() + } + ) + + override fun newImageLoader(): ImageLoader { + return ImageLoader.Builder(context) + .components { + add(okHttpNetworkFetcherFactory) + } + .build() + } + override fun newImageLoader(matrixMediaLoader: MatrixMediaLoader): ImageLoader { return ImageLoader.Builder(context) .components { - add( - OkHttpNetworkFetcherFactory( - callFactory = { - // Use newBuilder, see https://coil-kt.github.io/coil/network/#using-a-custom-okhttpclient - okHttpClient().newBuilder().build() - } - ) - ) + add(okHttpNetworkFetcherFactory) // Add gif support if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { add(AnimatedImageDecoder.Factory()) @@ -56,24 +64,3 @@ class DefaultLoggedInImageLoaderFactory( .build() } } - -@Inject -class NotLoggedInImageLoaderFactory( - @ApplicationContext private val context: Context, - private val okHttpClient: Provider, -) { - fun newImageLoader(): ImageLoader { - return ImageLoader.Builder(context) - .components { - add( - OkHttpNetworkFetcherFactory( - callFactory = { - // Use newBuilder, see https://coil-kt.github.io/coil/network/#using-a-custom-okhttpclient - okHttpClient().newBuilder().build() - } - ) - ) - } - .build() - } -} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataFetcherFactory.kt b/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataFetcherFactory.kt similarity index 100% rename from libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataFetcherFactory.kt rename to libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataFetcherFactory.kt diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataKeyer.kt b/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataKeyer.kt similarity index 92% rename from libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataKeyer.kt rename to libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataKeyer.kt index 2ebed2c126..488803e933 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataKeyer.kt +++ b/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataKeyer.kt @@ -25,6 +25,5 @@ internal class MediaRequestDataKeyer : Keyer { } private fun MediaRequestData.toKey(): String? { - if (source == null) return null - return "${source.url}_$kind" + return source?.let { "${it.url}_$kind" } } diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/media/DefaultImageLoaderHolderTest.kt b/libraries/matrixmedia/impl/src/test/kotlin/io/element/android/libraries/matrix/ui/media/DefaultImageLoaderHolderTest.kt similarity index 76% rename from libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/media/DefaultImageLoaderHolderTest.kt rename to libraries/matrixmedia/impl/src/test/kotlin/io/element/android/libraries/matrix/ui/media/DefaultImageLoaderHolderTest.kt index e6a14bf56e..dd9127a4ae 100644 --- a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/media/DefaultImageLoaderHolderTest.kt +++ b/libraries/matrixmedia/impl/src/test/kotlin/io/element/android/libraries/matrix/ui/media/DefaultImageLoaderHolderTest.kt @@ -14,6 +14,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.sessionstorage.api.observer.SessionObserver import io.element.android.libraries.sessionstorage.test.observer.FakeSessionObserver import io.element.android.libraries.sessionstorage.test.observer.NoOpSessionObserver import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -30,9 +31,10 @@ class DefaultImageLoaderHolderTest { val context = InstrumentationRegistry.getInstrumentation().context val lambda = lambdaRecorder { ImageLoader.Builder(context).build() } - val holder = DefaultImageLoaderHolder( - loggedInImageLoaderFactory = FakeLoggedInImageLoaderFactory(lambda), - sessionObserver = NoOpSessionObserver() + val holder = createDefaultImageLoaderHolder( + imageLoaderFactory = FakeImageLoaderFactory( + newMatrixImageLoaderLambda = lambda, + ), ) val client = FakeMatrixClient() val imageLoader1 = holder.get(client) @@ -50,8 +52,10 @@ class DefaultImageLoaderHolderTest { lambdaRecorder { ImageLoader.Builder(context).build() } val sessionObserver = FakeSessionObserver() val holder = DefaultImageLoaderHolder( - loggedInImageLoaderFactory = FakeLoggedInImageLoaderFactory(lambda), - sessionObserver = sessionObserver + imageLoaderFactory = FakeImageLoaderFactory( + newMatrixImageLoaderLambda = lambda, + ), + sessionObserver = sessionObserver, ) assertThat(sessionObserver.listeners.size).isEqualTo(1) val client = FakeMatrixClient() @@ -69,10 +73,20 @@ class DefaultImageLoaderHolderTest { lambdaRecorder { ImageLoader.Builder(context).build() } val sessionObserver = FakeSessionObserver() DefaultImageLoaderHolder( - loggedInImageLoaderFactory = FakeLoggedInImageLoaderFactory(lambda), - sessionObserver = sessionObserver + imageLoaderFactory = FakeImageLoaderFactory( + newMatrixImageLoaderLambda = lambda, + ), + sessionObserver = sessionObserver, ) assertThat(sessionObserver.listeners.size).isEqualTo(1) sessionObserver.onSessionCreated(A_SESSION_ID.value) } } + +private fun createDefaultImageLoaderHolder( + imageLoaderFactory: ImageLoaderFactory = FakeImageLoaderFactory(), + sessionObserver: SessionObserver = NoOpSessionObserver(), +) = DefaultImageLoaderHolder( + imageLoaderFactory = imageLoaderFactory, + sessionObserver = sessionObserver, +) diff --git a/libraries/matrixmedia/impl/src/test/kotlin/io/element/android/libraries/matrix/ui/media/FakeImageLoaderFactory.kt b/libraries/matrixmedia/impl/src/test/kotlin/io/element/android/libraries/matrix/ui/media/FakeImageLoaderFactory.kt new file mode 100644 index 0000000000..bcafad0737 --- /dev/null +++ b/libraries/matrixmedia/impl/src/test/kotlin/io/element/android/libraries/matrix/ui/media/FakeImageLoaderFactory.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.ui.media + +import coil3.ImageLoader +import io.element.android.libraries.matrix.api.media.MatrixMediaLoader +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeImageLoaderFactory( + private val newImageLoaderLambda: () -> ImageLoader = { lambdaError() }, + private val newMatrixImageLoaderLambda: (MatrixMediaLoader) -> ImageLoader = { lambdaError() }, +) : ImageLoaderFactory { + override fun newImageLoader(): ImageLoader { + return newImageLoaderLambda() + } + + override fun newImageLoader(matrixMediaLoader: MatrixMediaLoader): ImageLoader { + return newMatrixImageLoaderLambda(matrixMediaLoader) + } +} diff --git a/libraries/matrixmedia/test/build.gradle.kts b/libraries/matrixmedia/test/build.gradle.kts new file mode 100644 index 0000000000..5b8e966d08 --- /dev/null +++ b/libraries/matrixmedia/test/build.gradle.kts @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.matrix.ui.media.test" +} + +dependencies { + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixmedia.api) + implementation(projects.libraries.designsystem) + implementation(projects.tests.testutils) + implementation(libs.coil.compose) +} diff --git a/libraries/matrixui-test/src/main/kotlin/io/element/android/libraries/matrix/ui/test/media/FakeImageLoader.kt b/libraries/matrixmedia/test/src/main/kotlin/io/element/android/libraries/matrix/ui/test/media/FakeImageLoader.kt similarity index 100% rename from libraries/matrixui-test/src/main/kotlin/io/element/android/libraries/matrix/ui/test/media/FakeImageLoader.kt rename to libraries/matrixmedia/test/src/main/kotlin/io/element/android/libraries/matrix/ui/test/media/FakeImageLoader.kt diff --git a/libraries/matrixui-test/src/main/kotlin/io/element/android/libraries/matrix/ui/test/media/FakeImageLoaderHolder.kt b/libraries/matrixmedia/test/src/main/kotlin/io/element/android/libraries/matrix/ui/test/media/FakeImageLoaderHolder.kt similarity index 91% rename from libraries/matrixui-test/src/main/kotlin/io/element/android/libraries/matrix/ui/test/media/FakeImageLoaderHolder.kt rename to libraries/matrixmedia/test/src/main/kotlin/io/element/android/libraries/matrix/ui/test/media/FakeImageLoaderHolder.kt index 5c6804660d..e3298d3567 100644 --- a/libraries/matrixui-test/src/main/kotlin/io/element/android/libraries/matrix/ui/test/media/FakeImageLoaderHolder.kt +++ b/libraries/matrixmedia/test/src/main/kotlin/io/element/android/libraries/matrix/ui/test/media/FakeImageLoaderHolder.kt @@ -16,6 +16,10 @@ import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder class FakeImageLoaderHolder( val fakeImageLoader: ImageLoader = FakeImageLoader(), ) : ImageLoaderHolder { + override fun get(): ImageLoader { + return fakeImageLoader + } + override fun get(client: MatrixClient): ImageLoader { return fakeImageLoader } diff --git a/libraries/matrixmedia/test/src/main/kotlin/io/element/android/libraries/matrix/ui/test/media/FakeInitialsAvatarBitmapGenerator.kt b/libraries/matrixmedia/test/src/main/kotlin/io/element/android/libraries/matrix/ui/test/media/FakeInitialsAvatarBitmapGenerator.kt new file mode 100644 index 0000000000..6671561776 --- /dev/null +++ b/libraries/matrixmedia/test/src/main/kotlin/io/element/android/libraries/matrix/ui/test/media/FakeInitialsAvatarBitmapGenerator.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.ui.test.media + +import coil3.Bitmap +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.matrix.ui.media.InitialsAvatarBitmapGenerator +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeInitialsAvatarBitmapGenerator( + private val generateBitmapResult: (Int, AvatarData, Boolean, Float) -> Bitmap? = { _, _, _, _ -> lambdaError() } +) : InitialsAvatarBitmapGenerator { + override fun generateBitmap( + size: Int, + avatarData: AvatarData, + useDarkTheme: Boolean, + fontSizePercentage: Float, + ): Bitmap? { + return generateBitmapResult(size, avatarData, useDarkTheme, fontSizePercentage) + } +} diff --git a/libraries/matrixui/build.gradle.kts b/libraries/matrixui/build.gradle.kts index 89738613d2..94aa388c0c 100644 --- a/libraries/matrixui/build.gradle.kts +++ b/libraries/matrixui/build.gradle.kts @@ -30,13 +30,12 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.androidutils) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixmedia.api) implementation(projects.libraries.designsystem) implementation(projects.libraries.core) implementation(projects.libraries.uiStrings) implementation(projects.libraries.testtags) implementation(libs.coil.compose) - implementation(libs.coil.gif) - implementation(libs.coil.network.okhttp) implementation(libs.jsoup) implementation(projects.libraries.previewutils) diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/media/FakeLoggedInImageLoaderFactory.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/media/FakeLoggedInImageLoaderFactory.kt deleted file mode 100644 index b8ff9598e0..0000000000 --- a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/media/FakeLoggedInImageLoaderFactory.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2024, 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.matrix.ui.media - -import coil3.ImageLoader -import io.element.android.libraries.matrix.api.media.MatrixMediaLoader - -class FakeLoggedInImageLoaderFactory( - private val newImageLoaderLambda: (MatrixMediaLoader) -> ImageLoader -) : LoggedInImageLoaderFactory { - override fun newImageLoader(matrixMediaLoader: MatrixMediaLoader): ImageLoader { - return newImageLoaderLambda(matrixMediaLoader) - } -} diff --git a/libraries/mediaviewer/impl/build.gradle.kts b/libraries/mediaviewer/impl/build.gradle.kts index 6681007a4f..9c2342ecd9 100644 --- a/libraries/mediaviewer/impl/build.gradle.kts +++ b/libraries/mediaviewer/impl/build.gradle.kts @@ -44,7 +44,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.featureflag.api) implementation(projects.libraries.matrix.api) - implementation(projects.libraries.matrixui) + implementation(projects.libraries.matrixmedia.api) implementation(projects.libraries.uiStrings) implementation(projects.libraries.voiceplayer.api) implementation(projects.services.toolbox.api) @@ -61,6 +61,7 @@ dependencies { testImplementation(projects.libraries.dateformatter.test) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.matrixui) testImplementation(projects.libraries.mediaviewer.test) testImplementation(projects.services.toolbox.test) testImplementation(libs.coroutines.core) diff --git a/libraries/push/api/build.gradle.kts b/libraries/push/api/build.gradle.kts index 0f305dc9ab..df6ac6120a 100644 --- a/libraries/push/api/build.gradle.kts +++ b/libraries/push/api/build.gradle.kts @@ -17,7 +17,8 @@ dependencies { implementation(libs.androidx.corektx) implementation(libs.coroutines.core) implementation(libs.coil.compose) + implementation(projects.libraries.designsystem) implementation(projects.libraries.matrix.api) - implementation(projects.libraries.matrixui) + implementation(projects.libraries.matrixmedia.api) implementation(projects.libraries.pushproviders.api) } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationBitmapLoader.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationBitmapLoader.kt index b715bca561..bded69991b 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationBitmapLoader.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationBitmapLoader.kt @@ -11,17 +11,18 @@ package io.element.android.libraries.push.api.notifications import android.graphics.Bitmap import androidx.core.graphics.drawable.IconCompat import coil3.ImageLoader +import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.ui.media.AVATAR_THUMBNAIL_SIZE_IN_PIXEL interface NotificationBitmapLoader { /** * Get icon of a room. - * @param path mxc url + * @param avatarData the data related to the Avatar * @param imageLoader Coil image loader * @param targetSize The size we want the bitmap to be resized to */ suspend fun getRoomBitmap( - path: String?, + avatarData: AvatarData, imageLoader: ImageLoader, targetSize: Long = AVATAR_THUMBNAIL_SIZE_IN_PIXEL, ): Bitmap? @@ -29,8 +30,11 @@ interface NotificationBitmapLoader { /** * Get icon of a user. * Before Android P, this does nothing because the icon won't be used - * @param path mxc url + * @param avatarData the data related to the Avatar * @param imageLoader Coil image loader */ - suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat? + suspend fun getUserIcon( + avatarData: AvatarData, + imageLoader: ImageLoader, + ): IconCompat? } diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 162b62d2c1..b6ce453f5b 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(projects.libraries.network) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) + implementation(projects.libraries.matrixmedia.api) implementation(projects.features.networkmonitor.api) implementation(projects.libraries.preferences.api) implementation(projects.libraries.sessionStorage.api) @@ -77,7 +78,7 @@ dependencies { testCommonDependencies(libs) testImplementation(libs.coil.test) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.libraries.matrixuiTest) + testImplementation(projects.libraries.matrixmedia.test) testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.libraries.push.test) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationBitmapLoader.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationBitmapLoader.kt index d194f5d0db..086914ecf5 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationBitmapLoader.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationBitmapLoader.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.push.impl.notifications import android.content.Context +import android.content.res.Configuration import android.graphics.Bitmap import android.os.Build import androidx.core.graphics.drawable.IconCompat @@ -19,9 +20,11 @@ import coil3.toBitmap import coil3.transform.CircleCropTransformation import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.ui.media.AVATAR_THUMBNAIL_SIZE_IN_PIXEL +import io.element.android.libraries.matrix.ui.media.InitialsAvatarBitmapGenerator import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider @@ -31,48 +34,71 @@ import timber.log.Timber class DefaultNotificationBitmapLoader( @ApplicationContext private val context: Context, private val sdkIntProvider: BuildVersionSdkIntProvider, + private val initialsAvatarBitmapGenerator: InitialsAvatarBitmapGenerator, ) : NotificationBitmapLoader { - override suspend fun getRoomBitmap(path: String?, imageLoader: ImageLoader, targetSize: Long): Bitmap? { - if (path == null) { - return null - } - return loadRoomBitmap(path, imageLoader, targetSize) - } - - private suspend fun loadRoomBitmap(path: String, imageLoader: ImageLoader, targetSize: Long): Bitmap? { + override suspend fun getRoomBitmap( + avatarData: AvatarData, + imageLoader: ImageLoader, + targetSize: Long, + ): Bitmap? { return try { - val imageRequest = ImageRequest.Builder(context) - .data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(targetSize))) - .transformations(CircleCropTransformation()) - .build() - val result = imageLoader.execute(imageRequest) - result.image?.toBitmap() + loadBitmap( + avatarData = avatarData, + imageLoader = imageLoader, + targetSize = targetSize, + ) } catch (e: Throwable) { Timber.e(e, "Unable to load room bitmap") null } } - override suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat? { - if (path == null || sdkIntProvider.get() < Build.VERSION_CODES.P) { + override suspend fun getUserIcon( + avatarData: AvatarData, + imageLoader: ImageLoader, + ): IconCompat? { + if (sdkIntProvider.get() < Build.VERSION_CODES.P) { return null } - - return loadUserIcon(path, imageLoader) - } - - private suspend fun loadUserIcon(path: String, imageLoader: ImageLoader): IconCompat? { return try { - val imageRequest = ImageRequest.Builder(context) - .data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL))) - .transformations(CircleCropTransformation()) - .build() - val result = imageLoader.execute(imageRequest) - val bitmap = result.image?.toBitmap() - return bitmap?.let { IconCompat.createWithBitmap(it) } + loadBitmap( + avatarData = avatarData, + imageLoader = imageLoader, + targetSize = AVATAR_THUMBNAIL_SIZE_IN_PIXEL, + ) + ?.let { IconCompat.createWithBitmap(it) } } catch (e: Throwable) { Timber.e(e, "Unable to load user bitmap") null } } + + private fun isDarkTheme(): Boolean { + return context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + } + + private suspend fun loadBitmap( + avatarData: AvatarData, + imageLoader: ImageLoader, + targetSize: Long + ): Bitmap? { + val path = avatarData.url + val data = if (path != null) { + MediaRequestData( + source = MediaSource(path), + kind = MediaRequestData.Kind.Thumbnail(targetSize), + ) + } else { + initialsAvatarBitmapGenerator.generateBitmap( + size = targetSize.toInt(), + avatarData = avatarData, + useDarkTheme = isDarkTheme(), + ) + } + val imageRequest = ImageRequest.Builder(context) + .data(data) + .transformations(CircleCropTransformation()) + .build() + return imageLoader.execute(imageRequest).image?.toBitmap() + } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt index 7d251daf73..8683b58493 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt @@ -13,6 +13,8 @@ import android.graphics.Bitmap import coil3.ImageLoader import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader @@ -90,7 +92,18 @@ class DefaultRoomGroupMessageCreator( imageLoader: ImageLoader, ): Bitmap? { // Use the last event (most recent?) - return events.reversed().firstNotNullOfOrNull { it.roomAvatarPath } - ?.let { bitmapLoader.getRoomBitmap(it, imageLoader) } + val event = events.reversed().firstOrNull { it.roomAvatarPath != null } + ?: events.reversed().firstOrNull() + return event?.let { event -> + bitmapLoader.getRoomBitmap( + avatarData = AvatarData( + id = event.roomId.value, + name = event.roomName, + url = event.roomAvatarPath, + size = AvatarSize.RoomDetailsHeader, + ), + imageLoader = imageLoader, + ) + } } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationService.kt index db0540fcc7..ce20234385 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationService.kt @@ -10,7 +10,6 @@ package io.element.android.libraries.push.impl.notifications.conversations import android.content.Context import android.content.pm.ShortcutInfo -import android.content.res.Configuration import android.os.Build import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat @@ -29,7 +28,6 @@ import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder -import io.element.android.libraries.matrix.ui.media.InitialsAvatarBitmapGenerator import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService import io.element.android.libraries.push.impl.intent.IntentProvider @@ -95,15 +93,16 @@ class DefaultNotificationConversationService( val imageLoader = imageLoaderHolder.get(client) val defaultShortcutIconSize = ShortcutManagerCompat.getIconMaxWidth(context) - val useDarkTheme = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES val icon = bitmapLoader.getRoomBitmap( - path = roomAvatarUrl, + avatarData = AvatarData( + id = roomId.value, + name = roomName, + url = roomAvatarUrl, + size = AvatarSize.RoomDetailsHeader, + ), imageLoader = imageLoader, targetSize = defaultShortcutIconSize.toLong() )?.let(IconCompat::createWithBitmap) - ?: InitialsAvatarBitmapGenerator(useDarkTheme = useDarkTheme) - .generateBitmap(defaultShortcutIconSize, AvatarData(id = roomId.value, name = roomName, size = AvatarSize.RoomDetailsHeader)) - ?.let(IconCompat::createWithAdaptiveBitmap) val shortcutInfo = ShortcutInfoCompat.Builder(context, createShortcutId(sessionId, roomId)) .setShortLabel(roomName) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt index 3bf33d805e..a69ce5e673 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt @@ -20,12 +20,15 @@ import coil3.ImageLoader import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.timeline.item.event.EventType import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getBestName import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader import io.element.android.libraries.push.impl.R @@ -401,7 +404,17 @@ class DefaultNotificationCreator( } Person.Builder() .setName(displayName.annotateForDebug(70)) - .setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath, imageLoader)) + .setIcon( + bitmapLoader.getUserIcon( + avatarData = AvatarData( + id = event.senderId.value, + name = senderName, + url = event.senderAvatarPath, + size = AvatarSize.UserHeader, + ), + imageLoader = imageLoader, + ) + ) .setKey(key) .build() } @@ -460,7 +473,12 @@ class DefaultNotificationCreator( Person.Builder() // Note: name cannot be empty else NotificationCompat.MessagingStyle() will crash .setName(user.getBestName().annotateForDebug(50)) - .setIcon(bitmapLoader.getUserIcon(user.avatarUrl, imageLoader)) + .setIcon( + bitmapLoader.getUserIcon( + avatarData = user.getAvatarData(AvatarSize.UserHeader), + imageLoader = imageLoader, + ) + ) .setKey(user.userId.value) .build() ).also { diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultBaseRoomGroupMessageCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultBaseRoomGroupMessageCreatorTest.kt index 4fc3404323..bedcc2b34f 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultBaseRoomGroupMessageCreatorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultBaseRoomGroupMessageCreatorTest.kt @@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.media.AVATAR_THUMBNAIL_SIZE_IN_PIXEL import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.matrix.ui.test.media.FakeImageLoader +import io.element.android.libraries.matrix.ui.test.media.FakeInitialsAvatarBitmapGenerator import io.element.android.libraries.push.impl.notifications.factories.MARK_AS_READ_ACTION_TITLE import io.element.android.libraries.push.impl.notifications.factories.QUICK_REPLY_ACTION_TITLE import io.element.android.libraries.push.impl.notifications.factories.aNotificationAccountParams @@ -232,6 +233,7 @@ fun createRoomGroupMessageCreator( val bitmapLoader = DefaultNotificationBitmapLoader( context = RuntimeEnvironment.getApplication(), sdkIntProvider = sdkIntProvider, + initialsAvatarBitmapGenerator = FakeInitialsAvatarBitmapGenerator(), ) return DefaultRoomGroupMessageCreator( notificationCreator = createNotificationCreator(bitmapLoader = bitmapLoader), diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt index 670d5fad54..dccb4e7d11 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt @@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.test.media.FakeImageLoader +import io.element.android.libraries.matrix.ui.test.media.FakeInitialsAvatarBitmapGenerator import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader import io.element.android.libraries.push.impl.notifications.DefaultNotificationBitmapLoader import io.element.android.libraries.push.impl.notifications.NotificationActionIds @@ -297,7 +298,11 @@ fun createNotificationCreator( context: Context = RuntimeEnvironment.getApplication(), buildMeta: BuildMeta = aBuildMeta(), notificationChannels: NotificationChannels = createNotificationChannels(), - bitmapLoader: NotificationBitmapLoader = DefaultNotificationBitmapLoader(context, FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.R)), + bitmapLoader: NotificationBitmapLoader = DefaultNotificationBitmapLoader( + context = context, + sdkIntProvider = FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.R), + initialsAvatarBitmapGenerator = FakeInitialsAvatarBitmapGenerator(), + ), ): NotificationCreator { return DefaultNotificationCreator( context = context, diff --git a/libraries/push/test/build.gradle.kts b/libraries/push/test/build.gradle.kts index 259951008b..3a0b5532ae 100644 --- a/libraries/push/test/build.gradle.kts +++ b/libraries/push/test/build.gradle.kts @@ -17,6 +17,7 @@ android { dependencies { api(projects.libraries.push.api) api(projects.libraries.pushproviders.api) + implementation(projects.libraries.designsystem) implementation(projects.libraries.push.impl) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/push/FakeNotificationBitmapLoader.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/push/FakeNotificationBitmapLoader.kt index 56615dbba5..78af0e56c1 100644 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/push/FakeNotificationBitmapLoader.kt +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/push/FakeNotificationBitmapLoader.kt @@ -11,17 +11,18 @@ package io.element.android.libraries.push.test.notifications.push import android.graphics.Bitmap import androidx.core.graphics.drawable.IconCompat import coil3.ImageLoader +import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader class FakeNotificationBitmapLoader( - var getRoomBitmapResult: (String?, ImageLoader, Long) -> Bitmap? = { _, _, _ -> null }, - var getUserIconResult: (String?, ImageLoader) -> IconCompat? = { _, _ -> null }, + var getRoomBitmapResult: (AvatarData, ImageLoader, Long) -> Bitmap? = { _, _, _ -> null }, + var getUserIconResult: (AvatarData, ImageLoader) -> IconCompat? = { _, _ -> null }, ) : NotificationBitmapLoader { - override suspend fun getRoomBitmap(path: String?, imageLoader: ImageLoader, targetSize: Long): Bitmap? { - return getRoomBitmapResult(path, imageLoader, targetSize) + override suspend fun getRoomBitmap(avatarData: AvatarData, imageLoader: ImageLoader, targetSize: Long): Bitmap? { + return getRoomBitmapResult(avatarData, imageLoader, targetSize) } - override suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat? { - return getUserIconResult(path, imageLoader) + override suspend fun getUserIcon(avatarData: AvatarData, imageLoader: ImageLoader): IconCompat? { + return getUserIconResult(avatarData, imageLoader) } } diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index edf6127f49..5a4c4f9aeb 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -90,6 +90,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:designsystem")) implementation(project(":libraries:matrix:impl")) implementation(project(":libraries:matrixui")) + implementation(project(":libraries:matrixmedia:impl")) implementation(project(":libraries:network")) implementation(project(":libraries:core")) implementation(project(":libraries:eventformatter:impl"))