From 3b6c92943cab1f0563b40d5a7903902b49f3b851 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 24 Nov 2023 18:05:13 +0100 Subject: [PATCH 01/12] Remove dead code. --- .../libraries/androidutils/system/SystemUtils.kt | 15 --------------- .../channels/NotificationChannels.kt | 14 -------------- 2 files changed, 29 deletions(-) diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt index 08155b2a34..42eb81d4a7 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt @@ -16,8 +16,6 @@ package io.element.android.libraries.androidutils.system -import android.annotation.TargetApi -import android.app.Activity import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent @@ -104,19 +102,6 @@ fun Context.openAppSettingsPage( } } -/** - * Shows notification system settings for the given channel id. - */ -@TargetApi(Build.VERSION_CODES.O) -fun Activity.startNotificationChannelSettingsIntent(channelID: String) { - if (!supportNotificationChannels()) return - val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { - putExtra(Settings.EXTRA_APP_PACKAGE, packageName) - putExtra(Settings.EXTRA_CHANNEL_ID, channelID) - } - startActivity(intent) -} - @RequiresApi(Build.VERSION_CODES.O) fun Context.startInstallFromSourceIntent( activityResultLauncher: ActivityResultLauncher, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt index ab115262de..74020f9323 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt @@ -16,7 +16,6 @@ package io.element.android.libraries.push.impl.notifications.channels -import android.app.Activity import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context @@ -24,7 +23,6 @@ import android.os.Build import androidx.annotation.ChecksSdkIntAtLeast import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat -import io.element.android.libraries.androidutils.system.startNotificationChannelSettingsIntent import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SingleIn @@ -163,17 +161,5 @@ class NotificationChannels @Inject constructor( @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O) private fun supportNotificationChannels() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O - - fun openSystemSettingsForSilentCategory(activity: Activity) { - activity.startNotificationChannelSettingsIntent(SILENT_NOTIFICATION_CHANNEL_ID) - } - - fun openSystemSettingsForNoisyCategory(activity: Activity) { - activity.startNotificationChannelSettingsIntent(NOISY_NOTIFICATION_CHANNEL_ID) - } - - fun openSystemSettingsForCallCategory(activity: Activity) { - activity.startNotificationChannelSettingsIntent(CALL_NOTIFICATION_CHANNEL_ID) - } } } From 1fdf8dad1f3d8e1dc216a98ba819109c614963c6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 24 Nov 2023 18:10:10 +0100 Subject: [PATCH 02/12] Rename NotificationFactory to NotificationCreator to fix the name clash. --- .../push/impl/notifications/NotificationFactory.kt | 11 +++++------ .../impl/notifications/RoomGroupMessageCreator.kt | 6 +++--- .../impl/notifications/SummaryGroupMessageCreator.kt | 8 ++++---- ...{NotificationFactory.kt => NotificationCreator.kt} | 2 +- .../fake/FakeAndroidNotificationFactory.kt | 4 ++-- 5 files changed, 15 insertions(+), 16 deletions(-) rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/{NotificationFactory.kt => NotificationCreator.kt} (99%) 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 859bff17cf..15c236eac9 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 @@ -19,7 +19,7 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification 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.NotificationFactory +import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent @@ -28,9 +28,8 @@ import javax.inject.Inject private typealias ProcessedMessageEvents = List> -// TODO Find a better name, it clashes with io.element.android.libraries.push.impl.notifications.factories.NotificationFactory class NotificationFactory @Inject constructor( - private val notificationFactory: NotificationFactory, + private val notificationCreator: NotificationCreator, private val roomGroupMessageCreator: RoomGroupMessageCreator, private val summaryGroupMessageCreator: SummaryGroupMessageCreator ) { @@ -65,7 +64,7 @@ class NotificationFactory @Inject constructor( when (processed) { ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.roomId.value) ProcessedEvent.Type.KEEP -> OneShotNotification.Append( - notificationFactory.createRoomInvitationNotification(event), + notificationCreator.createRoomInvitationNotification(event), OneShotNotification.Append.Meta( key = event.roomId.value, summaryLine = event.description, @@ -83,7 +82,7 @@ class NotificationFactory @Inject constructor( when (processed) { ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId.value) ProcessedEvent.Type.KEEP -> OneShotNotification.Append( - notificationFactory.createSimpleEventNotification(event), + notificationCreator.createSimpleEventNotification(event), OneShotNotification.Append.Meta( key = event.eventId.value, summaryLine = event.description, @@ -100,7 +99,7 @@ class NotificationFactory @Inject constructor( when (processed) { ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId.value) ProcessedEvent.Type.KEEP -> OneShotNotification.Append( - notificationFactory.createFallbackNotification(event), + notificationCreator.createFallbackNotification(event), OneShotNotification.Append.Meta( key = event.eventId.value, summaryLine = event.description.orEmpty(), 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 29d828d34c..659c341c3b 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 @@ -27,7 +27,7 @@ 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 import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug -import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory +import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.services.toolbox.api.strings.StringProvider import timber.log.Timber @@ -36,7 +36,7 @@ import javax.inject.Inject class RoomGroupMessageCreator @Inject constructor( private val bitmapLoader: NotificationBitmapLoader, private val stringProvider: StringProvider, - private val notificationFactory: NotificationFactory + private val notificationCreator: NotificationCreator ) { suspend fun createRoomMessage( @@ -78,7 +78,7 @@ class RoomGroupMessageCreator @Inject constructor( shouldBing = events.any { it.noisy } ) return RoomNotification.Message( - notificationFactory.createMessagesListNotification( + notificationCreator.createMessagesListNotification( style, RoomEventGroupInfo( sessionId = currentUser.userId, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt index f999456107..f46df7d3a5 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt @@ -21,7 +21,7 @@ import androidx.core.app.NotificationCompat import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug -import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory +import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator import io.element.android.services.toolbox.api.strings.StringProvider import javax.inject.Inject @@ -41,7 +41,7 @@ import javax.inject.Inject */ class SummaryGroupMessageCreator @Inject constructor( private val stringProvider: StringProvider, - private val notificationFactory: NotificationFactory, + private val notificationCreator: NotificationCreator, ) { fun createSummaryNotification( @@ -77,7 +77,7 @@ class SummaryGroupMessageCreator @Inject constructor( // Use account name now, for multi-session .setSummaryText(currentUser.userId.value.annotateForDebug(44)) return if (useCompleteNotificationFormat) { - notificationFactory.createSummaryListNotification( + notificationCreator.createSummaryListNotification( currentUser, summaryInboxStyle, sumTitle, @@ -170,7 +170,7 @@ class SummaryGroupMessageCreator @Inject constructor( messageStr } } - return notificationFactory.createSummaryListNotification( + return notificationCreator.createSummaryListNotification( currentUser = currentUser, style = null, compatSummary = privacyTitle, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt similarity index 99% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt index 105b5789e4..2beddc53f9 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt @@ -41,7 +41,7 @@ import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiab import io.element.android.services.toolbox.api.strings.StringProvider import javax.inject.Inject -class NotificationFactory @Inject constructor( +class NotificationCreator @Inject constructor( @ApplicationContext private val context: Context, private val notificationChannels: NotificationChannels, private val stringProvider: StringProvider, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeAndroidNotificationFactory.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeAndroidNotificationFactory.kt index c046e1253f..6637b64979 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeAndroidNotificationFactory.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeAndroidNotificationFactory.kt @@ -17,14 +17,14 @@ package io.element.android.libraries.push.impl.notifications.fake import android.app.Notification -import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory +import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent import io.mockk.every import io.mockk.mockk class FakeAndroidNotificationFactory { - val instance = mockk() + val instance = mockk() fun givenCreateRoomInvitationNotificationFor(event: InviteNotifiableEvent): Notification { val mockNotification = mockk() From 5a979e623766b0d8213989266df8c158339e88de Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 24 Nov 2023 19:26:47 +0100 Subject: [PATCH 03/12] Add test for NotificationCreator --- .../factories/FakeIntentProvider.kt | 29 ++ .../factories/NotificationCreatorTest.kt | 314 ++++++++++++++++++ 2 files changed, 343 insertions(+) create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/FakeIntentProvider.kt create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreatorTest.kt diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/FakeIntentProvider.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/FakeIntentProvider.kt new file mode 100644 index 0000000000..2de70bb672 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/FakeIntentProvider.kt @@ -0,0 +1,29 @@ +/* + * 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.factories + +import android.content.Intent +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.push.impl.intent.IntentProvider + +class FakeIntentProvider : IntentProvider { + override fun getViewRoomIntent(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?) = Intent() + + override fun getInviteListIntent(sessionId: SessionId) = Intent() +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreatorTest.kt new file mode 100644 index 0000000000..49bb7be857 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreatorTest.kt @@ -0,0 +1,314 @@ +/* + * 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.factories + +import android.app.Notification +import android.content.Context +import androidx.core.app.NotificationCompat +import androidx.core.app.Person +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.core.meta.BuildMeta +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.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.push.impl.notifications.NotificationActionIds +import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo +import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels +import io.element.android.libraries.push.impl.notifications.factories.action.MarkAsReadActionFactory +import io.element.android.libraries.push.impl.notifications.factories.action.QuickReplyActionFactory +import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent +import io.element.android.services.toolbox.test.strings.FakeStringProvider +import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP +import io.element.android.services.toolbox.test.systemclock.FakeSystemClock +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@RunWith(RobolectricTestRunner::class) +class NotificationCreatorTest { + @Test + fun `test createDiagnosticNotification`() { + val sut = createNotificationCreator() + val result = sut.createDiagnosticNotification() + result.commonAssertions( + expectedGroup = null, + expectedCategory = NotificationCompat.CATEGORY_STATUS, + ) + } + + @Test + fun `test createFallbackNotification`() { + val sut = createNotificationCreator() + val result = sut.createFallbackNotification( + FallbackNotifiableEvent( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + editedEventId = null, + description = "description", + canBeReplaced = false, + isRedacted = false, + isUpdated = false, + timestamp = A_FAKE_TIMESTAMP, + ) + ) + result.commonAssertions( + expectedCategory = null, + ) + } + + @Test + fun `test createSimpleEventNotification`() { + val sut = createNotificationCreator() + val result = sut.createSimpleEventNotification( + SimpleNotifiableEvent( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + editedEventId = null, + noisy = false, + title = "title", + description = "description", + type = null, + timestamp = A_FAKE_TIMESTAMP, + soundName = null, + canBeReplaced = false, + isRedacted = false, + isUpdated = false, + ) + ) + result.commonAssertions( + expectedCategory = null, + ) + } + + @Test + fun `test createSimpleEventNotification noisy`() { + val sut = createNotificationCreator() + val result = sut.createSimpleEventNotification( + SimpleNotifiableEvent( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + editedEventId = null, + noisy = true, + title = "title", + description = "description", + type = null, + timestamp = A_FAKE_TIMESTAMP, + soundName = null, + canBeReplaced = false, + isRedacted = false, + isUpdated = false, + ) + ) + result.commonAssertions( + expectedCategory = null, + ) + } + + @Test + fun `test createRoomInvitationNotification`() { + val sut = createNotificationCreator() + val result = sut.createRoomInvitationNotification( + InviteNotifiableEvent( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + editedEventId = null, + noisy = false, + title = "title", + description = "description", + type = null, + timestamp = A_FAKE_TIMESTAMP, + soundName = null, + canBeReplaced = false, + isRedacted = false, + isUpdated = false, + roomName = "roomName", + ) + ) + result.commonAssertions( + expectedCategory = null, + ) + } + + @Test + fun `test createRoomInvitationNotification noisy`() { + val sut = createNotificationCreator() + val result = sut.createRoomInvitationNotification( + InviteNotifiableEvent( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + editedEventId = null, + noisy = true, + title = "title", + description = "description", + type = null, + timestamp = A_FAKE_TIMESTAMP, + soundName = null, + canBeReplaced = false, + isRedacted = false, + isUpdated = false, + roomName = "roomName", + ) + ) + result.commonAssertions( + expectedCategory = null, + ) + } + + @Test + fun `test createSummaryListNotification`() { + val sut = createNotificationCreator() + val matrixUser = aMatrixUser() + val result = sut.createSummaryListNotification( + currentUser = matrixUser, + style = null, + compatSummary = "compatSummary", + noisy = false, + lastMessageTimestamp = 123456L, + ) + result.commonAssertions( + expectedGroup = matrixUser.userId.value, + ) + } + + @Test + fun `test createSummaryListNotification noisy`() { + val sut = createNotificationCreator() + val matrixUser = aMatrixUser() + val result = sut.createSummaryListNotification( + currentUser = matrixUser, + style = null, + compatSummary = "compatSummary", + noisy = true, + lastMessageTimestamp = 123456L, + ) + result.commonAssertions( + expectedGroup = matrixUser.userId.value, + ) + } + + @Test + fun `test createMessagesListNotification`() { + val sut = createNotificationCreator() + aMatrixUser() + val result = sut.createMessagesListNotification( + messageStyle = NotificationCompat.MessagingStyle( + Person.Builder() + .setName("name") + .build() + ), + roomInfo = RoomEventGroupInfo( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + roomDisplayName = "roomDisplayName", + isDirect = false, + hasSmartReplyError = false, + shouldBing = false, + customSound = null, + isUpdated = false, + ), + threadId = null, + largeIcon = null, + lastMessageTimestamp = 123456L, + tickerText = "tickerText", + ) + result.commonAssertions() + } + + @Test + fun `test createMessagesListNotification should bing and thread`() { + val sut = createNotificationCreator() + aMatrixUser() + val result = sut.createMessagesListNotification( + messageStyle = NotificationCompat.MessagingStyle( + Person.Builder() + .setName("name") + .build() + ), + roomInfo = RoomEventGroupInfo( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + roomDisplayName = "roomDisplayName", + isDirect = false, + hasSmartReplyError = false, + shouldBing = true, + customSound = null, + isUpdated = false, + ), + threadId = A_THREAD_ID, + largeIcon = null, + lastMessageTimestamp = 123456L, + tickerText = "tickerText", + ) + result.commonAssertions() + } + + private fun createNotificationCreator( + context: Context = RuntimeEnvironment.getApplication(), + buildMeta: BuildMeta = aBuildMeta(), + notificationChannels: NotificationChannels = createNotificationChannels() + ): NotificationCreator { + return NotificationCreator( + context = context, + notificationChannels = notificationChannels, + stringProvider = FakeStringProvider("test"), + buildMeta = buildMeta, + pendingIntentFactory = PendingIntentFactory( + context, + FakeIntentProvider(), + FakeSystemClock(), + NotificationActionIds(buildMeta), + ), + markAsReadActionFactory = MarkAsReadActionFactory( + context = context, + actionIds = NotificationActionIds(buildMeta), + stringProvider = FakeStringProvider("MarkAsReadActionFactory"), + clock = FakeSystemClock(), + ), + quickReplyActionFactory = QuickReplyActionFactory( + context = context, + actionIds = NotificationActionIds(buildMeta), + stringProvider = FakeStringProvider("QuickReplyActionFactory"), + clock = FakeSystemClock(), + ), + ) + } + + private fun createNotificationChannels(): NotificationChannels { + val context = RuntimeEnvironment.getApplication() + return NotificationChannels(context, FakeStringProvider("")) + } + + private fun Notification.commonAssertions( + expectedGroup: String? = A_SESSION_ID.value, + expectedCategory: String? = NotificationCompat.CATEGORY_MESSAGE, + ) { + assertThat(contentIntent).isNotNull() + assertThat(group).isEqualTo(expectedGroup) + assertThat(category).isEqualTo(expectedCategory) + } +} From 435827af2805436b7cd21f11411bef19b33dcf34 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 27 Nov 2023 10:43:31 +0100 Subject: [PATCH 04/12] Add test for RoomGroupMessageCreator --- .../RoomGroupMessageCreatorTest.kt | 174 ++++++++++++++++++ .../factories/NotificationCreatorTest.kt | 72 ++++---- .../fixtures/NotifiableEventFixture.kt | 5 +- 3 files changed, 213 insertions(+), 38 deletions(-) create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreatorTest.kt 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 new file mode 100644 index 0000000000..1a4d0bf36c --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreatorTest.kt @@ -0,0 +1,174 @@ +/* + * 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 + +import android.content.Context +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.push.impl.notifications.factories.createNotificationCreator +import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent +import io.element.android.services.toolbox.impl.strings.AndroidStringProvider +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +private const val A_TIMESTAMP = 6480L + +@RunWith(RobolectricTestRunner::class) +class RoomGroupMessageCreatorTest { + @Test + fun `test createRoomMessage with one Event`() = runTest { + val sut = createRoomGroupMessageCreator() + val result = sut.createRoomMessage( + currentUser = aMatrixUser(), + events = listOf( + aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( + imageUriString = "aUri", + ) + ), + roomId = A_ROOM_ID, + ) + val resultMetaWithoutFormatting = result.meta.copy( + summaryLine = result.meta.summaryLine.toString() + ) + assertThat(resultMetaWithoutFormatting).isEqualTo( + RoomNotification.Message.Meta( + roomId = A_ROOM_ID, + summaryLine = "room-name: message-body", + messageCount = 1, + latestTimestamp = A_TIMESTAMP, + shouldBing = false, + ) + ) + } + + @Test + fun `test createRoomMessage with one noisy Event`() = runTest { + val sut = createRoomGroupMessageCreator() + val result = sut.createRoomMessage( + currentUser = aMatrixUser(), + events = listOf( + aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( + noisy = true, + ) + ), + roomId = A_ROOM_ID, + ) + val resultMetaWithoutFormatting = result.meta.copy( + summaryLine = result.meta.summaryLine.toString() + ) + assertThat(resultMetaWithoutFormatting).isEqualTo( + RoomNotification.Message.Meta( + roomId = A_ROOM_ID, + summaryLine = "room-name: message-body", + messageCount = 1, + latestTimestamp = A_TIMESTAMP, + shouldBing = true, + ) + ) + } + + @Test + fun `test createRoomMessage with two Events`() = runTest { + val sut = createRoomGroupMessageCreator() + val result = sut.createRoomMessage( + currentUser = aMatrixUser(), + events = listOf( + aNotifiableMessageEvent(timestamp = A_TIMESTAMP), + aNotifiableMessageEvent(timestamp = A_TIMESTAMP + 10), + ), + roomId = A_ROOM_ID, + ) + val resultMetaWithoutFormatting = result.meta.copy( + summaryLine = result.meta.summaryLine.toString() + ) + assertThat(resultMetaWithoutFormatting).isEqualTo( + RoomNotification.Message.Meta( + roomId = A_ROOM_ID, + summaryLine = "room-name: 2 messages", + messageCount = 2, + latestTimestamp = A_TIMESTAMP + 10, + shouldBing = false, + ) + ) + } + + @Test + fun `test createRoomMessage with smart reply error`() = runTest { + val sut = createRoomGroupMessageCreator() + val result = sut.createRoomMessage( + currentUser = aMatrixUser(), + events = listOf( + aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( + outGoingMessage = true, + outGoingMessageFailed = true, + ), + ), + roomId = A_ROOM_ID, + ) + val resultMetaWithoutFormatting = result.meta.copy( + summaryLine = result.meta.summaryLine.toString() + ) + assertThat(resultMetaWithoutFormatting).isEqualTo( + RoomNotification.Message.Meta( + roomId = A_ROOM_ID, + summaryLine = "room-name: message-body", + messageCount = 0, + latestTimestamp = A_TIMESTAMP, + shouldBing = false, + ) + ) + } + + @Test + fun `test createRoomMessage for direct room`() = runTest { + val sut = createRoomGroupMessageCreator() + val result = sut.createRoomMessage( + currentUser = aMatrixUser(), + events = listOf( + aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( + roomIsDirect = true, + ), + ), + roomId = A_ROOM_ID, + ) + val resultMetaWithoutFormatting = result.meta.copy( + summaryLine = result.meta.summaryLine.toString() + ) + assertThat(resultMetaWithoutFormatting).isEqualTo( + RoomNotification.Message.Meta( + roomId = A_ROOM_ID, + summaryLine = "sender-name: message-body", + messageCount = 1, + latestTimestamp = A_TIMESTAMP, + shouldBing = false, + ) + ) + } +} + +fun createRoomGroupMessageCreator(): RoomGroupMessageCreator { + val context = RuntimeEnvironment.getApplication() as Context + return RoomGroupMessageCreator( + notificationCreator = createNotificationCreator(), + bitmapLoader = NotificationBitmapLoader(RuntimeEnvironment.getApplication()), + stringProvider = AndroidStringProvider(context.resources) + ) +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreatorTest.kt index 49bb7be857..18d1fb723b 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreatorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreatorTest.kt @@ -267,42 +267,6 @@ class NotificationCreatorTest { result.commonAssertions() } - private fun createNotificationCreator( - context: Context = RuntimeEnvironment.getApplication(), - buildMeta: BuildMeta = aBuildMeta(), - notificationChannels: NotificationChannels = createNotificationChannels() - ): NotificationCreator { - return NotificationCreator( - context = context, - notificationChannels = notificationChannels, - stringProvider = FakeStringProvider("test"), - buildMeta = buildMeta, - pendingIntentFactory = PendingIntentFactory( - context, - FakeIntentProvider(), - FakeSystemClock(), - NotificationActionIds(buildMeta), - ), - markAsReadActionFactory = MarkAsReadActionFactory( - context = context, - actionIds = NotificationActionIds(buildMeta), - stringProvider = FakeStringProvider("MarkAsReadActionFactory"), - clock = FakeSystemClock(), - ), - quickReplyActionFactory = QuickReplyActionFactory( - context = context, - actionIds = NotificationActionIds(buildMeta), - stringProvider = FakeStringProvider("QuickReplyActionFactory"), - clock = FakeSystemClock(), - ), - ) - } - - private fun createNotificationChannels(): NotificationChannels { - val context = RuntimeEnvironment.getApplication() - return NotificationChannels(context, FakeStringProvider("")) - } - private fun Notification.commonAssertions( expectedGroup: String? = A_SESSION_ID.value, expectedCategory: String? = NotificationCompat.CATEGORY_MESSAGE, @@ -312,3 +276,39 @@ class NotificationCreatorTest { assertThat(category).isEqualTo(expectedCategory) } } + +fun createNotificationCreator( + context: Context = RuntimeEnvironment.getApplication(), + buildMeta: BuildMeta = aBuildMeta(), + notificationChannels: NotificationChannels = createNotificationChannels() +): NotificationCreator { + return NotificationCreator( + context = context, + notificationChannels = notificationChannels, + stringProvider = FakeStringProvider("test"), + buildMeta = buildMeta, + pendingIntentFactory = PendingIntentFactory( + context, + FakeIntentProvider(), + FakeSystemClock(), + NotificationActionIds(buildMeta), + ), + markAsReadActionFactory = MarkAsReadActionFactory( + context = context, + actionIds = NotificationActionIds(buildMeta), + stringProvider = FakeStringProvider("MarkAsReadActionFactory"), + clock = FakeSystemClock(), + ), + quickReplyActionFactory = QuickReplyActionFactory( + context = context, + actionIds = NotificationActionIds(buildMeta), + stringProvider = FakeStringProvider("QuickReplyActionFactory"), + clock = FakeSystemClock(), + ), + ) +} + +fun createNotificationChannels(): NotificationChannels { + val context = RuntimeEnvironment.getApplication() + return NotificationChannels(context, FakeStringProvider("")) +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt index 780d2abb71..44a2873465 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt @@ -77,13 +77,14 @@ fun aNotifiableMessageEvent( roomId: RoomId = A_ROOM_ID, eventId: EventId = AN_EVENT_ID, threadId: ThreadId? = null, - isRedacted: Boolean = false + isRedacted: Boolean = false, + timestamp: Long = 0, ) = NotifiableMessageEvent( sessionId = sessionId, eventId = eventId, editedEventId = null, noisy = false, - timestamp = 0, + timestamp = timestamp, senderName = "sender-name", senderId = UserId("@sending-id:domain.com"), body = "message-body", From abf1de7f42a80d28544434558343e20c7c3be5a9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 27 Nov 2023 10:45:47 +0100 Subject: [PATCH 05/12] Fix issue in RoomGroupMessageCreator --- .../push/impl/notifications/RoomGroupMessageCreator.kt | 2 +- .../push/impl/notifications/RoomGroupMessageCreatorTest.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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 659c341c3b..e667bd5936 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 @@ -166,7 +166,7 @@ class RoomGroupMessageCreator @Inject constructor( inSpans(StyleSpan(Typeface.BOLD)) { append(roomName) append(": ") - event.senderName + append(event.senderName) append(" ") } append(event.description) 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 1a4d0bf36c..31dd913cea 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 @@ -51,7 +51,7 @@ class RoomGroupMessageCreatorTest { assertThat(resultMetaWithoutFormatting).isEqualTo( RoomNotification.Message.Meta( roomId = A_ROOM_ID, - summaryLine = "room-name: message-body", + summaryLine = "room-name: sender-name message-body", messageCount = 1, latestTimestamp = A_TIMESTAMP, shouldBing = false, @@ -77,7 +77,7 @@ class RoomGroupMessageCreatorTest { assertThat(resultMetaWithoutFormatting).isEqualTo( RoomNotification.Message.Meta( roomId = A_ROOM_ID, - summaryLine = "room-name: message-body", + summaryLine = "room-name: sender-name message-body", messageCount = 1, latestTimestamp = A_TIMESTAMP, shouldBing = true, @@ -129,7 +129,7 @@ class RoomGroupMessageCreatorTest { assertThat(resultMetaWithoutFormatting).isEqualTo( RoomNotification.Message.Meta( roomId = A_ROOM_ID, - summaryLine = "room-name: message-body", + summaryLine = "room-name: sender-name message-body", messageCount = 0, latestTimestamp = A_TIMESTAMP, shouldBing = false, From 7cb6740dd6604c57b25e3b6f1d6b45bdfe34352e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 27 Nov 2023 10:47:19 +0100 Subject: [PATCH 06/12] Remove useless try catch. --- .../notifications/RoomGroupMessageCreator.kt | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) 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 e667bd5936..cde4f489a2 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 @@ -30,7 +30,6 @@ import io.element.android.libraries.push.impl.notifications.debug.annotateForDeb import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.services.toolbox.api.strings.StringProvider -import timber.log.Timber import javax.inject.Inject class RoomGroupMessageCreator @Inject constructor( @@ -133,22 +132,16 @@ class RoomGroupMessageCreator @Inject constructor( } private fun createRoomMessagesGroupSummaryLine(events: List, roomName: String, roomIsDirect: Boolean): CharSequence { - return try { - when (events.size) { - 1 -> createFirstMessageSummaryLine(events.first(), roomName, roomIsDirect) - else -> { - stringProvider.getQuantityString( - R.plurals.notification_compat_summary_line_for_room, - events.size, - roomName, - events.size - ) - } + return when (events.size) { + 1 -> createFirstMessageSummaryLine(events.first(), roomName, roomIsDirect) + else -> { + stringProvider.getQuantityString( + R.plurals.notification_compat_summary_line_for_room, + events.size, + roomName, + events.size + ) } - } catch (e: Throwable) { - // String not found or bad format - Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER failed to resolve string") - roomName } } From 73ebffab2ebc57fc13ae18d52322d31586ccefbc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 27 Nov 2023 14:31:17 +0100 Subject: [PATCH 07/12] Add coverage for NotificationBitmapLoader --- gradle/libs.versions.toml | 1 + libraries/push/impl/build.gradle.kts | 1 + .../notifications/NotificationBitmapLoader.kt | 6 +- .../RoomGroupMessageCreatorTest.kt | 110 +++++++++++++++++- 4 files changed, 114 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7c0086a3e9..8b421d45de 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -136,6 +136,7 @@ test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = " coil = { module = "io.coil-kt:coil", version.ref = "coil" } coil_compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } coil_gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" } +coil_test = { module = "io.coil-kt:coil-test", version.ref = "coil" } compound = { module = "io.element.android:compound-android", version = "0.0.1" } datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" } serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_json" } diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 6c765fc44f..03ae56acd5 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -65,6 +65,7 @@ dependencies { testImplementation(libs.test.mockk) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) + testImplementation(libs.coil.test) testImplementation(libs.coroutines.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.services.appnavstate.test) 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 1ad38bf787..e78ce37639 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 @@ -27,11 +27,13 @@ import coil.transform.CircleCropTransformation import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.ui.media.MediaRequestData +import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import timber.log.Timber import javax.inject.Inject class NotificationBitmapLoader @Inject constructor( - @ApplicationContext private val context: Context + @ApplicationContext private val context: Context, + private val sdkIntProvider: BuildVersionSdkIntProvider, ) { /** @@ -65,7 +67,7 @@ class NotificationBitmapLoader @Inject constructor( * @param path mxc url */ suspend fun getUserIcon(path: String?): IconCompat? { - if (path == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + if (path == null || sdkIntProvider.get() < Build.VERSION_CODES.P) { return null } 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 31dd913cea..46c41454d3 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,12 +17,23 @@ 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.fixtures.aNotifiableMessageEvent +import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import io.element.android.services.toolbox.impl.strings.AndroidStringProvider +import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -30,6 +41,9 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment private const val A_TIMESTAMP = 6480L +private const val A_ROOM_AVATAR = "mxc://roomAvatar" +private const val A_USER_AVATAR_1 = "mxc://userAvatar1" +private const val A_USER_AVATAR_2 = "mxc://userAvatar2" @RunWith(RobolectricTestRunner::class) class RoomGroupMessageCreatorTest { @@ -85,6 +99,93 @@ class RoomGroupMessageCreatorTest { ) } + @Test + fun `test createRoomMessage with room avatar and sender avatar android O`() { + `test createRoomMessage with room avatar and sender avatar`( + api = Build.VERSION_CODES.O, + // Only the Room avatar is loaded + expectedCoilRequests = listOf( + MediaRequestData( + source = MediaSource(url = A_ROOM_AVATAR), + kind = MediaRequestData.Kind.Thumbnail(1024) + ) + ) + ) + } + + @OptIn(ExperimentalCoilApi::class) + @Test + fun `test createRoomMessage with room avatar and sender avatar android P`() = runTest { + `test createRoomMessage with room avatar and sender avatar`( + api = Build.VERSION_CODES.P, + // Room and user avatar are loaded + expectedCoilRequests = listOf( + MediaRequestData( + source = MediaSource(url = A_USER_AVATAR_1), + kind = MediaRequestData.Kind.Thumbnail(1024) + ), + MediaRequestData( + source = MediaSource(url = A_USER_AVATAR_2), + kind = MediaRequestData.Kind.Thumbnail(1024) + ), + MediaRequestData( + source = MediaSource(url = A_ROOM_AVATAR), + kind = MediaRequestData.Kind.Thumbnail(1024) + ), + ) + ) + } + + @OptIn(ExperimentalCoilApi::class) + private fun `test createRoomMessage with room avatar and sender avatar`( + 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 sut = createRoomGroupMessageCreator( + sdkIntProvider = FakeBuildVersionSdkIntProvider(api) + ) + val result = sut.createRoomMessage( + currentUser = aMatrixUser( + // Some user avatar + avatarUrl = A_USER_AVATAR_1, + ), + events = listOf( + aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( + roomAvatarPath = A_ROOM_AVATAR, + senderAvatarPath = A_USER_AVATAR_2, + ) + ), + roomId = A_ROOM_ID, + ) + val resultMetaWithoutFormatting = result.meta.copy( + summaryLine = result.meta.summaryLine.toString() + ) + assertThat(resultMetaWithoutFormatting).isEqualTo( + RoomNotification.Message.Meta( + roomId = A_ROOM_ID, + summaryLine = "room-name: sender-name message-body", + messageCount = 1, + latestTimestamp = A_TIMESTAMP, + shouldBing = false, + ) + ) + assertThat(coilRequests.toList()).isEqualTo(expectedCoilRequests) + } + @Test fun `test createRoomMessage with two Events`() = runTest { val sut = createRoomGroupMessageCreator() @@ -164,11 +265,16 @@ class RoomGroupMessageCreatorTest { } } -fun createRoomGroupMessageCreator(): RoomGroupMessageCreator { +fun createRoomGroupMessageCreator( + sdkIntProvider: BuildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.O), +): RoomGroupMessageCreator { val context = RuntimeEnvironment.getApplication() as Context return RoomGroupMessageCreator( notificationCreator = createNotificationCreator(), - bitmapLoader = NotificationBitmapLoader(RuntimeEnvironment.getApplication()), + bitmapLoader = NotificationBitmapLoader( + context = RuntimeEnvironment.getApplication(), + sdkIntProvider = sdkIntProvider, + ), stringProvider = AndroidStringProvider(context.resources) ) } From a09fa1c5adb3d38876897ffda4e1983a16d8b6b4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 27 Nov 2023 14:49:19 +0100 Subject: [PATCH 08/12] Add test for NotificationEventPersistence --- .../notifications/NotificationEventQueue.kt | 2 +- .../NotificationEventPersistenceTest.kt | 64 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistenceTest.kt diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt index 8eea6a9d5a..bb76de47d2 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt @@ -28,7 +28,7 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableMess import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent import timber.log.Timber -data class NotificationEventQueue constructor( +data class NotificationEventQueue( private val queue: MutableList, /** * An in memory FIFO cache of the seen events. diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistenceTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistenceTest.kt new file mode 100644 index 0000000000..79343278b9 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistenceTest.kt @@ -0,0 +1,64 @@ +/* + * 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 + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.core.cache.CircularCache +import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@RunWith(RobolectricTestRunner::class) +class NotificationEventPersistenceTest { + @Test + fun `loadEvents should return empty NotificationEventQueue`() { + val sut = createNotificationEventPersistence() + val result = sut.loadEvents( + factory = { rawEvents -> + NotificationEventQueue(rawEvents.toMutableList(), seenEventIds = CircularCache.create(cacheSize = 25)) + } + ) + assertThat(result.isEmpty()).isTrue() + } + + @Test + fun `after persisting NotificationEventQueue, loadEvents should return non-empty NotificationEventQueue`() { + val sut = createNotificationEventPersistence() + val notificationEventQueue = NotificationEventQueue(mutableListOf(), seenEventIds = CircularCache.create(cacheSize = 25)) + // First persist an empty queue + sut.persistEvents(notificationEventQueue) + // Add an event + notificationEventQueue.add(aSimpleNotifiableEvent()) + // Persist + // Note: is cannot work because AndroidKeyStore is not available. But we check that the code does + // not crash. + sut.persistEvents(notificationEventQueue) + sut.loadEvents( + factory = { rawEvents -> + NotificationEventQueue(rawEvents.toMutableList(), seenEventIds = CircularCache.create(cacheSize = 25)) + } + ) + // assertThat(result.isEmpty()).isFalse() + } + + private fun createNotificationEventPersistence(): NotificationEventPersistence { + val context = RuntimeEnvironment.getApplication() + return NotificationEventPersistence(context) + } +} From b61b45dd0c445e8d734f5c254e28550a766b5e7a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 27 Nov 2023 15:23:39 +0100 Subject: [PATCH 09/12] NotificationEventPersistence is now an interface, to allow in-memory implementation. --- .../DefaultNotificationEventPersistence.kt | 94 +++++++++++++++++++ .../NotificationEventPersistence.kt | 75 +-------------- ...efaultNotificationEventPersistenceTest.kt} | 10 +- 3 files changed, 103 insertions(+), 76 deletions(-) create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationEventPersistence.kt rename libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/{NotificationEventPersistenceTest.kt => DefaultNotificationEventPersistenceTest.kt} (87%) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationEventPersistence.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationEventPersistence.kt new file mode 100644 index 0000000000..3646837937 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationEventPersistence.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2021 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 + +import android.content.Context +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.androidutils.file.EncryptedFileFactory +import io.element.android.libraries.androidutils.file.safeDelete +import io.element.android.libraries.core.data.tryOrNull +import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import timber.log.Timber +import java.io.File +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import javax.inject.Inject + +private const val ROOMS_NOTIFICATIONS_FILE_NAME_LEGACY = "im.vector.notifications.cache" +private const val FILE_NAME = "notifications.bin" + +private val loggerTag = LoggerTag("NotificationEventPersistence", LoggerTag.NotificationLoggerTag) + +@ContributesBinding(AppScope::class) +class DefaultNotificationEventPersistence @Inject constructor( + @ApplicationContext private val context: Context, +) : NotificationEventPersistence { + private val file by lazy { + deleteLegacyFileIfAny() + context.getDatabasePath(FILE_NAME) + } + + private val encryptedFile by lazy { + EncryptedFileFactory(context).create(file) + } + + override fun loadEvents(factory: (List) -> NotificationEventQueue): NotificationEventQueue { + val rawEvents: ArrayList? = file + .takeIf { it.exists() } + ?.let { + try { + encryptedFile.openFileInput().use { fis -> + ObjectInputStream(fis).use { ois -> + @Suppress("UNCHECKED_CAST") + ois.readObject() as? ArrayList + } + }.also { + Timber.tag(loggerTag.value).d("Deserializing ${it?.size} NotifiableEvent(s)") + } + } catch (e: Throwable) { + Timber.tag(loggerTag.value).e(e, "## Failed to load cached notification info") + null + } + } + return factory(rawEvents.orEmpty()) + } + + override fun persistEvents(queuedEvents: NotificationEventQueue) { + Timber.tag(loggerTag.value).d("Serializing ${queuedEvents.rawEvents().size} NotifiableEvent(s)") + // Always delete file before writing, or encryptedFile.openFileOutput() will throw + file.safeDelete() + if (queuedEvents.isEmpty()) return + try { + encryptedFile.openFileOutput().use { fos -> + ObjectOutputStream(fos).use { oos -> + oos.writeObject(queuedEvents.rawEvents()) + } + } + } catch (e: Throwable) { + Timber.tag(loggerTag.value).e(e, "## Failed to save cached notification info") + } + } + + private fun deleteLegacyFileIfAny() { + tryOrNull { + File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME_LEGACY).delete() + } + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt index d1aee8c0b2..e593f49824 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * 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. @@ -16,76 +16,9 @@ package io.element.android.libraries.push.impl.notifications -import android.content.Context -import io.element.android.libraries.androidutils.file.EncryptedFileFactory -import io.element.android.libraries.androidutils.file.safeDelete -import io.element.android.libraries.core.data.tryOrNull -import io.element.android.libraries.core.log.logger.LoggerTag -import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent -import timber.log.Timber -import java.io.File -import java.io.ObjectInputStream -import java.io.ObjectOutputStream -import javax.inject.Inject -private const val ROOMS_NOTIFICATIONS_FILE_NAME_LEGACY = "im.vector.notifications.cache" -private const val FILE_NAME = "notifications.bin" - -private val loggerTag = LoggerTag("NotificationEventPersistence", LoggerTag.NotificationLoggerTag) - -class NotificationEventPersistence @Inject constructor( - @ApplicationContext private val context: Context, -) { - private val file by lazy { - deleteLegacyFileIfAny() - context.getDatabasePath(FILE_NAME) - } - - private val encryptedFile by lazy { - EncryptedFileFactory(context).create(file) - } - - fun loadEvents(factory: (List) -> NotificationEventQueue): NotificationEventQueue { - val rawEvents: ArrayList? = file - .takeIf { it.exists() } - ?.let { - try { - encryptedFile.openFileInput().use { fis -> - ObjectInputStream(fis).use { ois -> - @Suppress("UNCHECKED_CAST") - ois.readObject() as? ArrayList - } - }.also { - Timber.tag(loggerTag.value).d("Deserializing ${it?.size} NotifiableEvent(s)") - } - } catch (e: Throwable) { - Timber.tag(loggerTag.value).e(e, "## Failed to load cached notification info") - null - } - } - return factory(rawEvents.orEmpty()) - } - - fun persistEvents(queuedEvents: NotificationEventQueue) { - Timber.tag(loggerTag.value).d("Serializing ${queuedEvents.rawEvents().size} NotifiableEvent(s)") - // Always delete file before writing, or encryptedFile.openFileOutput() will throw - file.safeDelete() - if (queuedEvents.isEmpty()) return - try { - encryptedFile.openFileOutput().use { fos -> - ObjectOutputStream(fos).use { oos -> - oos.writeObject(queuedEvents.rawEvents()) - } - } - } catch (e: Throwable) { - Timber.tag(loggerTag.value).e(e, "## Failed to save cached notification info") - } - } - - private fun deleteLegacyFileIfAny() { - tryOrNull { - File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME_LEGACY).delete() - } - } +interface NotificationEventPersistence { + fun loadEvents(factory: (List) -> NotificationEventQueue): NotificationEventQueue + fun persistEvents(queuedEvents: NotificationEventQueue) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistenceTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationEventPersistenceTest.kt similarity index 87% rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistenceTest.kt rename to libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationEventPersistenceTest.kt index 79343278b9..ce984c712f 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistenceTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationEventPersistenceTest.kt @@ -25,10 +25,10 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment @RunWith(RobolectricTestRunner::class) -class NotificationEventPersistenceTest { +class DefaultNotificationEventPersistenceTest { @Test fun `loadEvents should return empty NotificationEventQueue`() { - val sut = createNotificationEventPersistence() + val sut = createDefaultNotificationEventPersistence() val result = sut.loadEvents( factory = { rawEvents -> NotificationEventQueue(rawEvents.toMutableList(), seenEventIds = CircularCache.create(cacheSize = 25)) @@ -39,7 +39,7 @@ class NotificationEventPersistenceTest { @Test fun `after persisting NotificationEventQueue, loadEvents should return non-empty NotificationEventQueue`() { - val sut = createNotificationEventPersistence() + val sut = createDefaultNotificationEventPersistence() val notificationEventQueue = NotificationEventQueue(mutableListOf(), seenEventIds = CircularCache.create(cacheSize = 25)) // First persist an empty queue sut.persistEvents(notificationEventQueue) @@ -57,8 +57,8 @@ class NotificationEventPersistenceTest { // assertThat(result.isEmpty()).isFalse() } - private fun createNotificationEventPersistence(): NotificationEventPersistence { + private fun createDefaultNotificationEventPersistence(): DefaultNotificationEventPersistence { val context = RuntimeEnvironment.getApplication() - return NotificationEventPersistence(context) + return DefaultNotificationEventPersistence(context) } } From a02c5c2b44a6a41eb7574b653d2ade53e3eede5c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 27 Nov 2023 16:42:52 +0100 Subject: [PATCH 10/12] Add test for DefaultNotificationDrawerManager --- libraries/push/impl/build.gradle.kts | 1 + .../DefaultNotificationDrawerManager.kt | 10 +- .../DefaultNotificationDrawerManagerTest.kt | 134 ++++++++++++++++++ .../InMemoryNotificationEventPersistence.kt | 33 +++++ .../test/FakeAppNavigationStateService.kt | 10 +- 5 files changed, 180 insertions(+), 8 deletions(-) create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/InMemoryNotificationEventPersistence.kt diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 03ae56acd5..f72a0886a0 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -68,6 +68,7 @@ dependencies { testImplementation(libs.coil.test) testImplementation(libs.coroutines.test) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.toolbox.impl) testImplementation(projects.services.toolbox.test) 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 f2e3240203..09f622e978 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 @@ -36,6 +36,7 @@ import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.NavigationState import io.element.android.services.appnavstate.api.currentSessionId import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -61,6 +62,8 @@ class DefaultNotificationDrawerManager @Inject constructor( private val buildMeta: BuildMeta, private val matrixClientProvider: MatrixClientProvider, ) : NotificationDrawerManager { + private var appNavigationStateObserver: Job? = null + /** * Lazily initializes the NotificationState as we rely on having a current session in order to fetch the persisted queue of events. */ @@ -72,12 +75,17 @@ class DefaultNotificationDrawerManager @Inject constructor( init { // Observe application state - coroutineScope.launch { + appNavigationStateObserver = coroutineScope.launch { appNavigationStateService.appNavigationState .collect { onAppNavigationStateChange(it.navigationState) } } } + // For test only + fun destroy() { + appNavigationStateObserver?.cancel() + } + private var currentAppNavigationState: NavigationState? = null private fun onAppNavigationStateChange(navigationState: NavigationState) { 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 new file mode 100644 index 0000000000..6b4ea24fd4 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt @@ -0,0 +1,134 @@ +/* + * 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 + +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.matrix.test.A_SPACE_ID +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.FakeRoomGroupMessageCreator +import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator +import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import io.element.android.services.appnavstate.api.AppNavigationState +import io.element.android.services.appnavstate.api.AppNavigationStateService +import io.element.android.services.appnavstate.api.NavigationState +import io.element.android.services.appnavstate.test.FakeAppNavigationStateService +import io.element.android.services.appnavstate.test.aNavigationState +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) +class DefaultNotificationDrawerManagerTest { + @Test + fun `clearAllEvents should have no effect when queue is empty`() = runTest { + val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager() + defaultNotificationDrawerManager.clearAllEvents(A_SESSION_ID) + defaultNotificationDrawerManager.destroy() + } + + @Test + fun `cover all APIs`() = runTest { + // For now just call all the API. Later, add more valuable tests. + val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager() + defaultNotificationDrawerManager.notificationStyleChanged() + defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID, doRender = true) + defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID, doRender = false) + defaultNotificationDrawerManager.clearEvent(A_SESSION_ID, AN_EVENT_ID, doRender = true) + defaultNotificationDrawerManager.clearEvent(A_SESSION_ID, AN_EVENT_ID, doRender = false) + defaultNotificationDrawerManager.clearMessagesForRoom(A_SESSION_ID, A_ROOM_ID, doRender = true) + defaultNotificationDrawerManager.clearMessagesForRoom(A_SESSION_ID, A_ROOM_ID, doRender = false) + defaultNotificationDrawerManager.clearMembershipNotificationForSession(A_SESSION_ID) + defaultNotificationDrawerManager.clearMembershipNotificationForRoom(A_SESSION_ID, A_ROOM_ID, doRender = true) + defaultNotificationDrawerManager.clearMembershipNotificationForRoom(A_SESSION_ID, A_ROOM_ID, doRender = false) + defaultNotificationDrawerManager.onNotifiableEventReceived(aNotifiableMessageEvent()) + // Add the same Event again (will be ignored) + defaultNotificationDrawerManager.onNotifiableEventReceived(aNotifiableMessageEvent()) + defaultNotificationDrawerManager.destroy() + } + + @Test + fun `react to applicationStateChange`() = runTest { + // For now just call all the API. Later, add more valuable tests. + val appNavigationStateFlow: MutableStateFlow = MutableStateFlow( + AppNavigationState( + navigationState = NavigationState.Root, + isInForeground = true, + ) + ) + val appNavigationStateService = FakeAppNavigationStateService(appNavigationState = appNavigationStateFlow) + val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager( + appNavigationStateService = appNavigationStateService + ) + appNavigationStateFlow.emit(AppNavigationState(aNavigationState(), isInForeground = true)) + runCurrent() + appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID), isInForeground = true)) + runCurrent() + appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_SPACE_ID), isInForeground = true)) + runCurrent() + appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID), isInForeground = true)) + runCurrent() + appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID, A_THREAD_ID), isInForeground = true)) + runCurrent() + // Like a user sign out + appNavigationStateFlow.emit(AppNavigationState(aNavigationState(), isInForeground = true)) + runCurrent() + defaultNotificationDrawerManager.destroy() + } + + private fun TestScope.createDefaultNotificationDrawerManager( + appNavigationStateService: AppNavigationStateService = FakeAppNavigationStateService(), + initialData: List = emptyList() + ): DefaultNotificationDrawerManager { + val context = RuntimeEnvironment.getApplication() + return DefaultNotificationDrawerManager( + notifiableEventProcessor = NotifiableEventProcessor( + outdatedDetector = OutdatedEventDetector(), + appNavigationStateService = appNavigationStateService + ), + notificationRenderer = NotificationRenderer( + NotificationIdProvider(), + NotificationDisplayer(context), + NotificationFactory( + FakeAndroidNotificationFactory().instance, + FakeRoomGroupMessageCreator().instance, + FakeSummaryGroupMessageCreator().instance, + ) + ), + notificationEventPersistence = InMemoryNotificationEventPersistence(initialData = initialData), + filteredEventDetector = FilteredEventDetector(), + appNavigationStateService = appNavigationStateService, + coroutineScope = this, + dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + buildMeta = aBuildMeta(), + matrixClientProvider = FakeMatrixClientProvider(), + ) + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/InMemoryNotificationEventPersistence.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/InMemoryNotificationEventPersistence.kt new file mode 100644 index 0000000000..09f907f50b --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/InMemoryNotificationEventPersistence.kt @@ -0,0 +1,33 @@ +/* + * 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 + +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent + +class InMemoryNotificationEventPersistence( + initialData: List = emptyList() +) : NotificationEventPersistence { + private var data: List = initialData + + override fun loadEvents(factory: (List) -> NotificationEventQueue): NotificationEventQueue { + return factory(data) + } + + override fun persistEvents(queuedEvents: NotificationEventQueue) { + data = queuedEvents.rawEvents() + } +} diff --git a/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeAppNavigationStateService.kt b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeAppNavigationStateService.kt index a09e2a9c5e..ac6060fa8b 100644 --- a/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeAppNavigationStateService.kt +++ b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeAppNavigationStateService.kt @@ -20,23 +20,19 @@ 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.SpaceId import io.element.android.libraries.matrix.api.core.ThreadId -import io.element.android.services.appnavstate.api.NavigationState -import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.AppNavigationState +import io.element.android.services.appnavstate.api.AppNavigationStateService +import io.element.android.services.appnavstate.api.NavigationState import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow class FakeAppNavigationStateService( - private val fakeAppNavigationState: MutableStateFlow = MutableStateFlow( + override val appNavigationState: MutableStateFlow = MutableStateFlow( AppNavigationState( navigationState = NavigationState.Root, isInForeground = true, ) ), ) : AppNavigationStateService { - - override val appNavigationState: StateFlow = fakeAppNavigationState - override fun onNavigateToSession(owner: String, sessionId: SessionId) = Unit override fun onLeavingSession(owner: String) = Unit From 886627d633d319583bd31d7899201035c3a03a5a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 27 Nov 2023 17:13:59 +0100 Subject: [PATCH 11/12] Increase `Global minimum code coverage` boundaries. --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a2dbec823c..e29f7c393b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -236,11 +236,11 @@ koverMerged { name = "Global minimum code coverage." target = kotlinx.kover.api.VerificationTarget.ALL bound { - minValue = 60 + minValue = 65 // Setting a max value, so that if coverage is bigger, it means that we have to change minValue. // For instance if we have minValue = 20 and maxValue = 30, and current code coverage is now 31.32%, update // minValue to 25 and maxValue to 35. - maxValue = 70 + maxValue = 75 counter = kotlinx.kover.api.CounterType.INSTRUCTION valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE } From 9afba239bba670ba82a4d78d2d22f716146cf1cd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 27 Nov 2023 17:17:10 +0100 Subject: [PATCH 12/12] Fix: This number should be separated by underscores in order to increase readability --- .../notifications/factories/NotificationCreatorTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreatorTest.kt index 18d1fb723b..edf87fcd1a 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreatorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreatorTest.kt @@ -188,7 +188,7 @@ class NotificationCreatorTest { style = null, compatSummary = "compatSummary", noisy = false, - lastMessageTimestamp = 123456L, + lastMessageTimestamp = 123_456L, ) result.commonAssertions( expectedGroup = matrixUser.userId.value, @@ -204,7 +204,7 @@ class NotificationCreatorTest { style = null, compatSummary = "compatSummary", noisy = true, - lastMessageTimestamp = 123456L, + lastMessageTimestamp = 123_456L, ) result.commonAssertions( expectedGroup = matrixUser.userId.value, @@ -233,7 +233,7 @@ class NotificationCreatorTest { ), threadId = null, largeIcon = null, - lastMessageTimestamp = 123456L, + lastMessageTimestamp = 123_456L, tickerText = "tickerText", ) result.commonAssertions() @@ -261,7 +261,7 @@ class NotificationCreatorTest { ), threadId = A_THREAD_ID, largeIcon = null, - lastMessageTimestamp = 123456L, + lastMessageTimestamp = 123_456L, tickerText = "tickerText", ) result.commonAssertions()