From 2a509d9ea8766a56aac41e2c02a15766ced837b6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 9 Feb 2026 14:23:06 +0100 Subject: [PATCH 01/12] Sort audio device by device type before sending the list to Element Call. --- .../call/impl/utils/WebViewAudioManager.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt index d4811d65d5..e27084f707 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt @@ -82,6 +82,13 @@ class WebViewAudioManager( AudioDeviceInfo.TYPE_BUILTIN_EARPIECE, ) + private val audioDeviceComparator = Comparator { a, b -> + // If the device type is not in the wantedDeviceTypes list, we give it a high index, (i.e. low priority) + val indexOfA = wantedDeviceTypes.indexOf(a.type).let { if (it == -1) Int.MAX_VALUE else it } + val indexOfB = wantedDeviceTypes.indexOf(b.type).let { if (it == -1) Int.MAX_VALUE else it } + indexOfA.compareTo(indexOfB) + } + private val audioManager = webView.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager /** @@ -134,7 +141,7 @@ class WebViewAudioManager( if (validNewDevices.isEmpty()) return // We need to calculate the available devices ourselves, since calling `listAudioDevices` will return an outdated list - val audioDevices = (listAudioDevices() + validNewDevices).distinctBy { it.id } + val audioDevices = (listAudioDevices() + validNewDevices).distinctBy { it.id }.sortedWith(audioDeviceComparator) setAvailableAudioDevices(audioDevices.map(SerializableAudioDevice::fromAudioDeviceInfo)) // This should automatically switch to a new device if it has a higher priority than the current one selectDefaultAudioDevice(audioDevices) @@ -294,7 +301,7 @@ class WebViewAudioManager( } /** - * Returns the list of available audio devices. + * Returns the list of available audio devices, sorted by likelihood of it being used for communication. * * On Android 11 ([Build.VERSION_CODES.R]) and lower, it returns the list of output devices as a fallback. */ @@ -304,7 +311,7 @@ class WebViewAudioManager( } else { val rawAudioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) rawAudioDevices.filter { it.type in wantedDeviceTypes && it.isSink } - } + }.sortedWith(audioDeviceComparator) } /** @@ -323,19 +330,12 @@ class WebViewAudioManager( } /** - * Selects the default audio device based on the available devices. + * Selects the default audio device based on the sorted available devices. * * @param availableDevices The list of available audio devices to select from. If not provided, it will use the current list of audio devices. */ private fun selectDefaultAudioDevice(availableDevices: List = listAudioDevices()) { - val selectedDevice = availableDevices - .minByOrNull { - wantedDeviceTypes.indexOf(it.type).let { index -> - // If the device type is not in the wantedDeviceTypes list, we give it a low priority - if (index == -1) Int.MAX_VALUE else index - } - } - + val selectedDevice = availableDevices.firstOrNull() expectedNewCommunicationDeviceId = selectedDevice?.id audioManager.selectAudioDevice(selectedDevice) From cd9a1febc4a44b98b4e94e99089e6191e5f0c558 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Feb 2026 15:36:12 +0100 Subject: [PATCH 02/12] Cleanup: remove unused `summaryLine` field. --- .../notifications/NotificationDataFactory.kt | 52 ------------------- .../DefaultSummaryGroupMessageCreatorTest.kt | 1 - .../NotificationDataFactoryTest.kt | 4 -- .../notifications/NotificationRendererTest.kt | 2 +- 4 files changed, 1 insertion(+), 58 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt index 5489fa0894..6a84f8df8f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt @@ -9,17 +9,12 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification -import android.graphics.Typeface -import android.text.style.StyleSpan -import androidx.core.text.buildSpannedString -import androidx.core.text.inSpans import coil3.ImageLoader import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding 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.R import io.element.android.libraries.push.impl.notifications.factories.NotificationAccountParams import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent @@ -98,7 +93,6 @@ class DefaultNotificationDataFactory( notification = notification, roomId = roomId, threadId = threadId, - summaryLine = createRoomMessagesGroupSummaryLine(events, roomName, isDm), messageCount = events.size, latestTimestamp = events.maxOf { it.timestamp }, shouldBing = events.any { it.noisy } @@ -123,7 +117,6 @@ class DefaultNotificationDataFactory( OneShotNotification( tag = event.roomId.value, notification = notificationCreator.createRoomInvitationNotification(notificationAccountParams, event), - summaryLine = event.description, isNoisy = event.noisy, timestamp = event.timestamp ) @@ -140,7 +133,6 @@ class DefaultNotificationDataFactory( OneShotNotification( tag = event.eventId.value, notification = notificationCreator.createSimpleEventNotification(notificationAccountParams, event), - summaryLine = event.description, isNoisy = event.noisy, timestamp = event.timestamp ) @@ -157,7 +149,6 @@ class DefaultNotificationDataFactory( OneShotNotification( tag = event.eventId.value, notification = notificationCreator.createFallbackNotification(notificationAccountParams, event), - summaryLine = event.description.orEmpty(), isNoisy = false, timestamp = event.timestamp ) @@ -184,53 +175,12 @@ class DefaultNotificationDataFactory( ) } } - - private fun createRoomMessagesGroupSummaryLine(events: List, roomName: String, roomIsDm: Boolean): CharSequence { - return when (events.size) { - 1 -> createFirstMessageSummaryLine(events.first(), roomName, roomIsDm) - else -> { - stringProvider.getQuantityString( - R.plurals.notification_compat_summary_line_for_room, - events.size, - roomName, - events.size - ) - } - } - } - - private fun createFirstMessageSummaryLine(event: NotifiableMessageEvent, roomName: String, roomIsDm: Boolean): CharSequence { - return if (roomIsDm) { - buildSpannedString { - event.senderDisambiguatedDisplayName?.let { - inSpans(StyleSpan(Typeface.BOLD)) { - append(it) - append(": ") - } - } - append(event.description) - } - } else { - buildSpannedString { - inSpans(StyleSpan(Typeface.BOLD)) { - append(roomName) - append(": ") - event.senderDisambiguatedDisplayName?.let { - append(it) - append(" ") - } - } - append(event.description) - } - } - } } data class RoomNotification( val notification: Notification, val roomId: RoomId, val threadId: ThreadId?, - val summaryLine: CharSequence, val messageCount: Int, val latestTimestamp: Long, val shouldBing: Boolean, @@ -239,7 +189,6 @@ data class RoomNotification( return notification == other.notification && roomId == other.roomId && threadId == other.threadId && - summaryLine.toString() == other.summaryLine.toString() && messageCount == other.messageCount && latestTimestamp == other.latestTimestamp && shouldBing == other.shouldBing @@ -249,7 +198,6 @@ data class RoomNotification( data class OneShotNotification( val notification: Notification, val tag: String, - val summaryLine: CharSequence, val isNoisy: Boolean, val timestamp: Long, ) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultSummaryGroupMessageCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultSummaryGroupMessageCreatorTest.kt index ba17fd0683..01537fe09a 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultSummaryGroupMessageCreatorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultSummaryGroupMessageCreatorTest.kt @@ -39,7 +39,6 @@ class DefaultSummaryGroupMessageCreatorTest { RoomNotification( notification = Notification(), roomId = A_ROOM_ID, - summaryLine = "", messageCount = 1, latestTimestamp = A_FAKE_TIMESTAMP + 10, shouldBing = true, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt index 7d9d1e6550..2cd066ea71 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt @@ -64,7 +64,6 @@ class NotificationDataFactoryTest { OneShotNotification( notification = expectedNotification, tag = A_ROOM_ID.value, - summaryLine = AN_INVITATION_EVENT.description, isNoisy = AN_INVITATION_EVENT.noisy, timestamp = AN_INVITATION_EVENT.timestamp ) @@ -83,7 +82,6 @@ class NotificationDataFactoryTest { OneShotNotification( notification = expectedNotification, tag = AN_EVENT_ID.value, - summaryLine = A_SIMPLE_EVENT.description, isNoisy = A_SIMPLE_EVENT.noisy, timestamp = AN_INVITATION_EVENT.timestamp ) @@ -105,7 +103,6 @@ class NotificationDataFactoryTest { existingNotification = null, ), roomId = A_ROOM_ID, - summaryLine = "A room name: Bob Hello world!", messageCount = events.size, latestTimestamp = events.maxOf { it.timestamp }, shouldBing = events.any { it.noisy }, @@ -161,7 +158,6 @@ class NotificationDataFactoryTest { existingNotification = null, ), roomId = A_ROOM_ID, - summaryLine = "A room name: Bob Hello world!", messageCount = withRedactedRemoved.size, latestTimestamp = withRedactedRemoved.maxOf { it.timestamp }, shouldBing = withRedactedRemoved.any { it.noisy }, 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 51d491f444..957d199138 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 @@ -43,7 +43,7 @@ private const val USE_COMPLETE_NOTIFICATION_FORMAT = true private val A_SUMMARY_NOTIFICATION = SummaryNotification.Update(A_NOTIFICATION) private val ONE_SHOT_NOTIFICATION = - OneShotNotification(notification = A_NOTIFICATION, tag = "ignored", summaryLine = "ignored", isNoisy = false, timestamp = -1) + OneShotNotification(notification = A_NOTIFICATION, tag = "ignored", isNoisy = false, timestamp = -1) @RunWith(RobolectricTestRunner::class) class NotificationRendererTest { From fe7e67c683a04665f0d09d62d71617ad7f989a06 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Feb 2026 16:43:03 +0100 Subject: [PATCH 03/12] Notification: implement a counter in the fallback notification. --- .../ActiveNotificationsProvider.kt | 6 ++ .../FallbackNotificationFactory.kt | 5 +- .../notifications/NotificationDataFactory.kt | 36 +++++------ .../notifications/NotificationRenderer.kt | 12 ++-- .../SummaryGroupMessageCreator.kt | 2 - .../factories/NotificationCreator.kt | 59 +++++++++++-------- .../impl/src/main/res/values/localazy.xml | 4 ++ .../DefaultActiveNotificationsProviderTest.kt | 13 ++++ .../DefaultNotifiableEventResolverTest.kt | 4 +- .../DefaultNotificationDrawerManagerTest.kt | 2 - .../DefaultSummaryGroupMessageCreatorTest.kt | 1 - .../NotificationDataFactoryTest.kt | 2 - .../notifications/NotificationRendererTest.kt | 2 - .../DefaultNotificationCreatorTest.kt | 27 +++++---- .../fake/FakeActiveNotificationsProvider.kt | 5 ++ .../fake/FakeNotificationCreator.kt | 14 +++-- .../fake/FakeNotificationDataFactory.kt | 17 +++--- .../fake/FakeSummaryGroupMessageCreator.kt | 10 ++-- .../push/impl/push/DefaultPushHandlerTest.kt | 2 - 19 files changed, 126 insertions(+), 97 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt index 4cc279b8bb..fd2788279b 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt @@ -34,6 +34,7 @@ interface ActiveNotificationsProvider { fun getMembershipNotificationForSession(sessionId: SessionId): List fun getMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId): List fun getSummaryNotification(sessionId: SessionId): StatusBarNotification? + fun getFallbackNotification(sessionId: SessionId): StatusBarNotification? fun count(sessionId: SessionId): Int } @@ -76,6 +77,11 @@ class DefaultActiveNotificationsProvider( return getNotificationsForSession(sessionId).find { it.id == summaryId } } + override fun getFallbackNotification(sessionId: SessionId): StatusBarNotification? { + val fallbackId = NotificationIdProvider.getFallbackNotificationId(sessionId) + return getNotificationsForSession(sessionId).find { it.id == fallbackId } + } + override fun count(sessionId: SessionId): Int { return getNotificationsForSession(sessionId).size } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FallbackNotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FallbackNotificationFactory.kt index 7b06aae39f..970f19fd45 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FallbackNotificationFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FallbackNotificationFactory.kt @@ -12,15 +12,12 @@ import dev.zacsweers.metro.Inject import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent -import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.systemclock.SystemClock @Inject class FallbackNotificationFactory( private val clock: SystemClock, - private val stringProvider: StringProvider, ) { fun create( sessionId: SessionId, @@ -36,7 +33,7 @@ class FallbackNotificationFactory( isRedacted = false, isUpdated = false, timestamp = clock.epochMillis(), - description = stringProvider.getString(R.string.notification_fallback_content), + description = "", cause = cause, ) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt index 6a84f8df8f..f44ea41c45 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt @@ -21,7 +21,6 @@ import io.element.android.libraries.push.impl.notifications.model.FallbackNotifi import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent -import io.element.android.services.toolbox.api.strings.StringProvider interface NotificationDataFactory { suspend fun toNotifications( @@ -46,16 +45,15 @@ interface NotificationDataFactory { @JvmName("toNotificationFallbackEvents") @Suppress("INAPPLICABLE_JVM_NAME") - fun toNotifications( + fun toNotification( fallback: List, notificationAccountParams: NotificationAccountParams, - ): List + ): OneShotNotification? fun createSummaryNotification( roomNotifications: List, invitationNotifications: List, simpleNotifications: List, - fallbackNotifications: List, notificationAccountParams: NotificationAccountParams, ): SummaryNotification } @@ -66,7 +64,6 @@ class DefaultNotificationDataFactory( private val roomGroupMessageCreator: RoomGroupMessageCreator, private val summaryGroupMessageCreator: SummaryGroupMessageCreator, private val activeNotificationsProvider: ActiveNotificationsProvider, - private val stringProvider: StringProvider, ) : NotificationDataFactory { override suspend fun toNotifications( messages: List, @@ -141,25 +138,31 @@ class DefaultNotificationDataFactory( @JvmName("toNotificationFallbackEvents") @Suppress("INAPPLICABLE_JVM_NAME") - override fun toNotifications( + override fun toNotification( fallback: List, notificationAccountParams: NotificationAccountParams, - ): List { - return fallback.map { event -> - OneShotNotification( - tag = event.eventId.value, - notification = notificationCreator.createFallbackNotification(notificationAccountParams, event), - isNoisy = false, - timestamp = event.timestamp - ) - } + ): OneShotNotification? { + if (fallback.isEmpty()) return null + val existingNotification = activeNotificationsProvider + .getFallbackNotification(notificationAccountParams.user.userId) + ?.notification + val notification = notificationCreator.createFallbackNotification( + existingNotification, + notificationAccountParams, + fallback, + ) + return OneShotNotification( + tag = "FALLBACK", + notification = notification, + isNoisy = false, + timestamp = fallback.first().timestamp + ) } override fun createSummaryNotification( roomNotifications: List, invitationNotifications: List, simpleNotifications: List, - fallbackNotifications: List, notificationAccountParams: NotificationAccountParams, ): SummaryNotification { return when { @@ -169,7 +172,6 @@ class DefaultNotificationDataFactory( roomNotifications = roomNotifications, invitationNotifications = invitationNotifications, simpleNotifications = simpleNotifications, - fallbackNotifications = fallbackNotifications, notificationAccountParams = notificationAccountParams, ) ) 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 26769f09ba..c26a495ce8 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 @@ -55,12 +55,11 @@ class NotificationRenderer( val roomNotifications = notificationDataFactory.toNotifications(groupedEvents.roomEvents, imageLoader, notificationAccountParams) val invitationNotifications = notificationDataFactory.toNotifications(groupedEvents.invitationEvents, notificationAccountParams) val simpleNotifications = notificationDataFactory.toNotifications(groupedEvents.simpleEvents, notificationAccountParams) - val fallbackNotifications = notificationDataFactory.toNotifications(groupedEvents.fallbackEvents, notificationAccountParams) + val fallbackNotification = notificationDataFactory.toNotification(groupedEvents.fallbackEvents, notificationAccountParams) val summaryNotification = notificationDataFactory.createSummaryNotification( roomNotifications = roomNotifications, invitationNotifications = invitationNotifications, simpleNotifications = simpleNotifications, - fallbackNotifications = fallbackNotifications, notificationAccountParams = notificationAccountParams, ) @@ -107,13 +106,12 @@ class NotificationRenderer( } } - // Show only the first fallback notification - if (fallbackNotifications.isNotEmpty()) { - Timber.tag(loggerTag.value).d("Showing fallback notification") + if (fallbackNotification != null) { + Timber.tag(loggerTag.value).d("Showing or updating fallback notification") notificationDisplayer.showNotification( - tag = "FALLBACK", + tag = fallbackNotification.tag, id = NotificationIdProvider.getFallbackNotificationId(currentUser.userId), - notification = fallbackNotifications.first().notification + notification = fallbackNotification.notification, ) } 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 f7f0c057c6..d283adae71 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 @@ -22,7 +22,6 @@ interface SummaryGroupMessageCreator { roomNotifications: List, invitationNotifications: List, simpleNotifications: List, - fallbackNotifications: List, ): Notification } @@ -45,7 +44,6 @@ class DefaultSummaryGroupMessageCreator( roomNotifications: List, invitationNotifications: List, simpleNotifications: List, - fallbackNotifications: List, ): Notification { val summaryIsNoisy = roomNotifications.any { it.shouldBing } || invitationNotifications.any { it.isNoisy } || diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt index 9533f6b0ac..9707a35398 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt @@ -75,8 +75,9 @@ interface NotificationCreator { ): Notification fun createFallbackNotification( + existingNotification: Notification?, notificationAccountParams: NotificationAccountParams, - fallbackNotifiableEvent: FallbackNotifiableEvent, + fallbackNotifiableEvents: List, ): Notification /** @@ -240,11 +241,13 @@ class DefaultNotificationCreator( .addAction(rejectInvitationActionFactory.create(inviteNotifiableEvent)) .addAction(acceptInvitationActionFactory.create(inviteNotifiableEvent)) // Build the pending intent for when the notification is clicked - .setContentIntent(pendingIntentFactory.createOpenRoomPendingIntent( - sessionId = inviteNotifiableEvent.sessionId, - roomId = inviteNotifiableEvent.roomId, - eventId = null, - )) + .setContentIntent( + pendingIntentFactory.createOpenRoomPendingIntent( + sessionId = inviteNotifiableEvent.sessionId, + roomId = inviteNotifiableEvent.roomId, + eventId = null, + ) + ) .apply { if (inviteNotifiableEvent.noisy) { // Compat @@ -276,12 +279,14 @@ class DefaultNotificationCreator( .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .configureWith(notificationAccountParams) .setAutoCancel(true) - .setContentIntent(pendingIntentFactory.createOpenRoomPendingIntent( - sessionId = simpleNotifiableEvent.sessionId, - roomId = simpleNotifiableEvent.roomId, - eventId = null, - extras = bundleOf(ROOM_OPENED_FROM_NOTIFICATION to true), - )) + .setContentIntent( + pendingIntentFactory.createOpenRoomPendingIntent( + sessionId = simpleNotifiableEvent.sessionId, + roomId = simpleNotifiableEvent.roomId, + eventId = null, + extras = bundleOf(ROOM_OPENED_FROM_NOTIFICATION to true), + ) + ) .apply { if (simpleNotifiableEvent.noisy) { // Compat @@ -295,28 +300,35 @@ class DefaultNotificationCreator( } override fun createFallbackNotification( + existingNotification: Notification?, notificationAccountParams: NotificationAccountParams, - fallbackNotifiableEvent: FallbackNotifiableEvent, + fallbackNotifiableEvents: List, ): Notification { val channelId = notificationChannels.getChannelIdForMessage(false) + val existingCounter = existingNotification + ?.extras + ?.getInt(FALLBACK_COUNTER_EXTRA) + ?: 0 + val counter = existingCounter + fallbackNotifiableEvents.size + val fallbackNotifiableEvent = fallbackNotifiableEvents.first() return NotificationCompat.Builder(context, channelId) .setOnlyAlertOnce(true) .setContentTitle(buildMeta.applicationName.annotateForDebug(7)) - .setContentText(fallbackNotifiableEvent.description.orEmpty().annotateForDebug(8)) + .setContentText( + stringProvider.getQuantityString(R.plurals.notification_fallback_n_content, counter, counter) + .annotateForDebug(8) + ) + .setExtras( + bundleOf( + FALLBACK_COUNTER_EXTRA to counter + ) + ) + .setNumber(counter) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .configureWith(notificationAccountParams) .setAutoCancel(true) .setWhen(fallbackNotifiableEvent.timestamp) - // Ideally we'd use `createOpenRoomPendingIntent` here, but the broken notification might apply to an invite - // and the user won't have access to the room yet, resulting in an error screen. .setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(fallbackNotifiableEvent.sessionId)) - .setDeleteIntent( - pendingIntentFactory.createDismissEventPendingIntent( - fallbackNotifiableEvent.sessionId, - fallbackNotifiableEvent.roomId, - fallbackNotifiableEvent.eventId - ) - ) .setPriority(NotificationCompat.PRIORITY_LOW) .build() } @@ -503,6 +515,7 @@ class DefaultNotificationCreator( companion object { const val MESSAGE_EVENT_ID = "message_event_id" + private const val FALLBACK_COUNTER_EXTRA = "COUNTER" } } diff --git a/libraries/push/impl/src/main/res/values/localazy.xml b/libraries/push/impl/src/main/res/values/localazy.xml index aac5c6c72b..15afb93d3f 100644 --- a/libraries/push/impl/src/main/res/values/localazy.xml +++ b/libraries/push/impl/src/main/res/values/localazy.xml @@ -15,6 +15,10 @@ "The UnifiedPush notification distributor couldn\'t be registered, so you will not receive notifications anymore. Please check the notifications settings of the app and the status of the push distributor." "You have new messages." + + "You have %d new message." + "You have %d new messages." + "📹 Incoming call" "** Failed to send - please open room" "Join" diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt index 573ec37471..04a65073f6 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt @@ -153,6 +153,19 @@ class DefaultActiveNotificationsProviderTest { assertThat(activeNotificationsProvider.getSummaryNotification(A_SESSION_ID_2)).isNull() } + @Test + fun `getFallbackNotification returns only the fallback notification for that session id if it exists`() { + val activeNotifications = listOf( + aStatusBarNotification(id = notificationIdProvider.getFallbackNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value), + aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value), + aStatusBarNotification(id = notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value), + ) + val activeNotificationsProvider = createActiveNotificationsProvider(activeNotifications = activeNotifications) + + assertThat(activeNotificationsProvider.getFallbackNotification(A_SESSION_ID)).isNotNull() + assertThat(activeNotificationsProvider.getFallbackNotification(A_SESSION_ID_2)).isNull() + } + private fun aStatusBarNotification(id: Int, groupId: String, tag: String? = null) = mockk { every { this@mockk.id } returns id every { this@mockk.tag } returns tag diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt index 58f97e55cb..ba58db2a3c 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt @@ -56,7 +56,6 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableMess import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent import io.element.android.libraries.push.test.notifications.FakeCallNotificationEventResolver import io.element.android.services.toolbox.impl.strings.AndroidStringProvider -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 kotlinx.coroutines.test.runTest @@ -663,7 +662,7 @@ class DefaultNotifiableEventResolverTest { roomId = A_ROOM_ID, eventId = AN_EVENT_ID, editedEventId = null, - description = "You have new messages.", + description = "", canBeReplaced = true, isRedacted = false, isUpdated = false, @@ -895,7 +894,6 @@ class DefaultNotifiableEventResolverTest { callNotificationEventResolver = callNotificationEventResolver, fallbackNotificationFactory = FallbackNotificationFactory( clock = FakeSystemClock(), - stringProvider = FakeStringProvider(defaultResult = "You have new messages.") ), featureFlagService = FakeFeatureFlagService(), ) 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 58bb86e683..608c45e9b8 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 @@ -37,7 +37,6 @@ 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.services.toolbox.test.strings.FakeStringProvider import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -224,7 +223,6 @@ class DefaultNotificationDrawerManagerTest { roomGroupMessageCreator = roomGroupMessageCreator, summaryGroupMessageCreator = summaryGroupMessageCreator, activeNotificationsProvider = activeNotificationsProvider, - stringProvider = FakeStringProvider(), ), enterpriseService = enterpriseService, sessionStore = sessionStore, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultSummaryGroupMessageCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultSummaryGroupMessageCreatorTest.kt index 01537fe09a..0973ee69ce 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultSummaryGroupMessageCreatorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultSummaryGroupMessageCreatorTest.kt @@ -47,7 +47,6 @@ class DefaultSummaryGroupMessageCreatorTest { ), invitationNotifications = emptyList(), simpleNotifications = emptyList(), - fallbackNotifications = emptyList(), ) notificationCreator.createSummaryListNotificationResult.assertions() diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt index 2cd066ea71..c07c1ff89e 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt @@ -23,7 +23,6 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGrou import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent -import io.element.android.services.toolbox.test.strings.FakeStringProvider import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -47,7 +46,6 @@ class NotificationDataFactoryTest { roomGroupMessageCreator = fakeRoomGroupMessageCreator, summaryGroupMessageCreator = fakeSummaryGroupMessageCreator, activeNotificationsProvider = activeNotificationsProvider, - stringProvider = FakeStringProvider(), ) @Test 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 957d199138..db292fcc61 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 @@ -29,7 +29,6 @@ import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNot import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore -import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value import kotlinx.coroutines.test.runTest @@ -57,7 +56,6 @@ class NotificationRendererTest { roomGroupMessageCreator = roomGroupMessageCreator, summaryGroupMessageCreator = summaryGroupMessageCreator, activeNotificationsProvider = FakeActiveNotificationsProvider(), - stringProvider = FakeStringProvider(), ) private val notificationIdProvider = NotificationIdProvider diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt index 0504ae433b..a346aa839f 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt @@ -82,19 +82,22 @@ class DefaultNotificationCreatorTest { fun `test createFallbackNotification`() { val sut = createNotificationCreator() val result = sut.createFallbackNotification( + existingNotification = null, notificationAccountParams = aNotificationAccountParams(), - 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, - cause = null, - ), + fallbackNotifiableEvents = listOf( + 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, + cause = null, + ), + ) ) result.commonAssertions( expectedCategory = null, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeActiveNotificationsProvider.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeActiveNotificationsProvider.kt index ae3edce437..2b374e5ac1 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeActiveNotificationsProvider.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeActiveNotificationsProvider.kt @@ -21,6 +21,7 @@ class FakeActiveNotificationsProvider( private val getMembershipNotificationForSessionResult: (SessionId) -> List = { emptyList() }, private val getMembershipNotificationForRoomResult: (SessionId, RoomId) -> List = { _, _ -> emptyList() }, private val getSummaryNotificationResult: (SessionId) -> StatusBarNotification? = { null }, + private val getFallbackNotificationResult: (SessionId) -> StatusBarNotification? = { null }, private val countResult: (SessionId) -> Int = { 0 }, ) : ActiveNotificationsProvider { override fun getMessageNotificationsForRoom(sessionId: SessionId, roomId: RoomId, threadId: ThreadId?): List { @@ -47,6 +48,10 @@ class FakeActiveNotificationsProvider( return getSummaryNotificationResult(sessionId) } + override fun getFallbackNotification(sessionId: SessionId): StatusBarNotification? { + return getFallbackNotificationResult(sessionId) + } + override fun count(sessionId: SessionId): Int { return countResult(sessionId) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationCreator.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationCreator.kt index d5e4ad9695..33071ef1ac 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationCreator.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationCreator.kt @@ -24,6 +24,7 @@ import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiab import io.element.android.tests.testutils.lambda.LambdaFiveParamsRecorder import io.element.android.tests.testutils.lambda.LambdaListAnyParamsRecorder import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder +import io.element.android.tests.testutils.lambda.LambdaThreeParamsRecorder import io.element.android.tests.testutils.lambda.LambdaTwoParamsRecorder import io.element.android.tests.testutils.lambda.lambdaAnyRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -34,8 +35,8 @@ class FakeNotificationCreator( lambdaRecorder { _, _ -> A_NOTIFICATION }, var createSimpleNotificationResult: LambdaTwoParamsRecorder = lambdaRecorder { _, _ -> A_NOTIFICATION }, - var createFallbackNotificationResult: LambdaTwoParamsRecorder = - lambdaRecorder { _, _ -> A_NOTIFICATION }, + var createFallbackNotificationResult: LambdaThreeParamsRecorder, Notification> = + lambdaRecorder { _, _, _ -> A_NOTIFICATION }, var createSummaryListNotificationResult: LambdaFiveParamsRecorder< NotificationAccountParams, String, Boolean, Long, NotificationAccountParams, Notification > = lambdaRecorder { _, _, _, _, _ -> A_NOTIFICATION }, @@ -75,10 +76,15 @@ class FakeNotificationCreator( } override fun createFallbackNotification( + existingNotification: Notification?, notificationAccountParams: NotificationAccountParams, - fallbackNotifiableEvent: FallbackNotifiableEvent, + fallbackNotifiableEvents: List, ): Notification { - return createFallbackNotificationResult(notificationAccountParams, fallbackNotifiableEvent) + return createFallbackNotificationResult( + existingNotification, + notificationAccountParams, + fallbackNotifiableEvents, + ) } override fun createSummaryListNotification( diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDataFactory.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDataFactory.kt index 009513b640..32c50ed7fd 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDataFactory.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDataFactory.kt @@ -19,7 +19,7 @@ import io.element.android.libraries.push.impl.notifications.model.FallbackNotifi import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent -import io.element.android.tests.testutils.lambda.LambdaFiveParamsRecorder +import io.element.android.tests.testutils.lambda.LambdaFourParamsRecorder import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder import io.element.android.tests.testutils.lambda.LambdaThreeParamsRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -28,18 +28,17 @@ class FakeNotificationDataFactory( var messageEventToNotificationsResult: LambdaThreeParamsRecorder< List, ImageLoader, NotificationAccountParams, List > = lambdaRecorder { _, _, _ -> emptyList() }, - var summaryToNotificationsResult: LambdaFiveParamsRecorder< + var summaryToNotificationsResult: LambdaFourParamsRecorder< List, List, List, - List, NotificationAccountParams, SummaryNotification - > = lambdaRecorder { _, _, _, _, _ -> SummaryNotification.Update(A_NOTIFICATION) }, + > = lambdaRecorder { _, _, _, _ -> SummaryNotification.Update(A_NOTIFICATION) }, var inviteToNotificationsResult: LambdaOneParamRecorder, List> = lambdaRecorder { _ -> emptyList() }, var simpleEventToNotificationsResult: LambdaOneParamRecorder, List> = lambdaRecorder { _ -> emptyList() }, - var fallbackEventToNotificationsResult: LambdaOneParamRecorder, List> = - lambdaRecorder { _ -> emptyList() }, + var fallbackEventToNotificationsResult: LambdaOneParamRecorder, OneShotNotification?> = + lambdaRecorder { _ -> null }, ) : NotificationDataFactory { override suspend fun toNotifications( messages: List, @@ -69,10 +68,10 @@ class FakeNotificationDataFactory( @JvmName("toNotificationFallbackEvents") @Suppress("INAPPLICABLE_JVM_NAME") - override fun toNotifications( + override fun toNotification( fallback: List, notificationAccountParams: NotificationAccountParams, - ): List { + ): OneShotNotification? { return fallbackEventToNotificationsResult(fallback) } @@ -80,14 +79,12 @@ class FakeNotificationDataFactory( roomNotifications: List, invitationNotifications: List, simpleNotifications: List, - fallbackNotifications: List, notificationAccountParams: NotificationAccountParams, ): SummaryNotification { return summaryToNotificationsResult( roomNotifications, invitationNotifications, simpleNotifications, - fallbackNotifications, notificationAccountParams, ) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeSummaryGroupMessageCreator.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeSummaryGroupMessageCreator.kt index 9db6853408..8bf309cd7b 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeSummaryGroupMessageCreator.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeSummaryGroupMessageCreator.kt @@ -14,27 +14,25 @@ import io.element.android.libraries.push.impl.notifications.RoomNotification import io.element.android.libraries.push.impl.notifications.SummaryGroupMessageCreator import io.element.android.libraries.push.impl.notifications.factories.NotificationAccountParams import io.element.android.libraries.push.impl.notifications.fixtures.A_NOTIFICATION -import io.element.android.tests.testutils.lambda.LambdaFiveParamsRecorder +import io.element.android.tests.testutils.lambda.LambdaFourParamsRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder class FakeSummaryGroupMessageCreator( - var createSummaryNotificationResult: LambdaFiveParamsRecorder< - NotificationAccountParams, List, List, List, List, Notification> = - lambdaRecorder { _, _, _, _, _ -> A_NOTIFICATION } + var createSummaryNotificationResult: LambdaFourParamsRecorder< + NotificationAccountParams, List, List, List, Notification> = + lambdaRecorder { _, _, _, _ -> A_NOTIFICATION } ) : SummaryGroupMessageCreator { override fun createSummaryNotification( notificationAccountParams: NotificationAccountParams, roomNotifications: List, invitationNotifications: List, simpleNotifications: List, - fallbackNotifications: List, ): Notification { return createSummaryNotificationResult( notificationAccountParams, roomNotifications, invitationNotifications, simpleNotifications, - fallbackNotifications, ) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt index 7c95034985..043f996205 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt @@ -57,7 +57,6 @@ import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.Fa import io.element.android.libraries.workmanager.api.WorkManagerRequest import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider -import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.lambdaError @@ -724,7 +723,6 @@ class DefaultPushHandlerTest { appCoroutineScope = backgroundScope, fallbackNotificationFactory = FallbackNotificationFactory( clock = FakeSystemClock(), - stringProvider = FakeStringProvider(), ), syncOnNotifiableEvent = syncOnNotifiableEvent, featureFlagService = featureFlagService, From f93dfbdae4d4a31e00fb252fc376ed276b78720b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Feb 2026 18:28:23 +0100 Subject: [PATCH 04/12] Remove `NavigationState.Space`. We do not have the concept of current space. --- .../android/appnav/LoggedInFlowNode.kt | 4 - .../libraries/matrix/api/core/SpaceId.kt | 5 - .../DefaultNotificationDrawerManager.kt | 5 +- .../api/AppNavigationStateService.kt | 4 - .../appnavstate/api/NavigationState.kt | 10 +- .../api/NavigationStateExtension.kt | 18 +-- .../impl/DefaultAppNavigationStateService.kt | 49 ++------ .../impl/DefaultNavigationStateServiceTest.kt | 118 ++---------------- .../appnavstate/test/AppNavStateFixture.kt | 12 +- .../test/FakeAppNavigationStateService.kt | 7 -- 10 files changed, 22 insertions(+), 210 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index c76150a67e..ea78faf50f 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -79,7 +79,6 @@ import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.core.MAIN_SPACE import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.UserId @@ -211,8 +210,6 @@ class LoggedInFlowNode( onCreate = { analyticsRoomListStateWatcher.start() appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId) - // TODO We do not support Space yet, so directly navigate to main space - appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE) loggedInFlowProcessor.observeEvents(sessionCoroutineScope) matrixClient.sessionVerificationService.setListener(verificationListener) mediaPreviewConfigMigration() @@ -242,7 +239,6 @@ class LoggedInFlowNode( } }, onDestroy = { - appNavigationStateService.onLeavingSpace(id) appNavigationStateService.onLeavingSession(id) loggedInFlowProcessor.stopObserving() matrixClient.sessionVerificationService.setListener(null) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt index 6acf2e43d8..0f270b3453 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt @@ -9,8 +9,3 @@ package io.element.android.libraries.matrix.api.core typealias SpaceId = RoomId - -/** - * Value to use when no space is selected by the user. - */ -val MAIN_SPACE = SpaceId("!mainSpace:local") 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 cd3f0faf45..3dbf09e227 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 @@ -72,17 +72,16 @@ class DefaultNotificationDrawerManager( when (navigationState) { NavigationState.Root -> {} is NavigationState.Session -> {} - is NavigationState.Space -> {} is NavigationState.Room -> { // Cleanup notification for current room clearMessagesForRoom( - sessionId = navigationState.parentSpace.parentSession.sessionId, + sessionId = navigationState.parentSession.sessionId, roomId = navigationState.roomId, ) } is NavigationState.Thread -> { clearMessagesForThread( - sessionId = navigationState.parentRoom.parentSpace.parentSession.sessionId, + sessionId = navigationState.parentRoom.parentSession.sessionId, roomId = navigationState.parentRoom.roomId, threadId = navigationState.threadId, ) diff --git a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationStateService.kt b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationStateService.kt index 8387afca59..cf32bb0bf4 100644 --- a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationStateService.kt +++ b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationStateService.kt @@ -10,7 +10,6 @@ package io.element.android.services.appnavstate.api 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 kotlinx.coroutines.flow.StateFlow @@ -23,9 +22,6 @@ interface AppNavigationStateService { fun onNavigateToSession(owner: String, sessionId: SessionId) fun onLeavingSession(owner: String) - fun onNavigateToSpace(owner: String, spaceId: SpaceId) - fun onLeavingSpace(owner: String) - fun onNavigateToRoom(owner: String, roomId: RoomId) fun onLeavingRoom(owner: String) diff --git a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/NavigationState.kt b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/NavigationState.kt index 0ff606e051..c29e153a64 100644 --- a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/NavigationState.kt +++ b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/NavigationState.kt @@ -10,7 +10,6 @@ package io.element.android.services.appnavstate.api 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 /** @@ -29,17 +28,10 @@ sealed class NavigationState(open val owner: String) { val sessionId: SessionId, ) : NavigationState(owner) - data class Space( - override val owner: String, - // Can be fake value, if no space is selected - val spaceId: SpaceId, - val parentSession: Session, - ) : NavigationState(owner) - data class Room( override val owner: String, val roomId: RoomId, - val parentSpace: Space, + val parentSession: Session, ) : NavigationState(owner) data class Thread( diff --git a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/NavigationStateExtension.kt b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/NavigationStateExtension.kt index bf336a38bf..d0b263d698 100644 --- a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/NavigationStateExtension.kt +++ b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/NavigationStateExtension.kt @@ -10,26 +10,14 @@ package io.element.android.services.appnavstate.api 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 fun NavigationState.currentSessionId(): SessionId? { return when (this) { NavigationState.Root -> null is NavigationState.Session -> sessionId - is NavigationState.Space -> parentSession.sessionId - is NavigationState.Room -> parentSpace.parentSession.sessionId - is NavigationState.Thread -> parentRoom.parentSpace.parentSession.sessionId - } -} - -fun NavigationState.currentSpaceId(): SpaceId? { - return when (this) { - NavigationState.Root -> null - is NavigationState.Session -> null - is NavigationState.Space -> spaceId - is NavigationState.Room -> parentSpace.spaceId - is NavigationState.Thread -> parentRoom.parentSpace.spaceId + is NavigationState.Room -> parentSession.sessionId + is NavigationState.Thread -> parentRoom.parentSession.sessionId } } @@ -37,7 +25,6 @@ fun NavigationState.currentRoomId(): RoomId? { return when (this) { NavigationState.Root -> null is NavigationState.Session -> null - is NavigationState.Space -> null is NavigationState.Room -> roomId is NavigationState.Thread -> parentRoom.roomId } @@ -47,7 +34,6 @@ fun NavigationState.currentThreadId(): ThreadId? { return when (this) { NavigationState.Root -> null is NavigationState.Session -> null - is NavigationState.Space -> null is NavigationState.Room -> null is NavigationState.Thread -> threadId } diff --git a/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt index e820bf9262..cdc8f7b07f 100644 --- a/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt +++ b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt @@ -15,7 +15,6 @@ import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.annotations.AppCoroutineScope 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.AppForegroundStateService import io.element.android.services.appnavstate.api.AppNavigationState @@ -62,7 +61,6 @@ class DefaultAppNavigationStateService( Timber.tag(loggerTag.value).d("Navigating to session $sessionId. Current state: $currentValue") val newValue: NavigationState.Session = when (currentValue) { is NavigationState.Session, - is NavigationState.Space, is NavigationState.Room, is NavigationState.Thread, is NavigationState.Root -> NavigationState.Session(owner, sessionId) @@ -70,28 +68,14 @@ class DefaultAppNavigationStateService( state.getAndUpdate { it.copy(navigationState = newValue) } } - override fun onNavigateToSpace(owner: String, spaceId: SpaceId) { - val currentValue = state.value.navigationState - Timber.tag(loggerTag.value).d("Navigating to space $spaceId. Current state: $currentValue") - val newValue: NavigationState.Space = when (currentValue) { - NavigationState.Root -> return logError("onNavigateToSession()") - is NavigationState.Session -> NavigationState.Space(owner, spaceId, currentValue) - is NavigationState.Space -> NavigationState.Space(owner, spaceId, currentValue.parentSession) - is NavigationState.Room -> NavigationState.Space(owner, spaceId, currentValue.parentSpace.parentSession) - is NavigationState.Thread -> NavigationState.Space(owner, spaceId, currentValue.parentRoom.parentSpace.parentSession) - } - state.getAndUpdate { it.copy(navigationState = newValue) } - } - override fun onNavigateToRoom(owner: String, roomId: RoomId) { val currentValue = state.value.navigationState Timber.tag(loggerTag.value).d("Navigating to room $roomId. Current state: $currentValue") val newValue: NavigationState.Room = when (currentValue) { NavigationState.Root -> return logError("onNavigateToSession()") - is NavigationState.Session -> return logError("onNavigateToSpace()") - is NavigationState.Space -> NavigationState.Room(owner, roomId, currentValue) - is NavigationState.Room -> NavigationState.Room(owner, roomId, currentValue.parentSpace) - is NavigationState.Thread -> NavigationState.Room(owner, roomId, currentValue.parentRoom.parentSpace) + is NavigationState.Session -> NavigationState.Room(owner, roomId, currentValue) + is NavigationState.Room -> NavigationState.Room(owner, roomId, currentValue.parentSession) + is NavigationState.Thread -> NavigationState.Room(owner, roomId, currentValue.parentRoom.parentSession) } state.getAndUpdate { it.copy(navigationState = newValue) } } @@ -101,8 +85,7 @@ class DefaultAppNavigationStateService( Timber.tag(loggerTag.value).d("Navigating to thread $threadId. Current state: $currentValue") val newValue: NavigationState.Thread = when (currentValue) { NavigationState.Root -> return logError("onNavigateToSession()") - is NavigationState.Session -> return logError("onNavigateToSpace()") - is NavigationState.Space -> return logError("onNavigateToRoom()") + is NavigationState.Session -> return logError("onNavigateToRoom()") is NavigationState.Room -> NavigationState.Thread(owner, threadId, currentValue) is NavigationState.Thread -> NavigationState.Thread(owner, threadId, currentValue.parentRoom) } @@ -115,8 +98,7 @@ class DefaultAppNavigationStateService( if (!currentValue.assertOwner(owner)) return val newValue: NavigationState.Room = when (currentValue) { NavigationState.Root -> return logError("onNavigateToSession()") - is NavigationState.Session -> return logError("onNavigateToSpace()") - is NavigationState.Space -> return logError("onNavigateToRoom()") + is NavigationState.Session -> return logError("onNavigateToRoom()") is NavigationState.Room -> return logError("onNavigateToThread()") is NavigationState.Thread -> currentValue.parentRoom } @@ -127,26 +109,11 @@ class DefaultAppNavigationStateService( val currentValue = state.value.navigationState Timber.tag(loggerTag.value).d("Leaving room. Current state: $currentValue") if (!currentValue.assertOwner(owner)) return - val newValue: NavigationState.Space = when (currentValue) { - NavigationState.Root -> return logError("onNavigateToSession()") - is NavigationState.Session -> return logError("onNavigateToSpace()") - is NavigationState.Space -> return logError("onNavigateToRoom()") - is NavigationState.Room -> currentValue.parentSpace - is NavigationState.Thread -> currentValue.parentRoom.parentSpace - } - state.getAndUpdate { it.copy(navigationState = newValue) } - } - - override fun onLeavingSpace(owner: String) { - val currentValue = state.value.navigationState - Timber.tag(loggerTag.value).d("Leaving space. Current state: $currentValue") - if (!currentValue.assertOwner(owner)) return val newValue: NavigationState.Session = when (currentValue) { NavigationState.Root -> return logError("onNavigateToSession()") - is NavigationState.Session -> return logError("onNavigateToSpace()") - is NavigationState.Space -> currentValue.parentSession - is NavigationState.Room -> currentValue.parentSpace.parentSession - is NavigationState.Thread -> currentValue.parentRoom.parentSpace.parentSession + is NavigationState.Session -> return logError("onNavigateToRoom()") + is NavigationState.Room -> currentValue.parentSession + is NavigationState.Thread -> currentValue.parentRoom.parentSession } state.getAndUpdate { it.copy(navigationState = newValue) } } diff --git a/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultNavigationStateServiceTest.kt b/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultNavigationStateServiceTest.kt index 56f9c5e986..e29177f382 100644 --- a/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultNavigationStateServiceTest.kt +++ b/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultNavigationStateServiceTest.kt @@ -13,15 +13,12 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID_2 -import io.element.android.libraries.matrix.test.A_SPACE_ID -import io.element.android.libraries.matrix.test.A_SPACE_ID_2 import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.A_THREAD_ID_2 import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.NavigationState import io.element.android.services.appnavstate.test.A_ROOM_OWNER import io.element.android.services.appnavstate.test.A_SESSION_OWNER -import io.element.android.services.appnavstate.test.A_SPACE_OWNER import io.element.android.services.appnavstate.test.A_THREAD_OWNER import io.element.android.services.appnavstate.test.FakeAppForegroundStateService import kotlinx.coroutines.flow.first @@ -33,22 +30,17 @@ class DefaultNavigationStateServiceTest { private val navigationStateRoot = NavigationState.Root private val navigationStateSession = NavigationState.Session( owner = A_SESSION_OWNER, - sessionId = A_SESSION_ID - ) - private val navigationStateSpace = NavigationState.Space( - owner = A_SPACE_OWNER, - spaceId = A_SPACE_ID, - parentSession = navigationStateSession + sessionId = A_SESSION_ID, ) private val navigationStateRoom = NavigationState.Room( owner = A_ROOM_OWNER, roomId = A_ROOM_ID, - parentSpace = navigationStateSpace + parentSession = navigationStateSession, ) private val navigationStateThread = NavigationState.Thread( owner = A_THREAD_OWNER, threadId = A_THREAD_ID, - parentRoom = navigationStateRoom + parentRoom = navigationStateRoom, ) @Test @@ -57,8 +49,6 @@ class DefaultNavigationStateServiceTest { assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) service.onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession) - service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID) - assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace) service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoom) service.onNavigateToThread(A_THREAD_OWNER, A_THREAD_ID) @@ -67,8 +57,6 @@ class DefaultNavigationStateServiceTest { service.onLeavingThread(A_THREAD_OWNER) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoom) service.onLeavingRoom(A_ROOM_OWNER) - assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace) - service.onLeavingSpace(A_SPACE_OWNER) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession) service.onLeavingSession(A_SESSION_OWNER) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) @@ -77,7 +65,7 @@ class DefaultNavigationStateServiceTest { @Test fun testFailure() = runTest { val service = createStateService() - service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID) + service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID) assertThat(service.appNavigationState.value.navigationState).isEqualTo(NavigationState.Root) } @@ -92,11 +80,6 @@ class DefaultNavigationStateServiceTest { service.navigateToSession() service.onNavigateToThread(A_THREAD_OWNER, A_THREAD_ID) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession) - // From space (no effect) - service.reset() - service.navigateToSpace() - service.onNavigateToThread(A_THREAD_OWNER, A_THREAD_ID) - assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace) // From room service.reset() service.navigateToRoom() @@ -116,15 +99,10 @@ class DefaultNavigationStateServiceTest { // From root (no effect) service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) - // From session (no effect) + // From session service.reset() service.navigateToSession() service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID) - assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession) - // From space - service.reset() - service.navigateToSpace() - service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoom) // From room service.reset() @@ -139,35 +117,6 @@ class DefaultNavigationStateServiceTest { assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoom) } - @Test - fun testOnNavigateToSpace() = runTest { - val service = createStateService() - // From root (no effect) - service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID) - assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) - // From session - service.reset() - service.navigateToSession() - service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID) - assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace) - // From space - service.reset() - service.navigateToSpace() - // Navigate to another space - service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID_2) - assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace.copy(spaceId = A_SPACE_ID_2)) - // From room (no effect) - service.reset() - service.navigateToRoom() - service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID) - assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace) - // From thread (no effect) - service.reset() - service.navigateToThread() - service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID) - assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace) - } - @Test fun testOnNavigateToSession() = runTest { val service = createStateService() @@ -180,11 +129,6 @@ class DefaultNavigationStateServiceTest { // Navigate to another session service.onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID_2) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession.copy(sessionId = A_SESSION_ID_2)) - // From space - service.reset() - service.navigateToSpace() - service.onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID) - assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession) // From room service.reset() service.navigateToRoom() @@ -208,11 +152,6 @@ class DefaultNavigationStateServiceTest { service.navigateToSession() service.onLeavingThread(A_THREAD_OWNER) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession) - // From space (no effect) - service.reset() - service.navigateToSpace() - service.onLeavingThread(A_THREAD_OWNER) - assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace) // From room (no effect) service.reset() service.navigateToRoom() @@ -236,16 +175,11 @@ class DefaultNavigationStateServiceTest { service.navigateToSession() service.onLeavingRoom(A_ROOM_OWNER) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession) - // From space (no effect) - service.reset() - service.navigateToSpace() - service.onLeavingRoom(A_ROOM_OWNER) - assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace) // From room service.reset() service.navigateToRoom() service.onLeavingRoom(A_ROOM_OWNER) - assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace) + assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession) // From thread (no effect) service.reset() service.navigateToThread() @@ -253,34 +187,6 @@ class DefaultNavigationStateServiceTest { assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateThread) } - @Test - fun testOnLeavingSpace() = runTest { - val service = createStateService() - // From root (no effect) - service.onLeavingSpace(A_SPACE_OWNER) - assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) - // From session (no effect) - service.reset() - service.navigateToSession() - service.onLeavingSpace(A_SPACE_OWNER) - assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession) - // From space - service.reset() - service.navigateToSpace() - service.onLeavingSpace(A_SPACE_OWNER) - assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession) - // From room (no effect) - service.reset() - service.navigateToRoom() - service.onLeavingSpace(A_SPACE_OWNER) - assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoom) - // From thread (no effect) - service.reset() - service.navigateToThread() - service.onLeavingSpace(A_SPACE_OWNER) - assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateThread) - } - @Test fun testOnLeavingSession() = runTest { val service = createStateService() @@ -292,11 +198,6 @@ class DefaultNavigationStateServiceTest { service.navigateToSession() service.onLeavingSession(A_SESSION_OWNER) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) - // From space (no effect) - service.reset() - service.navigateToSpace() - service.onLeavingSession(A_SESSION_OWNER) - assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace) // From room (no effect) service.reset() service.navigateToRoom() @@ -318,13 +219,8 @@ class DefaultNavigationStateServiceTest { onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID) } - private fun AppNavigationStateService.navigateToSpace() { - navigateToSession() - onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID) - } - private fun AppNavigationStateService.navigateToRoom() { - navigateToSpace() + navigateToSession() onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID) } diff --git a/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/AppNavStateFixture.kt b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/AppNavStateFixture.kt index e9c2f6d2fa..7860320eee 100644 --- a/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/AppNavStateFixture.kt +++ b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/AppNavStateFixture.kt @@ -8,21 +8,17 @@ package io.element.android.services.appnavstate.test -import io.element.android.libraries.matrix.api.core.MAIN_SPACE 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 const val A_SESSION_OWNER = "aSessionOwner" -const val A_SPACE_OWNER = "aSpaceOwner" const val A_ROOM_OWNER = "aRoomOwner" const val A_THREAD_OWNER = "aThreadOwner" fun aNavigationState( sessionId: SessionId? = null, - spaceId: SpaceId? = MAIN_SPACE, roomId: RoomId? = null, threadId: ThreadId? = null, ): NavigationState { @@ -30,14 +26,10 @@ fun aNavigationState( return NavigationState.Root } val session = NavigationState.Session(A_SESSION_OWNER, sessionId) - if (spaceId == null) { + if (roomId == null) { return session } - val space = NavigationState.Space(A_SPACE_OWNER, spaceId, session) - if (roomId == null) { - return space - } - val room = NavigationState.Room(A_ROOM_OWNER, roomId, space) + val room = NavigationState.Room(A_ROOM_OWNER, roomId, session) if (threadId == null) { return room } 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 d01bc4f34b..fd23b7c422 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 @@ -10,7 +10,6 @@ package io.element.android.services.appnavstate.test 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.AppNavigationState import io.element.android.services.appnavstate.api.AppNavigationStateService @@ -28,15 +27,9 @@ class FakeAppNavigationStateService( override fun onNavigateToSession(owner: String, sessionId: SessionId) = Unit override fun onLeavingSession(owner: String) = Unit - override fun onNavigateToSpace(owner: String, spaceId: SpaceId) = Unit - - override fun onLeavingSpace(owner: String) = Unit - override fun onNavigateToRoom(owner: String, roomId: RoomId) = Unit - override fun onLeavingRoom(owner: String) = Unit override fun onNavigateToThread(owner: String, threadId: ThreadId) = Unit - override fun onLeavingThread(owner: String) = Unit } From 414e7ab9c6f398cd2a56e0870f6605ff481d31a0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Feb 2026 20:51:05 +0100 Subject: [PATCH 05/12] Cleanup --- .../push/impl/notifications/NotificationDataFactory.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt index f44ea41c45..5331519110 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt @@ -73,10 +73,7 @@ class DefaultNotificationDataFactory( val messagesToDisplay = messages.filterNot { it.canNotBeDisplayed() } .groupBy { it.roomId } return messagesToDisplay.flatMap { (roomId, events) -> - val roomName = events.lastOrNull()?.roomName ?: roomId.value - val isDm = events.lastOrNull()?.roomIsDm ?: false val eventsByThreadId = events.groupBy { it.threadId } - eventsByThreadId.map { (threadId, events) -> val notification = roomGroupMessageCreator.createRoomMessage( events = events, From 21f65a119e21a566a2ed0318ce78f4b163804d97 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Feb 2026 21:03:48 +0100 Subject: [PATCH 06/12] Add missing test --- .../NotificationDataFactoryTest.kt | 21 +++++++++++++++++++ .../DefaultNotificationCreatorTest.kt | 15 ++----------- .../fixtures/NotifiableEventFixture.kt | 15 +++++++++++++ .../push/impl/push/DefaultPushHandlerTest.kt | 15 ++----------- 4 files changed, 40 insertions(+), 26 deletions(-) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt index c07c1ff89e..f897388c66 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt @@ -20,6 +20,7 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotif import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator 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.aFallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent @@ -33,6 +34,7 @@ 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) +private val A_FALLBACK_EVENT = aFallbackNotifiableEvent() @RunWith(RobolectricTestRunner::class) class NotificationDataFactoryTest { @@ -69,6 +71,25 @@ class NotificationDataFactoryTest { ) } + @Test + fun `given a fallback invitation when mapping to notification then it's added`() = testWith(notificationDataFactory) { + val fallbackEvents = listOf(A_FALLBACK_EVENT) + val expectedNotification = notificationCreator.createFallbackNotificationResult( + null, + aNotificationAccountParams(), + fallbackEvents, + ) + val result = toNotification(fallbackEvents, aNotificationAccountParams()) + assertThat(result).isEqualTo( + OneShotNotification( + notification = expectedNotification, + tag = "FALLBACK", + isNoisy = false, + timestamp = A_FALLBACK_EVENT.timestamp + ) + ) + } + @Test fun `given a simple event when mapping to notification then it's added`() = testWith(notificationDataFactory) { val expectedNotification = notificationCreator.createRoomInvitationNotificationResult( diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt index a346aa839f..ae343d811d 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt @@ -35,8 +35,8 @@ import io.element.android.libraries.push.impl.notifications.factories.action.Acc 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.factories.action.RejectInvitationActionFactory +import io.element.android.libraries.push.impl.notifications.fixtures.aFallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent -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.sdk.FakeBuildVersionSdkIntProvider @@ -85,18 +85,7 @@ class DefaultNotificationCreatorTest { existingNotification = null, notificationAccountParams = aNotificationAccountParams(), fallbackNotifiableEvents = listOf( - 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, - cause = null, - ), + aFallbackNotifiableEvent(), ) ) result.commonAssertions( 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 edd0c2ba17..9b7929b6a0 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 @@ -24,10 +24,12 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_TIMESTAMP import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.A_USER_NAME_2 +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 import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent +import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP fun aSimpleNotifiableEvent( sessionId: SessionId = A_SESSION_ID, @@ -141,3 +143,16 @@ fun aNotifiableCallEvent( senderAvatarUrl = senderAvatarUrl, rtcNotificationType = rtcNotificationType, ) + +fun aFallbackNotifiableEvent() = FallbackNotifiableEvent( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + editedEventId = null, + description = "A fallback notification", + canBeReplaced = false, + isRedacted = false, + isUpdated = false, + timestamp = A_FAKE_TIMESTAMP, + cause = "Unable to decrypt event", +) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt index 043f996205..fc072144c7 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt @@ -40,9 +40,9 @@ import io.element.android.libraries.push.impl.notifications.DefaultNotificationR import io.element.android.libraries.push.impl.notifications.FakeNotifiableEventResolver import io.element.android.libraries.push.impl.notifications.FallbackNotificationFactory import io.element.android.libraries.push.impl.notifications.channels.FakeNotificationChannels +import io.element.android.libraries.push.impl.notifications.fixtures.aFallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableCallEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent -import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent import io.element.android.libraries.push.impl.test.DefaultTestPush @@ -626,18 +626,7 @@ class DefaultPushHandlerTest { @Test fun `when receiving a fallback event, we notify the push history service about it not being resolved`() = runTest { - val aNotifiableFallbackEvent = FallbackNotifiableEvent( - sessionId = A_SESSION_ID, - roomId = A_ROOM_ID, - eventId = AN_EVENT_ID, - editedEventId = null, - description = "A fallback notification", - canBeReplaced = false, - isRedacted = false, - isUpdated = false, - timestamp = 0L, - cause = "Unable to decrypt event", - ) + val aNotifiableFallbackEvent = aFallbackNotifiableEvent() val notifiableEventResult = lambdaRecorder, Result>>> { _, _ -> val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO) From 0f0f09b4c09474560c43feee268c60c3ccd25642 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Feb 2026 09:12:17 +0100 Subject: [PATCH 07/12] Fix test. --- .../notifications/DefaultNotificationDrawerManagerTest.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) 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 9bbc2175e5..7f175a12c9 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 @@ -16,7 +16,6 @@ import io.element.android.features.enterprise.test.FakeEnterpriseService 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.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClientProvider @@ -108,11 +107,9 @@ class DefaultNotificationDrawerManagerTest { runCurrent() appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID), isInForeground = true)) runCurrent() - appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_SPACE_ID), isInForeground = true)) + appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_ROOM_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)) + appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID), isInForeground = true)) runCurrent() // Like a user sign out appNavigationStateFlow.emit(AppNavigationState(aNavigationState(), isInForeground = true)) From eb240533fb6674ca2144f94bca2b810b28bfcb21 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 09:12:54 +0100 Subject: [PATCH 08/12] fix(deps): update activity to v1.12.4 (#6184) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 109a923109..ecf0fcd5ef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ datastore = "1.2.0" constraintlayout = "2.2.1" constraintlayout_compose = "1.1.1" lifecycle = "2.10.0" -activity = "1.12.3" +activity = "1.12.4" media3 = "1.9.2" camera = "1.5.3" work = "2.11.1" From 35a5ae27e218a7eadeddfcbc89b9ab9a80b9db20 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 13:10:36 +0100 Subject: [PATCH 09/12] fix(deps): update roborazzi to v1.59.0 (#6179) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ecf0fcd5ef..7bfea5df73 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,7 +32,7 @@ accompanist = "0.37.3" # Test test_core = "1.7.0" -roborazzi = "1.58.0" +roborazzi = "1.59.0" # Jetbrain datetime = "0.7.1" From edd747327bf0f4494480cca94cced1e27bb3f33b Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 12 Feb 2026 13:12:57 +0100 Subject: [PATCH 10/12] When linkifying, adjust the `URLSpan`'s url too (#6188) --- .../androidutils/text/LinkifyHelper.kt | 9 +++---- .../androidutils/text/LinkifierHelperTest.kt | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/text/LinkifyHelper.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/text/LinkifyHelper.kt index 5115605322..441f819546 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/text/LinkifyHelper.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/text/LinkifyHelper.kt @@ -16,8 +16,6 @@ import androidx.core.text.toSpannable import androidx.core.text.util.LinkifyCompat import io.element.android.libraries.core.extensions.runCatchingExceptions import timber.log.Timber -import kotlin.collections.component1 -import kotlin.collections.component2 /** * Helper class to linkify text while preserving existing URL spans. @@ -59,7 +57,8 @@ object LinkifyHelper { // Adapt the url in the URL span to the new end index too if needed if (end != newEnd) { - val url = spannable.subSequence(start, newEnd).toString() + val diff = end - newEnd + val url = urlSpan.url.substring(0, urlSpan.url.length - diff) spannable.removeSpan(urlSpan) spannable.setSpan(URLSpan(url), start, newEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } else { @@ -87,12 +86,12 @@ object LinkifyHelper { var end = end // Trailing punctuation found, adjust the end index - while (spannable[end - 1] in sequenceOf('.', ',', ';', ':', '!', '?', '…') && end > start) { + while (end > start && spannable[end - 1] in sequenceOf('.', ',', ';', ':', '!', '?', '…')) { end-- } // If the last character is a closing parenthesis, check if it's part of a pair - if (spannable[end - 1] == ')' && end > start) { + if (end > start && spannable[end - 1] == ')') { val linkifiedTextLastPath = spannable.substring(start, end).substringAfterLast('/') val closingParenthesisCount = linkifiedTextLastPath.count { it == ')' } val openingParenthesisCount = linkifiedTextLastPath.count { it == '(' } diff --git a/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/text/LinkifierHelperTest.kt b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/text/LinkifierHelperTest.kt index 6fa18be1ac..22928781c9 100644 --- a/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/text/LinkifierHelperTest.kt +++ b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/text/LinkifierHelperTest.kt @@ -10,7 +10,9 @@ package io.element.android.libraries.androidutils.text import android.telephony.TelephonyManager import android.text.style.URLSpan +import androidx.core.text.buildSpannedString import androidx.core.text.getSpans +import androidx.core.text.inSpans import androidx.core.text.toSpannable import com.google.common.truth.Truth.assertThat import io.element.android.tests.testutils.WarmUpRule @@ -140,4 +142,27 @@ class LinkifierHelperTest { assertThat(urlSpans.size).isEqualTo(1) assertThat(urlSpans.first().url).isEqualTo("https://github.com/element-hq/element-android/READ(ME)") } + + @Test + fun `linkification handles trailing question marks`() { + val text = "A url: https://github.com/element-hq/element-android?" + val result = LinkifyHelper.linkify(text) + val urlSpans = result.toSpannable().getSpans() + assertThat(urlSpans.size).isEqualTo(1) + assertThat(urlSpans.first().url).isEqualTo("https://github.com/element-hq/element-android") + } + + @Test + fun `linkification doesn't modify existing URLSpan`() { + val text = buildSpannedString { + append("A url: ") + inSpans(URLSpan("https://github.com/element-hq/element-android?")) { + append("here") + } + } + val result = LinkifyHelper.linkify(text) + val urlSpans = result.toSpannable().getSpans() + assertThat(urlSpans.size).isEqualTo(1) + assertThat(urlSpans.first().url).isEqualTo("https://github.com/element-hq/element-android?") + } } From c0a5d963c45a6d5ad1b803bbfc9634c204c4eacf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 12:47:53 +0000 Subject: [PATCH 11/12] fix(deps): update dependency androidx.compose:compose-bom to v2026.02.00 (#6172) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7bfea5df73..ee81c32682 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ camera = "1.5.3" work = "2.11.1" # Compose -compose_bom = "2026.01.00" +compose_bom = "2026.02.00" # Coroutines coroutines = "1.10.2" From 6bdb9c7c52b420a9ac315b722973265aa7f12863 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 12 Feb 2026 16:24:19 +0100 Subject: [PATCH 12/12] Fix Maestro tests again (#6167) * Increase the timeout for asserting 'Be in your element' is visible * Disable the chrome onboarding flow. This simplifies the code a lot and helps avoid corner cases. * More delays with timeout! * Add more info to the summary, specially when the tests fail, so we can quickly check what failed. * Make sure Maestro tests can't run in parallel, but APK builds can --- .github/workflows/maestro-local.yml | 24 +++++++++++++++---- .../maestro-local-with-screen-recording.sh | 7 ++++++ .maestro/tests/account/login.yaml | 21 ---------------- .maestro/tests/account/verifySession.yaml | 12 +++++++--- .../tests/assertions/assertInitDisplayed.yaml | 2 +- .../assertSessionVerificationDisplayed.yaml | 2 +- 6 files changed, 37 insertions(+), 31 deletions(-) diff --git a/.github/workflows/maestro-local.yml b/.github/workflows/maestro-local.yml index 4fd42534b7..a2851eb863 100644 --- a/.github/workflows/maestro-local.yml +++ b/.github/workflows/maestro-local.yml @@ -18,9 +18,8 @@ jobs: build-apk: name: Build APK runs-on: ubuntu-latest - # Allow one per PR. concurrency: - group: ${{ format('maestro-{0}', github.ref) }} + group: ${{ format('maestro-build-{0}', github.ref) }} cancel-in-progress: true steps: - uses: actions/checkout@v6 @@ -57,10 +56,10 @@ jobs: name: Maestro test suite runs-on: ubuntu-latest needs: [ build-apk ] - # Allow one per PR. + # Allow only one to run at a time, since they use the same environment. + # Otherwise, tests running in parallel can break each other. concurrency: - group: ${{ format('maestro-{0}', github.ref) }} - cancel-in-progress: true + group: maestro-test steps: - uses: actions/checkout@v6 if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch' @@ -110,6 +109,21 @@ jobs: retention-days: 5 overwrite: true if-no-files-found: error + - name: Update summary (success) + if: steps.maestro_test.outcome == 'success' + run: | + echo "### Maestro tests worked :rocket:!" + - name: Update summary (failure) + if: steps.maestro_test.outcome != 'success' + run: | + LOG_FILE=$(find ~/.maestro/tests/ -name maestro.log) + echo "Log file: $LOG_FILE" + LOG_LINES="$(tail -n 30 $LOG_FILE)" + echo "### :x: Maestro tests failed... + + \`\`\` + $LOG_LINES + \`\`\`" >> $GITHUB_STEP_SUMMARY - name: Fail the workflow in case of error in test if: steps.maestro_test.outcome != 'success' run: | diff --git a/.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh b/.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh index a9f789a4f5..51a968fdec 100755 --- a/.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh +++ b/.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh @@ -8,6 +8,13 @@ # Please see LICENSE in the repository root for full details. # +# First we disable the onboarding flow on Chrome, which is a source of issues +# (see https://stackoverflow.com/a/64629745) +echo "Disabling Chrome onboarding flow" +adb shell am set-debug-app --persistent com.android.chrome +adb shell 'echo "chrome --disable-fre --no-default-browser-check --no-first-run" > /data/local/tmp/chrome-command-line' +adb shell am start -n com.android.chrome/com.google.android.apps.chrome.Main + adb install -r $1 echo "Starting the screen recording..." adb push .github/workflows/scripts/maestro/local-recording.sh /data/local/tmp/ diff --git a/.maestro/tests/account/login.yaml b/.maestro/tests/account/login.yaml index 0661a30981..873843b22d 100644 --- a/.maestro/tests/account/login.yaml +++ b/.maestro/tests/account/login.yaml @@ -8,27 +8,6 @@ appId: ${MAESTRO_APP_ID} - tapOn: id: "login-continue" ## MAS page -## Conditional workflow to pass the Chrome first launch welcome page. -- retry: - maxRetries: 3 - commands: - - runFlow: - when: - visible: 'Use without an account' - commands: - - tapOn: "Use without an account" - ## For older chrome versions - - runFlow: - when: - visible: 'Accept & continue' - commands: - - tapOn: "Accept & continue" - - runFlow: - when: - visible: 'No thanks' - commands: - - tapOn: "No thanks" -## Working when running Maestro locally, but not on the CI yet. - retry: maxRetries: 3 commands: diff --git a/.maestro/tests/account/verifySession.yaml b/.maestro/tests/account/verifySession.yaml index a16322543f..a59f60b88f 100644 --- a/.maestro/tests/account/verifySession.yaml +++ b/.maestro/tests/account/verifySession.yaml @@ -1,5 +1,8 @@ appId: ${MAESTRO_APP_ID} --- +- extendedWaitUntil: + visible: "Enter recovery key" + timeout: 30000 - takeScreenshot: build/maestro/150-Verify - tapOn: "Enter recovery key" - tapOn: @@ -7,7 +10,10 @@ appId: ${MAESTRO_APP_ID} - inputText: ${MAESTRO_RECOVERY_KEY} - hideKeyboard - tapOn: "Continue" -- extendedWaitUntil: - visible: "Device verified" - timeout: 30000 +- retry: + maxRetries: 3 + commands: + - extendedWaitUntil: + visible: "Device verified" + timeout: 30000 - tapOn: "Continue" diff --git a/.maestro/tests/assertions/assertInitDisplayed.yaml b/.maestro/tests/assertions/assertInitDisplayed.yaml index 6e895d9bbf..7b5a2e665b 100644 --- a/.maestro/tests/assertions/assertInitDisplayed.yaml +++ b/.maestro/tests/assertions/assertInitDisplayed.yaml @@ -2,4 +2,4 @@ appId: ${MAESTRO_APP_ID} --- - extendedWaitUntil: visible: "Be in your element" - timeout: 10000 + timeout: 30000 diff --git a/.maestro/tests/assertions/assertSessionVerificationDisplayed.yaml b/.maestro/tests/assertions/assertSessionVerificationDisplayed.yaml index f983ced873..fff0fe7b32 100644 --- a/.maestro/tests/assertions/assertSessionVerificationDisplayed.yaml +++ b/.maestro/tests/assertions/assertSessionVerificationDisplayed.yaml @@ -2,4 +2,4 @@ appId: ${MAESTRO_APP_ID} --- - extendedWaitUntil: visible: "Confirm your identity" - timeout: 20000 + timeout: 60000