diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt index 7f36a7a51a..423228c110 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt @@ -33,13 +33,12 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.appnav.di.SessionComponentFactory import io.element.android.libraries.architecture.NodeInputs -import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.ui.di.MatrixUIBindings +import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder import kotlinx.parcelize.Parcelize /** @@ -52,6 +51,7 @@ class LoggedInAppScopeFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, sessionComponentFactory: SessionComponentFactory, + private val imageLoaderHolder: ImageLoaderHolder, ) : ParentNode( navModel = PermanentNavModel( navTargets = setOf(NavTarget), @@ -78,8 +78,7 @@ class LoggedInAppScopeFlowNode @AssistedInject constructor( super.onBuilt() lifecycle.subscribe( onCreate = { - val imageLoaderFactory = bindings().loggedInImageLoaderFactory() - Coil.setImageLoader(imageLoaderFactory) + Coil.setImageLoader(imageLoaderHolder.get(inputs.matrixClient)) }, ) } diff --git a/changelog.d/1991.bugfix b/changelog.d/1991.bugfix new file mode 100644 index 0000000000..e1dfcdcb2a --- /dev/null +++ b/changelog.d/1991.bugfix @@ -0,0 +1 @@ +Fix avatar not displayed in notification when the app is not in background diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummary.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummary.kt index cb695d4eda..9f74b90142 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummary.kt +++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummary.kt @@ -33,7 +33,7 @@ data class InviteListInviteSummary( val isNew: Boolean = false, ) -data class InviteSender constructor( +data class InviteSender( val userId: UserId, val displayName: String, val avatarData: AvatarData = AvatarData(userId.value, displayName, size = AvatarSize.InviteSender), diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt index c1f0158605..64e4c11723 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt @@ -16,7 +16,7 @@ package io.element.android.features.login.impl.resolver -data class HomeserverData constructor( +data class HomeserverData( // The computed homeserver url, for which a wellknown file has been retrieved, or just a valid Url val homeserverUrl: String, // True if a wellknown file has been found and is valid. If false, it means that the [homeserverUrl] is valid diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 2cf3d1f82c..f7c157d2ba 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -91,7 +91,7 @@ import org.matrix.rustcomponents.sdk.RoomVisibility as RustRoomVisibility import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService @OptIn(ExperimentalCoroutinesApi::class) -class RustMatrixClient constructor( +class RustMatrixClient( private val client: Client, private val syncService: ClientSyncService, private val sessionStore: SessionStore, diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt index c495558fd9..4c5309e2bd 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt @@ -28,8 +28,8 @@ import okhttp3.OkHttpClient import javax.inject.Inject import javax.inject.Provider -class LoggedInImageLoaderFactory @Inject constructor( - @ApplicationContext private val context: Context, +class LoggedInImageLoaderFactory( + private val context: Context, private val matrixClient: MatrixClient, private val okHttpClient: Provider, ) : ImageLoaderFactory { diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderHolder.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderHolder.kt new file mode 100644 index 0000000000..fc027d0e5c --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderHolder.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.ui.media + +import android.content.Context +import coil.ImageLoader +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.api.MatrixClient +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 +import okhttp3.OkHttpClient +import javax.inject.Inject +import javax.inject.Provider + +interface ImageLoaderHolder { + fun get(client: MatrixClient): ImageLoader +} + +@ContributesBinding(AppScope::class) +@SingleIn(AppScope::class) +class DefaultImageLoaderHolder @Inject constructor( + @ApplicationContext private val context: Context, + private val okHttpClient: Provider, + private val sessionObserver: SessionObserver, +) : ImageLoaderHolder { + private val map = mutableMapOf() + + init { + observeSessions() + } + + private fun observeSessions() { + sessionObserver.addListener(object : SessionListener { + override suspend fun onSessionCreated(userId: String) = Unit + + override suspend fun onSessionDeleted(userId: String) { + map.remove(SessionId(userId)) + } + }) + } + + override fun get(client: MatrixClient): ImageLoader { + return synchronized(map) { + map.getOrPut(client.sessionId) { + LoggedInImageLoaderFactory( + context = context, + matrixClient = client, + okHttpClient = okHttpClient, + ).newImageLoader() + } + } + } +} diff --git a/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/PickerProviderImpl.kt b/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/PickerProviderImpl.kt index 201fd9b069..f3c186a34f 100644 --- a/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/PickerProviderImpl.kt +++ b/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/PickerProviderImpl.kt @@ -37,7 +37,7 @@ import java.util.UUID import javax.inject.Inject @ContributesBinding(AppScope::class) -class PickerProviderImpl constructor(private val isInTest: Boolean) : PickerProvider { +class PickerProviderImpl(private val isInTest: Boolean) : PickerProvider { @Inject constructor(): this(false) diff --git a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakeComposablePermissionStateProvider.kt b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakeComposablePermissionStateProvider.kt index 900d9ccc69..948cc603c3 100644 --- a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakeComposablePermissionStateProvider.kt +++ b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakeComposablePermissionStateProvider.kt @@ -27,7 +27,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState import com.google.accompanist.permissions.PermissionStatus -class FakeComposablePermissionStateProvider constructor( +class FakeComposablePermissionStateProvider( private val permissionState: FakePermissionState ) : ComposablePermissionStateProvider { private lateinit var onPermissionResult: (Boolean) -> Unit diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index 09f622e978..904a6bf2ea 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt @@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder import io.element.android.libraries.push.api.notifications.NotificationDrawerManager import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.services.appnavstate.api.AppNavigationStateService @@ -61,6 +62,7 @@ class DefaultNotificationDrawerManager @Inject constructor( private val dispatchers: CoroutineDispatchers, private val buildMeta: BuildMeta, private val matrixClientProvider: MatrixClientProvider, + private val imageLoaderHolder: ImageLoaderHolder, ) : NotificationDrawerManager { private var appNavigationStateObserver: Job? = null @@ -288,10 +290,11 @@ class DefaultNotificationDrawerManager @Inject constructor( } eventsForSessions.forEach { (sessionId, notifiableEvents) -> + val client = matrixClientProvider.getOrRestore(sessionId).getOrThrow() + val imageLoader = imageLoaderHolder.get(client) val currentUser = tryOrNull( onError = { Timber.tag(loggerTag.value).e(it, "Unable to retrieve info for user ${sessionId.value}") }, operation = { - val client = matrixClientProvider.getOrRestore(sessionId).getOrThrow() // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash val myUserDisplayName = client.loadUserDisplayName().getOrNull() ?: sessionId.value val userAvatarUrl = client.loadUserAvatarURLString().getOrNull() @@ -307,7 +310,7 @@ class DefaultNotificationDrawerManager @Inject constructor( avatarUrl = null ) - notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents) + notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents, imageLoader) } } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt index e78ce37639..4c1e3043ad 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt @@ -21,7 +21,7 @@ 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.ImageLoader import coil.request.ImageRequest import coil.transform.CircleCropTransformation import io.element.android.libraries.di.ApplicationContext @@ -39,21 +39,22 @@ class NotificationBitmapLoader @Inject constructor( /** * Get icon of a room. * @param path mxc url + * @param imageLoader Coil image loader */ - suspend fun getRoomBitmap(path: String?): Bitmap? { + suspend fun getRoomBitmap(path: String?, imageLoader: ImageLoader): Bitmap? { if (path == null) { return null } - return loadRoomBitmap(path) + return loadRoomBitmap(path, imageLoader) } - private suspend fun loadRoomBitmap(path: String): Bitmap? { + private suspend fun loadRoomBitmap(path: String, imageLoader: ImageLoader): Bitmap? { return try { val imageRequest = ImageRequest.Builder(context) .data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(1024))) .transformations(CircleCropTransformation()) .build() - val result = context.imageLoader.execute(imageRequest) + val result = imageLoader.execute(imageRequest) result.drawable?.toBitmap() } catch (e: Throwable) { Timber.e(e, "Unable to load room bitmap") @@ -65,22 +66,23 @@ class NotificationBitmapLoader @Inject constructor( * Get icon of a user. * Before Android P, this does nothing because the icon won't be used * @param path mxc url + * @param imageLoader Coil image loader */ - suspend fun getUserIcon(path: String?): IconCompat? { + suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat? { if (path == null || sdkIntProvider.get() < Build.VERSION_CODES.P) { return null } - return loadUserIcon(path) + return loadUserIcon(path, imageLoader) } - private suspend fun loadUserIcon(path: String): IconCompat? { + private suspend fun loadUserIcon(path: String, imageLoader: ImageLoader): IconCompat? { return try { val imageRequest = ImageRequest.Builder(context) .data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(1024))) .transformations(CircleCropTransformation()) .build() - val result = context.imageLoader.execute(imageRequest) + val result = imageLoader.execute(imageRequest) val bitmap = result.drawable?.toBitmap() return bitmap?.let { IconCompat.createWithBitmap(it) } } catch (e: Throwable) { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt index 15c236eac9..4abdc03123 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt @@ -17,6 +17,7 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification +import coil.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.factories.NotificationCreator @@ -36,6 +37,7 @@ class NotificationFactory @Inject constructor( suspend fun Map.toNotifications( currentUser: MatrixUser, + imageLoader: ImageLoader, ): List { return map { (roomId, events) -> when { @@ -46,6 +48,7 @@ class NotificationFactory @Inject constructor( currentUser = currentUser, events = messageEvents, roomId = roomId, + imageLoader = imageLoader, ) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt index 713392c695..d05f3a1d03 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.push.impl.notifications +import coil.ImageLoader import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.user.MatrixUser @@ -38,11 +39,12 @@ class NotificationRenderer @Inject constructor( suspend fun render( currentUser: MatrixUser, useCompleteNotificationFormat: Boolean, - eventsToProcess: List> + eventsToProcess: List>, + imageLoader: ImageLoader, ) { val groupedEvents = eventsToProcess.groupByType() with(notificationFactory) { - val roomNotifications = groupedEvents.roomEvents.toNotifications(currentUser) + val roomNotifications = groupedEvents.roomEvents.toNotifications(currentUser, imageLoader) val invitationNotifications = groupedEvents.invitationEvents.toNotifications() val simpleNotifications = groupedEvents.simpleEvents.toNotifications() val fallbackNotifications = groupedEvents.fallbackEvents.toNotifications() 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 cde4f489a2..015b32c1d0 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 @@ -23,6 +23,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.Person import androidx.core.text.buildSpannedString import androidx.core.text.inSpans +import coil.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.R @@ -42,6 +43,7 @@ class RoomGroupMessageCreator @Inject constructor( currentUser: MatrixUser, events: List, roomId: RoomId, + imageLoader: ImageLoader, ): RoomNotification.Message { val lastKnownRoomEvent = events.last() val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderName ?: "Room name (${roomId.value.take(8)}…)" @@ -49,13 +51,13 @@ class RoomGroupMessageCreator @Inject constructor( val style = NotificationCompat.MessagingStyle( Person.Builder() .setName(currentUser.displayName?.annotateForDebug(50)) - .setIcon(bitmapLoader.getUserIcon(currentUser.avatarUrl)) + .setIcon(bitmapLoader.getUserIcon(currentUser.avatarUrl, imageLoader)) .setKey(lastKnownRoomEvent.sessionId.value) .build() ).also { it.conversationTitle = roomName.takeIf { roomIsGroup }?.annotateForDebug(51) it.isGroupConversation = roomIsGroup - it.addMessagesFromEvents(events) + it.addMessagesFromEvents(events, imageLoader) } val tickerText = if (roomIsGroup) { @@ -64,7 +66,7 @@ class RoomGroupMessageCreator @Inject constructor( stringProvider.getString(R.string.notification_ticker_text_dm, events.last().senderName, events.last().description) } - val largeBitmap = getRoomBitmap(events) + val largeBitmap = getRoomBitmap(events, imageLoader) val lastMessageTimestamp = events.last().timestamp val smartReplyErrors = events.filter { it.isSmartReplyError() } @@ -98,14 +100,17 @@ class RoomGroupMessageCreator @Inject constructor( ) } - private suspend fun NotificationCompat.MessagingStyle.addMessagesFromEvents(events: List) { + private suspend fun NotificationCompat.MessagingStyle.addMessagesFromEvents( + events: List, + imageLoader: ImageLoader, + ) { events.forEach { event -> val senderPerson = if (event.outGoingMessage) { null } else { Person.Builder() .setName(event.senderName?.annotateForDebug(70)) - .setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath)) + .setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath, imageLoader)) .setKey(event.senderId.value) .build() } @@ -167,10 +172,13 @@ class RoomGroupMessageCreator @Inject constructor( } } - private suspend fun getRoomBitmap(events: List): Bitmap? { + private suspend fun getRoomBitmap( + events: List, + imageLoader: ImageLoader, + ): Bitmap? { // Use the last event (most recent?) return events.reversed().firstNotNullOfOrNull { it.roomAvatarPath } - ?.let { bitmapLoader.getRoomBitmap(it) } + ?.let { bitmapLoader.getRoomBitmap(it, imageLoader) } } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt index 6b4ea24fd4..e641029c0d 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt @@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.push.impl.notifications.fake.FakeAndroidNotificationFactory +import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoaderHolder import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent @@ -129,6 +130,7 @@ class DefaultNotificationDrawerManagerTest { dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), buildMeta = aBuildMeta(), matrixClientProvider = FakeMatrixClientProvider(), + imageLoaderHolder = FakeImageLoaderHolder(), ) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt index 18d8870ac3..ca41fcc15a 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt @@ -23,6 +23,7 @@ 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 import io.element.android.libraries.push.impl.notifications.fake.FakeAndroidNotificationFactory +import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent @@ -30,12 +31,15 @@ import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNoti import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent import kotlinx.coroutines.test.runTest import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner private val MY_AVATAR_URL: String? = null private val AN_INVITATION_EVENT = anInviteNotifiableEvent(roomId = A_ROOM_ID) private val A_SIMPLE_EVENT = aSimpleNotifiableEvent(eventId = AN_EVENT_ID) private val A_MESSAGE_EVENT = aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID) +@RunWith(RobolectricTestRunner::class) class NotificationFactoryTest { private val androidNotificationFactory = FakeAndroidNotificationFactory() @@ -130,11 +134,14 @@ class NotificationFactoryTest { ) val roomWithMessage = mapOf(A_ROOM_ID to listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT))) + val fakeImageLoader = FakeImageLoader() val result = roomWithMessage.toNotifications( - MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) + currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), + imageLoader = fakeImageLoader.getImageLoader(), ) assertThat(result).isEqualTo(listOf(expectedNotification)) + assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) } @Test @@ -142,8 +149,10 @@ class NotificationFactoryTest { val events = listOf(ProcessedEvent(ProcessedEvent.Type.REMOVE, A_MESSAGE_EVENT)) val emptyRoom = mapOf(A_ROOM_ID to events) + val fakeImageLoader = FakeImageLoader() val result = emptyRoom.toNotifications( - MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) + currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), + imageLoader = fakeImageLoader.getImageLoader(), ) assertThat(result).isEqualTo( @@ -153,14 +162,17 @@ class NotificationFactoryTest { ) ) ) + assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) } @Test fun `given a room with only redacted events when mapping to notification then is Empty`() = testWith(notificationFactory) { val redactedRoom = mapOf(A_ROOM_ID to listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT.copy(isRedacted = true)))) + val fakeImageLoader = FakeImageLoader() val result = redactedRoom.toNotifications( - MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) + currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), + imageLoader = fakeImageLoader.getImageLoader(), ) assertThat(result).isEqualTo( @@ -170,6 +182,7 @@ class NotificationFactoryTest { ) ) ) + assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) } @Test @@ -189,11 +202,14 @@ class NotificationFactoryTest { A_ROOM_ID, ) + val fakeImageLoader = FakeImageLoader() val result = roomWithRedactedMessage.toNotifications( - MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) + currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), + imageLoader = fakeImageLoader.getImageLoader(), ) assertThat(result).isEqualTo(listOf(expectedNotification)) + assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt index 80875406c7..18f2266b87 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt @@ -21,12 +21,15 @@ import io.element.android.libraries.matrix.api.user.MatrixUser 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 +import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationFactory import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner private const val MY_USER_DISPLAY_NAME = "display-name" private const val MY_USER_AVATAR_URL = "avatar-url" @@ -42,6 +45,7 @@ private val MESSAGE_META = RoomNotification.Message.Meta( ) private val ONE_SHOT_META = OneShotNotification.Append.Meta(key = "ignored", summaryLine = "ignored", isNoisy = false, timestamp = -1) +@RunWith(RobolectricTestRunner::class) class NotificationRendererTest { private val notificationDisplayer = FakeNotificationDisplayer() @@ -197,7 +201,8 @@ class NotificationRendererTest { notificationRenderer.render( MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL), useCompleteNotificationFormat = USE_COMPLETE_NOTIFICATION_FORMAT, - eventsToProcess = AN_EVENT_LIST + eventsToProcess = AN_EVENT_LIST, + imageLoader = FakeImageLoader().getImageLoader(), ) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreatorTest.kt index 46c41454d3..399b0fc4b3 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreatorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreatorTest.kt @@ -17,19 +17,15 @@ package io.element.android.libraries.push.impl.notifications import android.content.Context -import android.graphics.Color -import android.graphics.drawable.ColorDrawable import android.os.Build -import coil.Coil -import coil.ImageLoader import coil.annotation.ExperimentalCoilApi -import coil.test.FakeImageLoaderEngine import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.push.impl.notifications.factories.createNotificationCreator +import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import io.element.android.services.toolbox.impl.strings.AndroidStringProvider @@ -50,6 +46,7 @@ class RoomGroupMessageCreatorTest { @Test fun `test createRoomMessage with one Event`() = runTest { val sut = createRoomGroupMessageCreator() + val fakeImageLoader = FakeImageLoader() val result = sut.createRoomMessage( currentUser = aMatrixUser(), events = listOf( @@ -58,6 +55,7 @@ class RoomGroupMessageCreatorTest { ) ), roomId = A_ROOM_ID, + imageLoader = fakeImageLoader.getImageLoader(), ) val resultMetaWithoutFormatting = result.meta.copy( summaryLine = result.meta.summaryLine.toString() @@ -71,11 +69,13 @@ class RoomGroupMessageCreatorTest { shouldBing = false, ) ) + assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) } @Test fun `test createRoomMessage with one noisy Event`() = runTest { val sut = createRoomGroupMessageCreator() + val fakeImageLoader = FakeImageLoader() val result = sut.createRoomMessage( currentUser = aMatrixUser(), events = listOf( @@ -84,6 +84,7 @@ class RoomGroupMessageCreatorTest { ) ), roomId = A_ROOM_ID, + imageLoader = fakeImageLoader.getImageLoader(), ) val resultMetaWithoutFormatting = result.meta.copy( summaryLine = result.meta.summaryLine.toString() @@ -97,6 +98,7 @@ class RoomGroupMessageCreatorTest { shouldBing = true, ) ) + assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) } @Test @@ -141,20 +143,7 @@ class RoomGroupMessageCreatorTest { api: Int, expectedCoilRequests: List, ) = runTest { - val coilRequests = mutableListOf() - val engine = FakeImageLoaderEngine.Builder() - .intercept( - predicate = { - coilRequests.add(it) - true - }, - drawable = ColorDrawable(Color.BLUE) - ) - .build() - val imageLoader = ImageLoader.Builder(RuntimeEnvironment.getApplication()) - .components { add(engine) } - .build() - Coil.setImageLoader(imageLoader) + val fakeImageLoader = FakeImageLoader() val sut = createRoomGroupMessageCreator( sdkIntProvider = FakeBuildVersionSdkIntProvider(api) ) @@ -170,6 +159,7 @@ class RoomGroupMessageCreatorTest { ) ), roomId = A_ROOM_ID, + imageLoader = fakeImageLoader.getImageLoader(), ) val resultMetaWithoutFormatting = result.meta.copy( summaryLine = result.meta.summaryLine.toString() @@ -183,12 +173,13 @@ class RoomGroupMessageCreatorTest { shouldBing = false, ) ) - assertThat(coilRequests.toList()).isEqualTo(expectedCoilRequests) + assertThat(fakeImageLoader.getCoilRequests()).isEqualTo(expectedCoilRequests) } @Test fun `test createRoomMessage with two Events`() = runTest { val sut = createRoomGroupMessageCreator() + val fakeImageLoader = FakeImageLoader() val result = sut.createRoomMessage( currentUser = aMatrixUser(), events = listOf( @@ -196,6 +187,7 @@ class RoomGroupMessageCreatorTest { aNotifiableMessageEvent(timestamp = A_TIMESTAMP + 10), ), roomId = A_ROOM_ID, + imageLoader = fakeImageLoader.getImageLoader(), ) val resultMetaWithoutFormatting = result.meta.copy( summaryLine = result.meta.summaryLine.toString() @@ -209,11 +201,13 @@ class RoomGroupMessageCreatorTest { shouldBing = false, ) ) + assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) } @Test fun `test createRoomMessage with smart reply error`() = runTest { val sut = createRoomGroupMessageCreator() + val fakeImageLoader = FakeImageLoader() val result = sut.createRoomMessage( currentUser = aMatrixUser(), events = listOf( @@ -223,6 +217,7 @@ class RoomGroupMessageCreatorTest { ), ), roomId = A_ROOM_ID, + imageLoader = fakeImageLoader.getImageLoader(), ) val resultMetaWithoutFormatting = result.meta.copy( summaryLine = result.meta.summaryLine.toString() @@ -236,11 +231,13 @@ class RoomGroupMessageCreatorTest { shouldBing = false, ) ) + assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) } @Test fun `test createRoomMessage for direct room`() = runTest { val sut = createRoomGroupMessageCreator() + val fakeImageLoader = FakeImageLoader() val result = sut.createRoomMessage( currentUser = aMatrixUser(), events = listOf( @@ -249,6 +246,7 @@ class RoomGroupMessageCreatorTest { ), ), roomId = A_ROOM_ID, + imageLoader = fakeImageLoader.getImageLoader(), ) val resultMetaWithoutFormatting = result.meta.copy( summaryLine = result.meta.summaryLine.toString() @@ -262,6 +260,7 @@ class RoomGroupMessageCreatorTest { shouldBing = false, ) ) + assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoader.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoader.kt new file mode 100644 index 0000000000..5c747b45e2 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoader.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications.fake + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import coil.ImageLoader +import coil.annotation.ExperimentalCoilApi +import coil.test.FakeImageLoaderEngine +import org.robolectric.RuntimeEnvironment + +@OptIn(ExperimentalCoilApi::class) +class FakeImageLoader { + private val coilRequests = mutableListOf() + + private var cache: ImageLoader? = null + + fun getImageLoader(): ImageLoader { + return cache ?: ImageLoader.Builder(RuntimeEnvironment.getApplication()) + .components { + val engine = FakeImageLoaderEngine.Builder() + .intercept( + predicate = { + coilRequests.add(it) + true + }, + drawable = ColorDrawable(Color.BLUE) + ) + .build() + add(engine) + } + .build() + .also { + cache = it + } + } + + fun getCoilRequests(): List { + return coilRequests.toList() + } +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/di/MatrixUIBindings.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoaderHolder.kt similarity index 58% rename from libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/di/MatrixUIBindings.kt rename to libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoaderHolder.kt index 919971b307..0e18036e94 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/di/MatrixUIBindings.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoaderHolder.kt @@ -14,13 +14,15 @@ * limitations under the License. */ -package io.element.android.libraries.matrix.ui.di +package io.element.android.libraries.push.impl.notifications.fake -import com.squareup.anvil.annotations.ContributesTo -import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.matrix.ui.media.LoggedInImageLoaderFactory +import coil.ImageLoader +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder -@ContributesTo(SessionScope::class) -interface MatrixUIBindings { - fun loggedInImageLoaderFactory(): LoggedInImageLoaderFactory +class FakeImageLoaderHolder : ImageLoaderHolder { + private val fakeImageLoader = FakeImageLoader() + override fun get(client: MatrixClient): ImageLoader { + return fakeImageLoader.getImageLoader() + } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt index 60b9e10c3d..9c7755aa9d 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt @@ -40,7 +40,7 @@ class FakeNotificationFactory { summaryNotification: SummaryNotification ) { with(instance) { - coEvery { groupedEvents.roomEvents.toNotifications(matrixUser) } returns roomNotifications + coEvery { groupedEvents.roomEvents.toNotifications(matrixUser, any()) } returns roomNotifications every { groupedEvents.invitationEvents.toNotifications() } returns invitationNotifications every { groupedEvents.simpleEvents.toNotifications() } returns simpleNotifications every { groupedEvents.fallbackEvents.toNotifications() } returns fallbackNotifications diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt index b896737e6f..946b0ab0db 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt @@ -39,6 +39,7 @@ class FakeRoomGroupMessageCreator { currentUser = matrixUser, events = events, roomId = roomId, + imageLoader = any(), ) } returns mockMessage return mockMessage