diff --git a/app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt b/app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt index f7b96f3aad..94c8572f20 100644 --- a/app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt +++ b/app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt @@ -33,4 +33,9 @@ class IntentProviderImpl @Inject constructor( override fun getMainIntent(): Intent { return Intent(context, MainActivity::class.java) } + + override fun getIntent(sessionId: String, roomId: String?, threadId: String?): Intent { + // TODO Handle deeplink or pass parameters + return Intent(context, MainActivity::class.java) + } } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt index 5582e7fe92..cf5792be35 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt @@ -19,12 +19,6 @@ package io.element.android.libraries.push.api import io.element.android.libraries.matrix.api.MatrixClient interface PushService { - // TODO EAx remove - fun setCurrentRoom(roomId: String?) - - // TODO EAx remove - fun setCurrentThread(threadId: String?) - fun notificationStyleChanged() // Ensure pusher is registered diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/store/PushDataStore.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/store/PushDataStore.kt index 823ea0c88c..d2a6bda0b0 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/store/PushDataStore.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/store/PushDataStore.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.Flow interface PushDataStore { val pushCounterFlow: Flow + // TODO Move all those settings to the per user store... fun areNotificationEnabledForDevice(): Boolean fun setNotificationEnabledForDevice(enabled: Boolean) diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index bb1c5bfa76..3724832e6a 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -46,6 +46,7 @@ dependencies { api(projects.libraries.push.api) implementation(projects.services.analytics.api) + implementation(projects.services.appnavstate.api) implementation(projects.services.toolbox.api) api("me.gujun.android:span:1.7") { diff --git a/libraries/push/impl/src/main/AndroidManifest.xml b/libraries/push/impl/src/main/AndroidManifest.xml index 71fc629aaa..1c380c260a 100644 --- a/libraries/push/impl/src/main/AndroidManifest.xml +++ b/libraries/push/impl/src/main/AndroidManifest.xml @@ -60,5 +60,15 @@ + + + + + diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt index 327a3eea96..7924ddf996 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt @@ -32,14 +32,6 @@ class DefaultPushService @Inject constructor( private val pushersManager: PushersManager, private val fcmHelper: FcmHelper, ) : PushService { - override fun setCurrentRoom(roomId: String?) { - notificationDrawerManager.setCurrentRoom(roomId) - } - - override fun setCurrentThread(threadId: String?) { - notificationDrawerManager.setCurrentThread(threadId) - } - override fun notificationStyleChanged() { notificationDrawerManager.notificationStyleChanged() } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt index 936ccfcbde..0036da43f6 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt @@ -20,7 +20,13 @@ import android.content.Intent interface IntentProvider { /** - * Provide an intent to start the application + * Provide an intent to start the application. */ fun getMainIntent(): Intent + + fun getIntent( + sessionId: String, + roomId: String?, + threadId: String?, + ): Intent } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/log/LoggerTag.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/log/LoggerTag.kt index 359779fd8a..3fa613d097 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/log/LoggerTag.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/log/LoggerTag.kt @@ -19,3 +19,4 @@ package io.element.android.libraries.push.impl.log import io.element.android.libraries.core.log.logger.LoggerTag internal val pushLoggerTag = LoggerTag("Push") +internal val notificationLoggerTag = LoggerTag("Notification", pushLoggerTag) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt index 209c42f4ae..7c083555e4 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt @@ -17,6 +17,7 @@ package io.element.android.libraries.push.impl.notifications import io.element.android.libraries.push.impl.notifications.model.* +import io.element.android.services.appnavstate.api.AppNavigationState import timber.log.Timber import javax.inject.Inject @@ -26,12 +27,16 @@ class NotifiableEventProcessor @Inject constructor( private val outdatedDetector: OutdatedEventDetector, ) { - fun process(queuedEvents: List, currentRoomId: String?, currentThreadId: String?, renderedEvents: ProcessedEvents): ProcessedEvents { + fun process( + queuedEvents: List, + appNavigationState: AppNavigationState?, + renderedEvents: ProcessedEvents, + ): ProcessedEvents { val processedEvents = queuedEvents.map { val type = when (it) { is InviteNotifiableEvent -> ProcessedEvent.Type.KEEP is NotifiableMessageEvent -> when { - it.shouldIgnoreMessageEventInRoom(currentRoomId, currentThreadId) -> { + it.shouldIgnoreMessageEventInRoom(appNavigationState) -> { ProcessedEvent.Type.REMOVE .also { Timber.d("notification message removed due to currently viewing the same room or thread") } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt index d9c3a8a167..487147eacb 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt @@ -15,13 +15,28 @@ */ package io.element.android.libraries.push.impl.notifications +import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.notification.NotificationData +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent +import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails +import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType +import io.element.android.libraries.push.impl.log.pushLoggerTag import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.systemclock.SystemClock +import timber.log.Timber import javax.inject.Inject +private val loggerTag = LoggerTag("NotifiableEventResolver", pushLoggerTag) + /** * The notifiable event resolver is able to create a NotifiableEvent (view model for notifications) from an sdk Event. * It is used as a bridge between the Event Thread and the NotificationDrawerManager. @@ -33,232 +48,92 @@ class NotifiableEventResolver @Inject constructor( // private val noticeEventFormatter: NoticeEventFormatter, // private val displayableEventFormatter: DisplayableEventFormatter, private val clock: SystemClock, + private val matrixAuthenticationService: MatrixAuthenticationService, private val buildMeta: BuildMeta, ) { - suspend fun resolveEvent(/*event: Event, session: Session, isNoisy: Boolean*/): NotifiableEvent? { - return TODO() - /* - val roomID = event.roomId ?: return null - val eventId = event.eventId ?: return null - if (event.getClearType() == EventType.STATE_ROOM_MEMBER) { - return resolveStateRoomEvent(event, session, canBeReplaced = false, isNoisy = isNoisy) - } - val timelineEvent = session.getRoom(roomID)?.getTimelineEvent(eventId) ?: return null - return when { - event.supportsNotification() || event.type == EventType.ENCRYPTED -> { - resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy) - } - else -> { - // If the event can be displayed, display it as is - Timber.w("NotifiableEventResolver Received an unsupported event matching a bing rule") - // TODO Better event text display - val bodyPreview = event.type ?: EventType.MISSING_TYPE - - SimpleNotifiableEvent( - session.myUserId, - eventId = event.eventId!!, - editedEventId = timelineEvent.getEditedEventId(), - noisy = false, // will be updated - timestamp = event.originServerTs ?: clock.epochMillis(), - description = bodyPreview, - title = stringProvider.getString(StringR.string.notification_unknown_new_event), - soundName = null, - type = event.type, - canBeReplaced = false - ) - } - } - - */ - } - - suspend fun resolveInMemoryEvent(/*session: Session, event: Event, canBeReplaced: Boolean*/): NotifiableEvent? { - TODO() - /* - if (!event.supportsNotification()) return null - - // Ignore message edition - if (event.isEdition()) return null - - val actions = session.pushRuleService().getActions(event) - val notificationAction = actions.toNotificationAction() - - return if (notificationAction.shouldNotify) { - val user = session.getUserOrDefault(event.senderId!!) - - val timelineEvent = TimelineEvent( - root = event, - localId = -1, - eventId = event.eventId!!, - displayIndex = 0, - senderInfo = SenderInfo( - userId = user.userId, - displayName = user.toMatrixItem().getBestName(), - isUniqueDisplayName = true, - avatarUrl = user.avatarUrl - ) + suspend fun resolveEvent(userId: String, roomId: String, eventId: String): NotifiableEvent? { + // Restore session + val session = matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull() ?: return null + // TODO EAx, no need for a session? + val notificationData = session.let {// TODO Use make the app crashes + it.notificationService().getNotification( + userId = userId, + roomId = roomId, + eventId = eventId, ) - resolveMessageEvent(timelineEvent, session, canBeReplaced = canBeReplaced, isNoisy = !notificationAction.soundName.isNullOrBlank()) - } else { - Timber.d("Matched push rule is set to not notify") - null - } - - */ - } - - private suspend fun resolveMessageEvent(/*event: TimelineEvent, session: Session, canBeReplaced: Boolean, isNoisy: Boolean*/): NotifiableMessageEvent? { - TODO() - /* - // The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...) - val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/) - - return if (room == null) { - Timber.e("## Unable to resolve room for eventId [$event]") - // Ok room is not known in store, but we can still display something - val body = displayableEventFormatter.format(event, isDm = false, appendAuthor = false) - val roomName = stringProvider.getString(StringR.string.notification_unknown_room_name) - val senderDisplayName = event.senderInfo.disambiguatedDisplayName - - NotifiableMessageEvent( - eventId = event.root.eventId!!, - editedEventId = event.getEditedEventId(), - canBeReplaced = canBeReplaced, - timestamp = event.root.originServerTs ?: 0, - noisy = isNoisy, - senderName = senderDisplayName, - senderId = event.root.senderId, - body = body.toString(), - imageUriString = event.fetchImageIfPresent(session)?.toString(), - roomId = event.root.roomId!!, - threadId = event.root.getRootThreadEventId(), - roomName = roomName, - matrixID = session.myUserId - ) - } else { - event.attemptToDecryptIfNeeded(session) - // only convert encrypted messages to NotifiableMessageEvents - when { - event.root.supportsNotification() -> { - val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString() - val roomName = room.roomSummary()?.displayName ?: "" - val senderDisplayName = event.senderInfo.disambiguatedDisplayName - - NotifiableMessageEvent( - eventId = event.root.eventId!!, - editedEventId = event.getEditedEventId(), - canBeReplaced = canBeReplaced, - timestamp = event.root.originServerTs ?: 0, - noisy = isNoisy, - senderName = senderDisplayName, - senderId = event.root.senderId, - body = body, - imageUriString = event.fetchImageIfPresent(session)?.toString(), - roomId = event.root.roomId!!, - threadId = event.root.getRootThreadEventId(), - roomName = roomName, - roomIsDirect = room.roomSummary()?.isDirect ?: false, - roomAvatarPath = session.contentUrlResolver() - .resolveThumbnail( - room.roomSummary()?.avatarUrl, - 250, - 250, - ContentUrlResolver.ThumbnailMethod.SCALE - ), - senderAvatarPath = session.contentUrlResolver() - .resolveThumbnail( - event.senderInfo.avatarUrl, - 250, - 250, - ContentUrlResolver.ThumbnailMethod.SCALE - ), - matrixID = session.myUserId, - soundName = null - ) - } - else -> null + }.fold( + { + it + }, + { + Timber.tag(loggerTag.value).e(it, "Unable to resolve event.") + null } - } + ).orDefault(roomId, eventId) - */ + return notificationData.asNotifiableEvent(userId, roomId, eventId) } - - /* - private suspend fun TimelineEvent.attemptToDecryptIfNeeded(session: Session) { - if (root.isEncrypted() && root.mxDecryptionResult == null) { - // TODO use a global event decryptor? attache to session and that listen to new sessionId? - // for now decrypt sync - try { - val result = session.cryptoService().decryptEvent(root, root.roomId + UUID.randomUUID().toString()) - root.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, - isSafe = result.isSafe - ) - } catch (ignore: MXCryptoError) { - } - } - } - */ - - /* - private suspend fun TimelineEvent.fetchImageIfPresent(session: Session): Uri? { - return when { - root.isEncrypted() && root.mxDecryptionResult == null -> null - root.isImageMessage() -> downloadAndExportImage(session) - else -> null - } - } - */ - - /* - private suspend fun TimelineEvent.downloadAndExportImage(session: Session): Uri? { - return kotlin.runCatching { - getVectorLastMessageContent()?.takeAs()?.let { imageMessage -> - val fileService = session.fileService() - fileService.downloadFile(imageMessage) - fileService.getTemporarySharableURI(imageMessage) - } - }.onFailure { - Timber.e(it, "Failed to download and export image for notification") - }.getOrNull() - } - */ - - /* - private fun resolveStateRoomEvent(event: Event, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent? { - val content = event.content?.toModel() ?: return null - val roomId = event.roomId ?: return null - val dName = event.senderId?.let { session.roomService().getRoomMember(it, roomId)?.displayName } - if (Membership.INVITE == content.membership) { - val roomSummary = session.getRoomSummary(roomId) - val body = noticeEventFormatter.format(event, dName, isDm = roomSummary?.isDirect.orFalse()) - ?: stringProvider.getString(StringR.string.notification_new_invitation) - return InviteNotifiableEvent( - session.myUserId, - eventId = event.eventId!!, - editedEventId = null, - canBeReplaced = canBeReplaced, - roomId = roomId, - roomName = roomSummary?.displayName, - timestamp = event.originServerTs ?: 0, - noisy = isNoisy, - title = stringProvider.getString(StringR.string.notification_new_invitation), - description = body.toString(), - soundName = null, // will be set later - type = event.getClearType() - ) - } else { - Timber.e("## unsupported notifiable event for eventId [${event.eventId}]") - if (buildMeta.lowPrivacyLoggingEnabled) { - Timber.e("## unsupported notifiable event for event [$event]") - } - // TODO generic handling? - } - return null - } - */ +} + +private fun NotificationData.asNotifiableEvent(userId: String, roomId: String, eventId: String): NotifiableEvent { + return NotifiableMessageEvent( + sessionId = userId, + roomId = roomId, + eventId = eventId, + editedEventId = null, + canBeReplaced = true, + noisy = false, + timestamp = System.currentTimeMillis(), + senderName = null, + senderId = null, + body = "$eventId in $roomId", + imageUriString = null, + threadId = null, + roomName = null, + roomIsDirect = false, + roomAvatarPath = null, + senderAvatarPath = null, + soundName = null, + outGoingMessage = false, + outGoingMessageFailed = false, + isRedacted = false, + isUpdated = false + ) +} + +/** + * TODO This is a temporary method for EAx + */ +private fun NotificationData?.orDefault(roomId: String, eventId: String): NotificationData { + return this ?: NotificationData( + item = MatrixTimelineItem.Event( + event = EventTimelineItem( + uniqueIdentifier = eventId, + eventId = EventId(eventId), + isEditable = false, + isLocal = false, + isOwn = false, + isRemote = false, + localSendState = null, + reactions = emptyList(), + sender = UserId(""), + senderProfile = ProfileTimelineDetails.Unavailable, + timestamp = System.currentTimeMillis(), + content = MessageContent( + body = eventId, + inReplyTo = null, + isEdited = false, + type = TextMessageType( + body = eventId, + formatted = null + ) + ) + ), + ), + title = roomId, + subtitle = eventId, + isNoisy = false, + avatarUrl = null, + ) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt index b2a7129998..56054bbef8 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt @@ -27,7 +27,6 @@ import javax.inject.Inject data class NotificationActionIds @Inject constructor( private val buildMeta: BuildMeta, ) { - val join = "${buildMeta.applicationId}.NotificationActions.JOIN_ACTION" val reject = "${buildMeta.applicationId}.NotificationActions.REJECT_ACTION" val quickLaunch = "${buildMeta.applicationId}.NotificationActions.QUICK_LAUNCH_ACTION" diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt index 21a006e3f4..1139beccbd 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt @@ -21,11 +21,15 @@ import android.content.Context import android.content.Intent import androidx.core.app.RemoteInput import io.element.android.libraries.architecture.bindings +import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.push.impl.log.notificationLoggerTag import io.element.android.services.analytics.api.AnalyticsTracker import io.element.android.services.toolbox.api.systemclock.SystemClock import timber.log.Timber import javax.inject.Inject +private val loggerTag = LoggerTag("NotificationBroadcastReceiver", notificationLoggerTag) + /** * Receives actions broadcast by notification (on click, on dismiss, inline replies, etc.). */ @@ -41,37 +45,38 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent == null || context == null) return context.bindings().inject(this) - Timber.v("NotificationBroadcastReceiver received : $intent") + Timber.tag(loggerTag.value).v("NotificationBroadcastReceiver received : $intent") + val sessionId = intent.extras?.getString(KEY_SESSION_ID) ?: return when (intent.action) { actionIds.smartReply -> handleSmartReply(intent, context) actionIds.dismissRoom -> intent.getStringExtra(KEY_ROOM_ID)?.let { roomId -> - notificationDrawerManager.updateEvents { it.clearMessagesForRoom(roomId) } + notificationDrawerManager.updateEvents { it.clearMessagesForRoom(sessionId, roomId) } } actionIds.dismissSummary -> - notificationDrawerManager.clearAllEvents() + notificationDrawerManager.clearAllEvents(sessionId) actionIds.markRoomRead -> intent.getStringExtra(KEY_ROOM_ID)?.let { roomId -> - notificationDrawerManager.updateEvents { it.clearMessagesForRoom(roomId) } - handleMarkAsRead(roomId) + notificationDrawerManager.updateEvents { it.clearMessagesForRoom(sessionId, roomId) } + handleMarkAsRead(sessionId, roomId) } actionIds.join -> { intent.getStringExtra(KEY_ROOM_ID)?.let { roomId -> - notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(roomId) } - handleJoinRoom(roomId) + notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(sessionId, roomId) } + handleJoinRoom(sessionId, roomId) } } actionIds.reject -> { intent.getStringExtra(KEY_ROOM_ID)?.let { roomId -> - notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(roomId) } - handleRejectRoom(roomId) + notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(sessionId, roomId) } + handleRejectRoom(sessionId, roomId) } } } } - private fun handleJoinRoom(roomId: String) { + private fun handleJoinRoom(sessionId: String, roomId: String) { /* activeSessionHolder.getSafeActiveSession()?.let { session -> val room = session.getRoom(roomId) @@ -88,7 +93,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { */ } - private fun handleRejectRoom(roomId: String) { + private fun handleRejectRoom(sessionId: String, roomId: String) { /* activeSessionHolder.getSafeActiveSession()?.let { session -> session.coroutineScope.launch { @@ -99,7 +104,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { */ } - private fun handleMarkAsRead(roomId: String) { + private fun handleMarkAsRead(sessionId: String, roomId: String) { /* activeSessionHolder.getActiveSession().let { session -> val room = session.getRoom(roomId) @@ -115,6 +120,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { private fun handleSmartReply(intent: Intent, context: Context) { val message = getReplyMessage(intent) + val sessionId = intent.getStringExtra(KEY_SESSION_ID) val roomId = intent.getStringExtra(KEY_ROOM_ID) val threadId = intent.getStringExtra(KEY_THREAD_ID) @@ -234,6 +240,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { } companion object { + const val KEY_SESSION_ID = "sessionID" const val KEY_ROOM_ID = "roomID" const val KEY_THREAD_ID = "threadID" const val KEY_TEXT_REPLY = "key_text_reply" diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt index 838356b370..c8331ed8cd 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt @@ -26,8 +26,6 @@ import io.element.android.libraries.di.ApplicationContext import timber.log.Timber import javax.inject.Inject -const val TEMPORARY_ID = 101 - class NotificationDisplayer @Inject constructor( @ApplicationContext private val context: Context, ) { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt index 1bf3e68f7f..35a3dd526a 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt @@ -30,6 +30,10 @@ import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreMessageEventInRoom +import io.element.android.services.appnavstate.api.AppNavigationState +import io.element.android.services.appnavstate.api.AppNavigationStateService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -42,31 +46,24 @@ import javax.inject.Inject class NotificationDrawerManager @Inject constructor( @ApplicationContext context: Context, private val pushDataStore: PushDataStore, - // private val activeSessionDataSource: ActiveSessionDataSource, private val notifiableEventProcessor: NotifiableEventProcessor, private val notificationRenderer: NotificationRenderer, private val notificationEventPersistence: NotificationEventPersistence, private val filteredEventDetector: FilteredEventDetector, + private val appNavigationStateService: AppNavigationStateService, + private val coroutineScope: CoroutineScope, private val buildMeta: BuildMeta, ) { private val handlerThread: HandlerThread = HandlerThread("NotificationDrawerManager", Thread.MIN_PRIORITY) private var backgroundHandler: Handler - // TODO Multi-session: this will have to be improved - /* - private val currentSession: Session? - get() = activeSessionDataSource.currentValue?.orNull() - - */ - /** * Lazily initializes the NotificationState as we rely on having a current session in order to fetch the persisted queue of events. */ private val notificationState by lazy { createInitialNotificationState() } private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size) - private var currentRoomId: String? = null - private var currentThreadId: String? = null + private var currentAppNavigationState: AppNavigationState? = null private val firstThrottler = FirstThrottler(200) private var useCompleteNotificationFormat = pushDataStore.useCompleteNotificationFormat() @@ -74,6 +71,31 @@ class NotificationDrawerManager @Inject constructor( init { handlerThread.start() backgroundHandler = Handler(handlerThread.looper) + // Observe application state + coroutineScope.launch { + appNavigationStateService.appNavigationStateFlow + .collect { onAppNavigationStateChange(it) } + } + } + + private fun onAppNavigationStateChange(appNavigationState: AppNavigationState) { + currentAppNavigationState = appNavigationState + when (appNavigationState) { + AppNavigationState.Root -> {} + is AppNavigationState.Session -> {} + is AppNavigationState.Space -> {} + is AppNavigationState.Room -> { + // Cleanup notification for current room + onEnteringRoom(appNavigationState.parentSpace.parentSession.sessionId.value, appNavigationState.roomId.value) + } + is AppNavigationState.Thread -> { + onEnteringThread( + appNavigationState.parentRoom.parentSpace.parentSession.sessionId.value, + appNavigationState.parentRoom.roomId.value, + appNavigationState.threadId.value + ) + } + } } private fun createInitialNotificationState(): NotificationState { @@ -114,21 +136,17 @@ class NotificationDrawerManager @Inject constructor( /** * Clear all known events and refresh the notification drawer. */ - fun clearAllEvents() { - updateEvents { it.clear() } + fun clearAllEvents(sessionId: String) { + updateEvents { it.clearMessagesForSession(sessionId) } } /** * Should be called when the application is currently opened and showing timeline for the given roomId. * Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room. */ - fun setCurrentRoom(roomId: String?) { + private fun onEnteringRoom(sessionId: String, roomId: String) { updateEvents { - val hasChanged = roomId != currentRoomId - currentRoomId = roomId - if (hasChanged && roomId != null) { - it.clearMessagesForRoom(roomId) - } + it.clearMessagesForRoom(sessionId, roomId) } } @@ -136,18 +154,13 @@ class NotificationDrawerManager @Inject constructor( * Should be called when the application is currently opened and showing timeline for the given threadId. * Used to ignore events related to that thread (no need to display notification) and clean any existing notification on this room. */ - fun setCurrentThread(threadId: String?) { + private fun onEnteringThread(sessionId: String, roomId: String, threadId: String) { updateEvents { - val hasChanged = threadId != currentThreadId - currentThreadId = threadId - currentRoomId?.let { roomId -> - if (hasChanged && threadId != null) { - it.clearMessagesForThread(roomId, threadId) - } - } + it.clearMessagesForThread(sessionId, roomId, threadId) } } + // TODO EAx Must be per account fun notificationStyleChanged() { updateEvents { val newSettings = pushDataStore.useCompleteNotificationFormat() @@ -189,7 +202,7 @@ class NotificationDrawerManager @Inject constructor( private fun refreshNotificationDrawerBg() { Timber.v("refreshNotificationDrawerBg()") val eventsToRender = notificationState.updateQueuedEvents(this) { queuedEvents, renderedEvents -> - notifiableEventProcessor.process(queuedEvents.rawEvents(), currentRoomId, currentThreadId, renderedEvents).also { + notifiableEventProcessor.process(queuedEvents.rawEvents(), currentAppNavigationState, renderedEvents).also { queuedEvents.clearAndAdd(it.onlyKeptEvents()) } } @@ -198,9 +211,7 @@ class NotificationDrawerManager @Inject constructor( Timber.d("Skipping notification update due to event list not changing") } else { notificationState.clearAndAddRenderedEvents(eventsToRender) - // TODO EAx - //val session = currentSession ?: return - //renderEvents(session, eventsToRender) + renderEvents(eventsToRender) persistEvents() } } @@ -211,37 +222,28 @@ class NotificationDrawerManager @Inject constructor( } } - private fun renderEvents(/*session: Session, eventsToRender: List>*/) { - /* TODO EAx - val user = session.getUserOrDefault(session.myUserId) - // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash - val myUserDisplayName = user.toMatrixItem().getBestName() - val myUserAvatarUrl = session.contentUrlResolver().resolveThumbnail( - contentUrl = user.avatarUrl, - width = avatarSize, - height = avatarSize, - method = ContentUrlResolver.ThumbnailMethod.SCALE - ) - notificationRenderer.render(session.myUserId, myUserDisplayName, myUserAvatarUrl, useCompleteNotificationFormat, eventsToRender) + private fun renderEvents(eventsToRender: List>) { + // Group by sessionId + val eventsForSessions = eventsToRender.groupBy { + it.event.sessionId + } - */ + eventsForSessions.forEach { (sessionId, notifiableEvents) -> + // TODO EAx val user = session.getUserOrDefault(session.myUserId) + // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash + val myUserDisplayName = "Todo display name" // user.toMatrixItem().getBestName() + // TODO EAx avatar URL + val myUserAvatarUrl = null // session.contentUrlResolver().resolveThumbnail( + // contentUrl = user.avatarUrl, + // width = avatarSize, + // height = avatarSize, + // method = ContentUrlResolver.ThumbnailMethod.SCALE + //) + notificationRenderer.render(sessionId, myUserDisplayName, myUserAvatarUrl, useCompleteNotificationFormat, notifiableEvents) + } } fun shouldIgnoreMessageEventInRoom(resolvedEvent: NotifiableMessageEvent): Boolean { - return resolvedEvent.shouldIgnoreMessageEventInRoom(currentRoomId, currentThreadId) - } - - /** - * Temporary notification for EAx - */ - fun displayTemporaryNotification() { - notificationRenderer.displayTemporaryNotification() - } - - companion object { - const val SUMMARY_NOTIFICATION_ID = 0 - const val ROOM_MESSAGES_NOTIFICATION_ID = 1 - const val ROOM_EVENT_NOTIFICATION_ID = 2 - const val ROOM_INVITATION_NOTIFICATION_ID = 3 + return resolvedEvent.shouldIgnoreMessageEventInRoom(currentAppNavigationState) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt index 7766ed04e8..09f451c501 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt @@ -23,7 +23,7 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableMess import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent import timber.log.Timber -data class NotificationEventQueue( +data class NotificationEventQueue constructor( private val queue: MutableList, /** * An in memory FIFO cache of the seen events. @@ -103,7 +103,7 @@ data class NotificationEventQueue( } private fun findExistingById(notifiableEvent: NotifiableEvent): NotifiableEvent? { - return queue.firstOrNull { it.eventId == notifiableEvent.eventId } + return queue.firstOrNull { it.sessionId == notifiableEvent.sessionId && it.eventId == notifiableEvent.eventId } } private fun findEdited(notifiableEvent: NotifiableEvent): NotifiableEvent? { @@ -125,19 +125,24 @@ data class NotificationEventQueue( ) } - fun clearMemberShipNotificationForRoom(roomId: String) { - Timber.d("clearMemberShipOfRoom $roomId") - queue.removeAll { it is InviteNotifiableEvent && it.roomId == roomId } + fun clearMemberShipNotificationForRoom(sessionId: String, roomId: String) { + Timber.d("clearMemberShipOfRoom $sessionId, $roomId") + queue.removeAll { it is InviteNotifiableEvent && it.sessionId == sessionId && it.roomId == roomId } } - fun clearMessagesForRoom(roomId: String) { - Timber.d("clearMessageEventOfRoom $roomId") - queue.removeAll { it is NotifiableMessageEvent && it.roomId == roomId } + fun clearMessagesForSession(sessionId: String) { + Timber.d("clearMessagesForSession $sessionId") + queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId} } - fun clearMessagesForThread(roomId: String, threadId: String) { - Timber.d("clearMessageEventOfThread $roomId, $threadId") - queue.removeAll { it is NotifiableMessageEvent && it.roomId == roomId && it.threadId == threadId } + fun clearMessagesForRoom(sessionId: String, roomId: String) { + Timber.d("clearMessageEventOfRoom $sessionId, $roomId") + queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId && it.roomId == roomId } + } + + fun clearMessagesForThread(sessionId: String, roomId: String, threadId: String) { + Timber.d("clearMessageEventOfThread $sessionId, $roomId, $threadId") + queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId && it.roomId == roomId && it.threadId == threadId } } fun rawEvents(): List = queue diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt index 5a3f008963..1ffdc210d8 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt @@ -25,18 +25,28 @@ import javax.inject.Inject private typealias ProcessedMessageEvents = List> class NotificationFactory @Inject constructor( - private val notificationUtils: NotificationUtils, - private val roomGroupMessageCreator: RoomGroupMessageCreator, - private val summaryGroupMessageCreator: SummaryGroupMessageCreator + private val notificationUtils: NotificationUtils, + private val roomGroupMessageCreator: RoomGroupMessageCreator, + private val summaryGroupMessageCreator: SummaryGroupMessageCreator ) { - fun Map.toNotifications(myUserDisplayName: String, myUserAvatarUrl: String?): List { + fun Map.toNotifications( + sessionId: String, + myUserDisplayName: String, + myUserAvatarUrl: String? + ): List { return map { (roomId, events) -> when { events.hasNoEventsToDisplay() -> RoomNotification.Removed(roomId) else -> { val messageEvents = events.onlyKeptEvents().filterNot { it.isRedacted } - roomGroupMessageCreator.createRoomMessage(messageEvents, roomId, myUserDisplayName, myUserAvatarUrl) + roomGroupMessageCreator.createRoomMessage( + sessionId = sessionId, + events = messageEvents, + roomId = roomId, + userDisplayName = myUserDisplayName, + userAvatarUrl = myUserAvatarUrl + ) } } } @@ -49,46 +59,47 @@ class NotificationFactory @Inject constructor( private fun NotifiableMessageEvent.canNotBeDisplayed() = isRedacted @JvmName("toNotificationsInviteNotifiableEvent") - fun List>.toNotifications(myUserId: String): List { + fun List>.toNotifications(): List { return map { (processed, event) -> when (processed) { ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.roomId) ProcessedEvent.Type.KEEP -> OneShotNotification.Append( - notificationUtils.buildRoomInvitationNotification(event, myUserId), - OneShotNotification.Append.Meta( - key = event.roomId, - summaryLine = event.description, - isNoisy = event.noisy, - timestamp = event.timestamp - ) + notificationUtils.buildRoomInvitationNotification(event), + OneShotNotification.Append.Meta( + key = event.roomId, + summaryLine = event.description, + isNoisy = event.noisy, + timestamp = event.timestamp + ) ) } } } @JvmName("toNotificationsSimpleNotifiableEvent") - fun List>.toNotifications(myUserId: String): List { + fun List>.toNotifications(): List { return map { (processed, event) -> when (processed) { ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId) ProcessedEvent.Type.KEEP -> OneShotNotification.Append( - notificationUtils.buildSimpleEventNotification(event, myUserId), - OneShotNotification.Append.Meta( - key = event.eventId, - summaryLine = event.description, - isNoisy = event.noisy, - timestamp = event.timestamp - ) + notificationUtils.buildSimpleEventNotification(event), + OneShotNotification.Append.Meta( + key = event.eventId, + summaryLine = event.description, + isNoisy = event.noisy, + timestamp = event.timestamp + ) ) } } } fun createSummaryNotification( - roomNotifications: List, - invitationNotifications: List, - simpleNotifications: List, - useCompleteNotificationFormat: Boolean + sessionId: String, + roomNotifications: List, + invitationNotifications: List, + simpleNotifications: List, + useCompleteNotificationFormat: Boolean ): SummaryNotification { val roomMeta = roomNotifications.filterIsInstance().map { it.meta } val invitationMeta = invitationNotifications.filterIsInstance().map { it.meta } @@ -96,30 +107,27 @@ class NotificationFactory @Inject constructor( return when { roomMeta.isEmpty() && invitationMeta.isEmpty() && simpleMeta.isEmpty() -> SummaryNotification.Removed else -> SummaryNotification.Update( - summaryGroupMessageCreator.createSummaryNotification( - roomNotifications = roomMeta, - invitationNotifications = invitationMeta, - simpleNotifications = simpleMeta, - useCompleteNotificationFormat = useCompleteNotificationFormat - ) + summaryGroupMessageCreator.createSummaryNotification( + sessionId = sessionId, + roomNotifications = roomMeta, + invitationNotifications = invitationMeta, + simpleNotifications = simpleMeta, + useCompleteNotificationFormat = useCompleteNotificationFormat + ) ) } } - - fun createTemporaryNotification(): Notification { - return notificationUtils.createTemporaryNotification() - } } sealed interface RoomNotification { data class Removed(val roomId: String) : RoomNotification data class Message(val notification: Notification, val meta: Meta) : RoomNotification { data class Meta( - val summaryLine: CharSequence, - val messageCount: Int, - val latestTimestamp: Long, - val roomId: String, - val shouldBing: Boolean + val roomId: String, + val summaryLine: CharSequence, + val messageCount: Int, + val latestTimestamp: Long, + val shouldBing: Boolean ) } } @@ -128,10 +136,10 @@ sealed interface OneShotNotification { data class Removed(val key: String) : OneShotNotification data class Append(val notification: Notification, val meta: Meta) : OneShotNotification { data class Meta( - val key: String, - val summaryLine: CharSequence, - val isNoisy: Boolean, - val timestamp: Long, + val key: String, + val summaryLine: CharSequence, + val isNoisy: Boolean, + val timestamp: Long, ) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProvider.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProvider.kt new file mode 100644 index 0000000000..3a715eb844 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProvider.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import javax.inject.Inject + +@SingleIn(AppScope::class) +class NotificationIdProvider @Inject constructor() { + fun getSummaryNotificationId(sessionId: String): Int { + return getOffset(sessionId) + SUMMARY_NOTIFICATION_ID + } + + fun getRoomMessagesNotificationId(sessionId: String): Int { + return getOffset(sessionId) + ROOM_MESSAGES_NOTIFICATION_ID + } + + fun getRoomEventNotificationId(sessionId: String): Int { + return getOffset(sessionId) + ROOM_EVENT_NOTIFICATION_ID + } + + fun getRoomInvitationNotificationId(sessionId: String): Int { + return getOffset(sessionId) + ROOM_INVITATION_NOTIFICATION_ID + } + + private fun getOffset(sessionId: String): Int { + // TODO EAx multi account: return different value for users and persist data + return 0 + } + + companion object { + private const val SUMMARY_NOTIFICATION_ID = 0 + private const val ROOM_MESSAGES_NOTIFICATION_ID = 1 + private const val ROOM_EVENT_NOTIFICATION_ID = 2 + private const val ROOM_INVITATION_NOTIFICATION_ID = 3 + } +} 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 e0fc44cca0..aa30cb04db 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt @@ -16,10 +16,6 @@ package io.element.android.libraries.push.impl.notifications import androidx.annotation.WorkerThread -import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager.Companion.ROOM_EVENT_NOTIFICATION_ID -import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager.Companion.ROOM_INVITATION_NOTIFICATION_ID -import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager.Companion.ROOM_MESSAGES_NOTIFICATION_ID -import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager.Companion.SUMMARY_NOTIFICATION_ID import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent @@ -28,13 +24,14 @@ import timber.log.Timber import javax.inject.Inject class NotificationRenderer @Inject constructor( + private val notificationIdProvider: NotificationIdProvider, private val notificationDisplayer: NotificationDisplayer, private val notificationFactory: NotificationFactory, ) { @WorkerThread fun render( - myUserId: String, + sessionId: String, myUserDisplayName: String, myUserAvatarUrl: String?, useCompleteNotificationFormat: Boolean, @@ -42,10 +39,11 @@ class NotificationRenderer @Inject constructor( ) { val (roomEvents, simpleEvents, invitationEvents) = eventsToProcess.groupByType() with(notificationFactory) { - val roomNotifications = roomEvents.toNotifications(myUserDisplayName, myUserAvatarUrl) - val invitationNotifications = invitationEvents.toNotifications(myUserId) - val simpleNotifications = simpleEvents.toNotifications(myUserId) + val roomNotifications = roomEvents.toNotifications(sessionId, myUserDisplayName, myUserAvatarUrl) + val invitationNotifications = invitationEvents.toNotifications() + val simpleNotifications = simpleEvents.toNotifications() val summaryNotification = createSummaryNotification( + sessionId = sessionId, roomNotifications = roomNotifications, invitationNotifications = invitationNotifications, simpleNotifications = simpleNotifications, @@ -55,18 +53,22 @@ class NotificationRenderer @Inject constructor( // Remove summary first to avoid briefly displaying it after dismissing the last notification if (summaryNotification == SummaryNotification.Removed) { Timber.d("Removing summary notification") - notificationDisplayer.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID) + notificationDisplayer.cancelNotificationMessage(null, notificationIdProvider.getSummaryNotificationId(sessionId)) } roomNotifications.forEach { wrapper -> when (wrapper) { is RoomNotification.Removed -> { Timber.d("Removing room messages notification ${wrapper.roomId}") - notificationDisplayer.cancelNotificationMessage(wrapper.roomId, ROOM_MESSAGES_NOTIFICATION_ID) + notificationDisplayer.cancelNotificationMessage(wrapper.roomId, notificationIdProvider.getRoomMessagesNotificationId(sessionId)) } is RoomNotification.Message -> if (useCompleteNotificationFormat) { Timber.d("Updating room messages notification ${wrapper.meta.roomId}") - notificationDisplayer.showNotificationMessage(wrapper.meta.roomId, ROOM_MESSAGES_NOTIFICATION_ID, wrapper.notification) + notificationDisplayer.showNotificationMessage( + wrapper.meta.roomId, + notificationIdProvider.getRoomMessagesNotificationId(sessionId), + wrapper.notification + ) } } } @@ -75,11 +77,15 @@ class NotificationRenderer @Inject constructor( when (wrapper) { is OneShotNotification.Removed -> { Timber.d("Removing invitation notification ${wrapper.key}") - notificationDisplayer.cancelNotificationMessage(wrapper.key, ROOM_INVITATION_NOTIFICATION_ID) + notificationDisplayer.cancelNotificationMessage(wrapper.key, notificationIdProvider.getRoomInvitationNotificationId(sessionId)) } is OneShotNotification.Append -> if (useCompleteNotificationFormat) { Timber.d("Updating invitation notification ${wrapper.meta.key}") - notificationDisplayer.showNotificationMessage(wrapper.meta.key, ROOM_INVITATION_NOTIFICATION_ID, wrapper.notification) + notificationDisplayer.showNotificationMessage( + wrapper.meta.key, + notificationIdProvider.getRoomInvitationNotificationId(sessionId), + wrapper.notification + ) } } } @@ -88,11 +94,15 @@ class NotificationRenderer @Inject constructor( when (wrapper) { is OneShotNotification.Removed -> { Timber.d("Removing simple notification ${wrapper.key}") - notificationDisplayer.cancelNotificationMessage(wrapper.key, ROOM_EVENT_NOTIFICATION_ID) + notificationDisplayer.cancelNotificationMessage(wrapper.key, notificationIdProvider.getRoomEventNotificationId(sessionId)) } is OneShotNotification.Append -> if (useCompleteNotificationFormat) { Timber.d("Updating simple notification ${wrapper.meta.key}") - notificationDisplayer.showNotificationMessage(wrapper.meta.key, ROOM_EVENT_NOTIFICATION_ID, wrapper.notification) + notificationDisplayer.showNotificationMessage( + wrapper.meta.key, + notificationIdProvider.getRoomEventNotificationId(sessionId), + wrapper.notification + ) } } } @@ -100,7 +110,11 @@ class NotificationRenderer @Inject constructor( // Update summary last to avoid briefly displaying it before other notifications if (summaryNotification is SummaryNotification.Update) { Timber.d("Updating summary notification") - notificationDisplayer.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, summaryNotification.notification) + notificationDisplayer.showNotificationMessage( + null, + notificationIdProvider.getSummaryNotificationId(sessionId), + summaryNotification.notification + ) } } } @@ -108,11 +122,6 @@ class NotificationRenderer @Inject constructor( fun cancelAllNotifications() { notificationDisplayer.cancelAllNotifications() } - - fun displayTemporaryNotification() { - val notification = notificationFactory.createTemporaryNotification() - notificationDisplayer.showNotificationMessage(null, TEMPORARY_ID, notification) - } } private fun List>.groupByType(): GroupedNotificationEvents { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt index 3fce6e0d84..65fb475db8 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt @@ -228,7 +228,7 @@ class NotificationUtils @Inject constructor( true /** TODO EAx vectorPreferences.areThreadMessagesEnabled() */ -> buildOpenThreadIntent(roomInfo, threadId) - else -> buildOpenRoomIntent(roomInfo.roomId) + else -> buildOpenRoomIntent(roomInfo.sessionId, roomInfo.roomId) } val smallIcon = R.drawable.ic_notification @@ -259,8 +259,7 @@ class NotificationUtils @Inject constructor( ) // Auto-bundling is enabled for 4 or more notifications on API 24+ (N+) // devices and all Wear devices. But we want a custom grouping, so we specify the groupID - // TODO Group should be current user display name - .setGroup(buildMeta.applicationName) + .setGroup(roomInfo.sessionId) // In order to avoid notification making sound twice (due to the summary notification) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .setSmallIcon(smallIcon) @@ -287,7 +286,8 @@ class NotificationUtils @Inject constructor( // Mark room as read val markRoomReadIntent = Intent(context, NotificationBroadcastReceiver::class.java) markRoomReadIntent.action = actionIds.markRoomRead - markRoomReadIntent.data = createIgnoredUri(roomInfo.roomId) + markRoomReadIntent.data = createIgnoredUri("markRead?${roomInfo.sessionId}&$${roomInfo.roomId}") + markRoomReadIntent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, roomInfo.sessionId) markRoomReadIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId) val markRoomReadPendingIntent = PendingIntent.getBroadcast( context, @@ -307,7 +307,7 @@ class NotificationUtils @Inject constructor( // Quick reply if (!roomInfo.hasSmartReplyError) { - buildQuickReplyIntent(roomInfo.roomId, threadId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent -> + buildQuickReplyIntent(roomInfo.sessionId, roomInfo.roomId, threadId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent -> val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY) .setLabel(stringProvider.getString(StringR.string.action_quick_reply)) .build() @@ -332,8 +332,9 @@ class NotificationUtils @Inject constructor( } val intent = Intent(context, NotificationBroadcastReceiver::class.java) - intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId) intent.action = actionIds.dismissRoom + intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, roomInfo.sessionId) + intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId) val pendingIntent = PendingIntent.getBroadcast( context.applicationContext, clock.epochMillis().toInt(), @@ -347,8 +348,7 @@ class NotificationUtils @Inject constructor( } fun buildRoomInvitationNotification( - inviteNotifiableEvent: InviteNotifiableEvent, - matrixId: String + inviteNotifiableEvent: InviteNotifiableEvent ): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) // Build the pending intent for when the notification is clicked @@ -359,7 +359,7 @@ class NotificationUtils @Inject constructor( .setOnlyAlertOnce(true) .setContentTitle(inviteNotifiableEvent.roomName ?: buildMeta.applicationName) .setContentText(inviteNotifiableEvent.description) - .setGroup(buildMeta.applicationName) + .setGroup(inviteNotifiableEvent.sessionId) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .setSmallIcon(smallIcon) .setColor(accentColor) @@ -368,7 +368,8 @@ class NotificationUtils @Inject constructor( // offer to type a quick reject button val rejectIntent = Intent(context, NotificationBroadcastReceiver::class.java) rejectIntent.action = actionIds.reject - rejectIntent.data = createIgnoredUri("$roomId&$matrixId") + rejectIntent.data = createIgnoredUri("rejectInvite?${inviteNotifiableEvent.sessionId}&$roomId") + rejectIntent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, inviteNotifiableEvent.sessionId) rejectIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) val rejectIntentPendingIntent = PendingIntent.getBroadcast( context, @@ -386,7 +387,8 @@ class NotificationUtils @Inject constructor( // offer to type a quick accept button val joinIntent = Intent(context, NotificationBroadcastReceiver::class.java) joinIntent.action = actionIds.join - joinIntent.data = createIgnoredUri("$roomId&$matrixId") + joinIntent.data = createIgnoredUri("acceptInvite?${inviteNotifiableEvent.sessionId}&$roomId") + joinIntent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, inviteNotifiableEvent.sessionId) joinIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) val joinIntentPendingIntent = PendingIntent.getBroadcast( context, @@ -433,7 +435,6 @@ class NotificationUtils @Inject constructor( fun buildSimpleEventNotification( simpleNotifiableEvent: SimpleNotifiableEvent, - matrixId: String ): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) // Build the pending intent for when the notification is clicked @@ -445,7 +446,7 @@ class NotificationUtils @Inject constructor( .setOnlyAlertOnce(true) .setContentTitle(buildMeta.applicationName) .setContentText(simpleNotifiableEvent.description) - .setGroup(buildMeta.applicationName) + .setGroup(simpleNotifiableEvent.sessionId) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .setSmallIcon(smallIcon) .setColor(accentColor) @@ -476,81 +477,45 @@ class NotificationUtils @Inject constructor( .build() } - private fun buildOpenRoomIntent(roomId: String): PendingIntent? { - return null - /* - val roomIntentTap = RoomDetailActivity.newIntent(context, TimelineArgs(roomId = roomId, switchToParentSpace = true), true) - roomIntentTap.action = actionIds.tapToView + private fun buildOpenRoomIntent(sessionId: String, roomId: String): PendingIntent? { + val roomIntent = intentProvider.getIntent(sessionId = sessionId, roomId = roomId, threadId = null) + roomIntent.action = actionIds.tapToView // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that - roomIntentTap.data = createIgnoredUri("openRoom?$roomId") + roomIntent.data = createIgnoredUri("openRoom?$sessionId&$roomId") - // Recreate the back stack - return TaskStackBuilder.create(context) - .addNextIntentWithParentStack(HomeActivity.newIntent(context, firstStartMainActivity = false)) - .addNextIntent(roomIntentTap) - .getPendingIntent( - clock.epochMillis().toInt(), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE - ) - */ + return PendingIntent.getActivity( + context, + clock.epochMillis().toInt(), + roomIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) } private fun buildOpenThreadIntent(roomInfo: RoomEventGroupInfo, threadId: String?): PendingIntent? { - return null - /* - val threadTimelineArgs = ThreadTimelineArgs( - startsThread = false, - roomId = roomInfo.roomId, - rootThreadEventId = threadId, - showKeyboard = false, - displayName = roomInfo.roomDisplayName, - avatarUrl = null, - roomEncryptionTrustLevel = null, - ) - val threadIntentTap = ThreadsActivity.newIntent( - context = context, - threadTimelineArgs = threadTimelineArgs, - threadListArgs = null, - firstStartMainActivity = true, - ) + val sessionId = roomInfo.sessionId + val roomId = roomInfo.roomId + val threadIntentTap = intentProvider.getIntent(sessionId = sessionId, roomId = roomId, threadId = threadId) threadIntentTap.action = actionIds.tapToView // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that - threadIntentTap.data = createIgnoredUri("openThread?$threadId") + threadIntentTap.data = createIgnoredUri("openThread?$sessionId&$roomId&$threadId") - val roomIntent = RoomDetailActivity.newIntent( - context = context, - timelineArgs = TimelineArgs( - roomId = roomInfo.roomId, - switchToParentSpace = true - ), - firstStartMainActivity = false - ) - // Recreate the back stack - return TaskStackBuilder.create(context) - .addNextIntentWithParentStack(HomeActivity.newIntent(context, firstStartMainActivity = false)) - .addNextIntentWithParentStack(roomIntent) - .addNextIntent(threadIntentTap) - .getPendingIntent( - clock.epochMillis().toInt(), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE - ) - */ - } - - private fun buildOpenHomePendingIntentForSummary(): PendingIntent { - TODO() - /* - val intent = HomeActivity.newIntent(context, firstStartMainActivity = false, clearNotification = true) - intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP - intent.data = createIgnoredUri("tapSummary") - val mainIntent = MainActivity.getIntentWithNextIntent(context, intent) return PendingIntent.getActivity( context, - Random.nextInt(1000), - mainIntent, + clock.epochMillis().toInt(), + threadIntentTap, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + } + + private fun buildOpenHomePendingIntentForSummary(sessionId: String): PendingIntent { + val intent = intentProvider.getIntent(sessionId = sessionId, roomId = null, threadId = null) + intent.data = createIgnoredUri("tapSummary?$sessionId") + return PendingIntent.getActivity( + context, + clock.epochMillis().toInt(), + intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) - */ } /* @@ -560,12 +525,18 @@ class NotificationUtils @Inject constructor( However, for Android devices running Marshmallow and below (API level 23 and below), it will be more appropriate to use an activity. Since you have to provide your own UI. */ - private fun buildQuickReplyIntent(roomId: String, threadId: String?, senderName: String?): PendingIntent? { + private fun buildQuickReplyIntent( + sessionId: String, + roomId: String, + threadId: String?, + senderName: String? + ): PendingIntent? { val intent: Intent if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { intent = Intent(context, NotificationBroadcastReceiver::class.java) intent.action = actionIds.smartReply - intent.data = createIgnoredUri(roomId) + intent.data = createIgnoredUri("quickReply?$sessionId&$roomId") + intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId) intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) threadId?.let { intent.putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, it) @@ -602,6 +573,7 @@ class NotificationUtils @Inject constructor( * Build the summary notification. */ fun buildSummaryListNotification( + sessionId: String, style: NotificationCompat.InboxStyle?, compatSummary: String, noisy: Boolean, @@ -615,12 +587,12 @@ class NotificationUtils @Inject constructor( // used in compat < N, after summary is built based on child notifications .setWhen(lastMessageTimestamp) .setStyle(style) - .setContentTitle(buildMeta.applicationName) + .setContentTitle(sessionId) .setCategory(NotificationCompat.CATEGORY_MESSAGE) .setSmallIcon(smallIcon) // set content text to support devices running API level < 24 .setContentText(compatSummary) - .setGroup(buildMeta.applicationName) + .setGroup(sessionId) // set this notification as the summary for the group .setGroupSummary(true) .setColor(accentColor) @@ -639,15 +611,16 @@ class NotificationUtils @Inject constructor( priority = NotificationCompat.PRIORITY_LOW } } - .setContentIntent(buildOpenHomePendingIntentForSummary()) - .setDeleteIntent(getDismissSummaryPendingIntent()) + .setContentIntent(buildOpenHomePendingIntentForSummary(sessionId)) + .setDeleteIntent(getDismissSummaryPendingIntent(sessionId)) .build() } - private fun getDismissSummaryPendingIntent(): PendingIntent { + private fun getDismissSummaryPendingIntent(sessionId: String): PendingIntent { val intent = Intent(context, NotificationBroadcastReceiver::class.java) intent.action = actionIds.dismissSummary - intent.data = createIgnoredUri("deleteSummary") + intent.data = createIgnoredUri("deleteSummary?$sessionId") + intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId) return PendingIntent.getBroadcast( context.applicationContext, 0, @@ -703,23 +676,6 @@ class NotificationUtils @Inject constructor( ) } - fun createTemporaryNotification(): Notification { - val contentIntent = intentProvider.getMainIntent() - val pendingIntent = PendingIntent.getActivity(context, 0, contentIntent, PendingIntentCompat.FLAG_IMMUTABLE) - - return NotificationCompat.Builder(context, NOISY_NOTIFICATION_CHANNEL_ID) - .setContentTitle(buildMeta.applicationName) - .setContentText(stringProvider.getString(R.string.notification_new_messages_temporary)) - .setSmallIcon(R.drawable.ic_notification) - .setLargeIcon(getBitmap(context, R.drawable.element_logo_green)) - .setColor(ContextCompat.getColor(context, R.color.notification_accent_color)) - .setPriority(NotificationCompat.PRIORITY_MAX) - .setCategory(NotificationCompat.CATEGORY_STATUS) - .setAutoCancel(true) - .setContentIntent(pendingIntent) - .build() - } - private fun getBitmap(context: Context, @DrawableRes drawableRes: Int): Bitmap? { val drawable = ResourcesCompat.getDrawable(context.resources, drawableRes, null) ?: return null val canvas = Canvas() diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomEventGroupInfo.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomEventGroupInfo.kt index b09482264b..668a4bb38a 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomEventGroupInfo.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomEventGroupInfo.kt @@ -20,8 +20,9 @@ package io.element.android.libraries.push.impl.notifications * Data class to hold information about a group of notifications for a room. */ data class RoomEventGroupInfo( + val sessionId: String, val roomId: String, - val roomDisplayName: String = "", + val roomDisplayName: String, val isDirect: Boolean = false ) { // An event in the list has not yet been display diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt index 360c5c1bb5..362cd86f4c 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt @@ -33,7 +33,13 @@ class RoomGroupMessageCreator @Inject constructor( private val notificationUtils: NotificationUtils ) { - fun createRoomMessage(events: List, roomId: String, userDisplayName: String, userAvatarUrl: String?): RoomNotification.Message { + fun createRoomMessage( + sessionId: String, + events: List, + roomId: String, + userDisplayName: String, + userAvatarUrl: String? + ): RoomNotification.Message { val lastKnownRoomEvent = events.last() val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderName ?: "" val roomIsGroup = !lastKnownRoomEvent.roomIsDirect @@ -41,7 +47,7 @@ class RoomGroupMessageCreator @Inject constructor( Person.Builder() .setName(userDisplayName) .setIcon(bitmapLoader.getUserIcon(userAvatarUrl)) - .setKey(lastKnownRoomEvent.matrixID) + .setKey(lastKnownRoomEvent.sessionId) .build() ).also { it.conversationTitle = roomName.takeIf { roomIsGroup } @@ -70,7 +76,12 @@ class RoomGroupMessageCreator @Inject constructor( return RoomNotification.Message( notificationUtils.buildMessagesListNotification( style, - RoomEventGroupInfo(roomId, roomName, isDirect = !roomIsGroup).also { + RoomEventGroupInfo( + sessionId = sessionId, + roomId = roomId, + roomDisplayName = roomName, + isDirect = !roomIsGroup, + ).also { it.hasSmartReplyError = smartReplyErrors.isNotEmpty() it.shouldBing = meta.shouldBing it.customSound = events.last().soundName 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 86b741bbd7..2826f01101 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 @@ -42,6 +42,7 @@ class SummaryGroupMessageCreator @Inject constructor( ) { fun createSummaryNotification( + sessionId: String, roomNotifications: List, invitationNotifications: List, simpleNotifications: List, @@ -71,6 +72,7 @@ class SummaryGroupMessageCreator @Inject constructor( .setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents)) return if (useCompleteNotificationFormat) { notificationUtils.buildSummaryListNotification( + sessionId, summaryInboxStyle, sumTitle, noisy = summaryIsNoisy, @@ -78,6 +80,7 @@ class SummaryGroupMessageCreator @Inject constructor( ) } else { processSimpleGroupSummary( + sessionId, summaryIsNoisy, messageCount, simpleNotifications.size, @@ -89,6 +92,7 @@ class SummaryGroupMessageCreator @Inject constructor( } private fun processSimpleGroupSummary( + sessionId: String, summaryIsNoisy: Boolean, messageEventsCount: Int, simpleEventsCount: Int, @@ -147,6 +151,7 @@ class SummaryGroupMessageCreator @Inject constructor( } } return notificationUtils.buildSummaryListNotification( + sessionId = sessionId, style = null, compatSummary = privacyTitle, noisy = summaryIsNoisy, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/InviteNotifiableEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/InviteNotifiableEvent.kt index 3c49ba742f..5bb8124f16 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/InviteNotifiableEvent.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/InviteNotifiableEvent.kt @@ -16,18 +16,18 @@ package io.element.android.libraries.push.impl.notifications.model data class InviteNotifiableEvent( - val matrixID: String?, - override val eventId: String, - override val editedEventId: String?, - override val canBeReplaced: Boolean, - val roomId: String, - val roomName: String?, - val noisy: Boolean, - val title: String, - val description: String, - val type: String?, - val timestamp: Long, - val soundName: String?, - override val isRedacted: Boolean = false, - override val isUpdated: Boolean = false + override val sessionId: String, + override val roomId: String, + override val eventId: String, + override val editedEventId: String?, + override val canBeReplaced: Boolean, + val roomName: String?, + val noisy: Boolean, + val title: String, + val description: String, + val type: String?, + val timestamp: Long, + val soundName: String?, + override val isRedacted: Boolean = false, + override val isUpdated: Boolean = false ) : NotifiableEvent diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt index bcbf614659..7324cd7042 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt @@ -21,6 +21,8 @@ import java.io.Serializable * Parent interface for all events which can be displayed as a Notification. */ sealed interface NotifiableEvent : Serializable { + val sessionId: String + val roomId: String val eventId: String val editedEventId: String? diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt index 52f3ad3be6..657c60d3b0 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt @@ -16,8 +16,14 @@ package io.element.android.libraries.push.impl.notifications.model import android.net.Uri +import io.element.android.services.appnavstate.api.AppNavigationState +import io.element.android.services.appnavstate.api.currentRoomId +import io.element.android.services.appnavstate.api.currentSessionId +import io.element.android.services.appnavstate.api.currentThreadId data class NotifiableMessageEvent( + override val sessionId: String, + override val roomId: String, override val eventId: String, override val editedEventId: String?, override val canBeReplaced: Boolean, @@ -29,13 +35,11 @@ data class NotifiableMessageEvent( // We cannot use Uri? type here, as that could trigger a // NotSerializableException when persisting this to storage val imageUriString: String?, - val roomId: String, val threadId: String?, val roomName: String?, val roomIsDirect: Boolean = false, val roomAvatarPath: String? = null, val senderAvatarPath: String? = null, - val matrixID: String? = null, val soundName: String? = null, // This is used for >N notification, as the result of a smart reply val outGoingMessage: Boolean = false, @@ -52,9 +56,12 @@ data class NotifiableMessageEvent( get() = imageUriString?.let { Uri.parse(it) } } -fun NotifiableMessageEvent.shouldIgnoreMessageEventInRoom(currentRoomId: String?, currentThreadId: String?): Boolean { - return when (currentRoomId) { +fun NotifiableMessageEvent.shouldIgnoreMessageEventInRoom( + appNavigationState: AppNavigationState? +): Boolean { + val currentSessionId = appNavigationState?.currentSessionId()?.value ?: return false + return when (val currentRoomId = appNavigationState.currentRoomId()?.value) { null -> false - else -> roomId == currentRoomId && threadId == currentThreadId + else -> sessionId == currentSessionId && roomId == currentRoomId && threadId == appNavigationState.currentThreadId()?.value } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/SimpleNotifiableEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/SimpleNotifiableEvent.kt index e1d5c3347b..5ebaea7a58 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/SimpleNotifiableEvent.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/SimpleNotifiableEvent.kt @@ -16,16 +16,17 @@ package io.element.android.libraries.push.impl.notifications.model data class SimpleNotifiableEvent( - val matrixID: String?, - override val eventId: String, - override val editedEventId: String?, - val noisy: Boolean, - val title: String, - val description: String, - val type: String?, - val timestamp: Long, - val soundName: String?, - override var canBeReplaced: Boolean, - override val isRedacted: Boolean = false, - override val isUpdated: Boolean = false + override val sessionId: String, + override val roomId: String, + override val eventId: String, + override val editedEventId: String?, + val noisy: Boolean, + val title: String, + val description: String, + val type: String?, + val timestamp: Long, + val soundName: String?, + override var canBeReplaced: Boolean, + override val isRedacted: Boolean = false, + override val isUpdated: Boolean = false ) : NotifiableEvent diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt index 280fda99e5..4250bef867 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt @@ -20,15 +20,12 @@ import android.content.Context import android.content.Intent import android.os.Handler import android.os.Looper -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.ProcessLifecycleOwner import androidx.localbroadcastmanager.content.LocalBroadcastManager import io.element.android.libraries.androidutils.network.WifiDetector import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.push.api.store.PushDataStore import io.element.android.libraries.push.impl.PushersManager import io.element.android.libraries.push.impl.clientsecret.PushClientSecret @@ -49,7 +46,6 @@ private val loggerTag = LoggerTag("PushHandler", pushLoggerTag) class PushHandler @Inject constructor( private val notificationDrawerManager: NotificationDrawerManager, private val notifiableEventResolver: NotifiableEventResolver, - // private val activeSessionHolder: ActiveSessionHolder, private val pushDataStore: PushDataStore, private val defaultPushDataStore: DefaultPushDataStore, private val pushClientSecret: PushClientSecret, @@ -95,12 +91,7 @@ class PushHandler @Inject constructor( } mUIHandler.post { - if (ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { - // we are in foreground, let the sync do the things? - Timber.tag(loggerTag.value).d("PUSH received in a foreground state, ignore") - } else { - coroutineScope.launch(Dispatchers.IO) { handleInternal(pushData) } - } + coroutineScope.launch(Dispatchers.IO) { handleInternal(pushData) } } } @@ -136,96 +127,16 @@ class PushHandler @Inject constructor( return } - // Restore session - val session = matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull() ?: return - // TODO EAx, no need for a session? - val notificationData = session.let {// TODO Use make the app crashes - it.notificationService().getNotification( - userId = userId, - roomId = pushData.roomId, - eventId = pushData.eventId, - ) + val notificationData = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId) + + if (notificationData == null) { + Timber.w("Unable to get a notification data") + return } - // TODO Remove - Timber.w("Notification: $notificationData") - // TODO Display notification - - notificationDrawerManager.displayTemporaryNotification() - - /* TODO EAx - - get the event - - display the notif - - val session = activeSessionHolder.getOrInitializeSession() - - if (session == null) { - Timber.tag(loggerTag.value).w("## Can't sync from push, no current session") - } else { - if (isEventAlreadyKnown(pushData)) { - Timber.tag(loggerTag.value).d("Ignoring push, event already known") - } else { - // Try to get the Event content faster - Timber.tag(loggerTag.value).d("Requesting event in fast lane") - getEventFastLane(session, pushData) - - Timber.tag(loggerTag.value).d("Requesting background sync") - session.syncService().requireBackgroundSync() - } - } - - */ + notificationDrawerManager.updateEvents { it.onNotifiableEventReceived(notificationData) } } catch (e: Exception) { Timber.tag(loggerTag.value).e(e, "## handleInternal() failed") } } - - /* TODO EAx - private suspend fun getEventFastLane(session: Session, pushData: PushData) { - pushData.roomId ?: return - pushData.eventId ?: return - - if (wifiDetector.isConnectedToWifi().not()) { - Timber.tag(loggerTag.value).d("No WiFi network, do not get Event") - return - } - - Timber.tag(loggerTag.value).d("Fast lane: start request") - val event = tryOrNull { session.eventService().getEvent(pushData.roomId, pushData.eventId) } ?: return - - val resolvedEvent = notifiableEventResolver.resolveInMemoryEvent(session, event, canBeReplaced = true) - - if (resolvedEvent is NotifiableMessageEvent) { - // If the room is currently displayed, we will not show a notification, so no need to get the Event faster - if (notificationDrawerManager.shouldIgnoreMessageEventInRoom(resolvedEvent)) { - return - } - } - - resolvedEvent - ?.also { Timber.tag(loggerTag.value).d("Fast lane: notify drawer") } - ?.let { - notificationDrawerManager.updateEvents { it.onNotifiableEventReceived(resolvedEvent) } - } - } - - */ - - // check if the event was not yet received - // a previous catchup might have already retrieved the notified event - private fun isEventAlreadyKnown(pushData: PushData): Boolean { - /* TODO EAx - if (pushData.eventId != null && pushData.roomId != null) { - try { - val session = activeSessionHolder.getSafeActiveSession() ?: return false - val room = session.getRoom(pushData.roomId) ?: return false - return room.getTimelineEvent(pushData.eventId) != null - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined") - } - } - - */ - return false - } } diff --git a/libraries/push/impl/src/main/res/values/temporary.xml b/libraries/push/impl/src/main/res/values/temporary.xml index e7b8618cc2..b560669f57 100644 --- a/libraries/push/impl/src/main/res/values/temporary.xml +++ b/libraries/push/impl/src/main/res/values/temporary.xml @@ -25,7 +25,6 @@ Silent notifications Call New Messages - You have new message(s) Mark as read Join Reject