Browse Source

Making progress on notification for multi account.

test/jme/compound-poc
Benoit Marty 1 year ago committed by Benoit Marty
parent
commit
7e7e798acf
  1. 5
      app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt
  2. 6
      libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt
  3. 1
      libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/store/PushDataStore.kt
  4. 1
      libraries/push/impl/build.gradle.kts
  5. 10
      libraries/push/impl/src/main/AndroidManifest.xml
  6. 8
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt
  7. 8
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt
  8. 1
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/log/LoggerTag.kt
  9. 9
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt
  10. 311
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
  11. 1
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt
  12. 31
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt
  13. 2
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt
  14. 118
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt
  15. 27
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt
  16. 96
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt
  17. 52
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProvider.kt
  18. 51
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt
  19. 154
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt
  20. 3
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomEventGroupInfo.kt
  21. 17
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt
  22. 5
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt
  23. 28
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/InviteNotifiableEvent.kt
  24. 2
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt
  25. 17
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt
  26. 25
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/SimpleNotifiableEvent.kt
  27. 101
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt
  28. 1
      libraries/push/impl/src/main/res/values/temporary.xml

5
app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt

@ -33,4 +33,9 @@ class IntentProviderImpl @Inject constructor( @@ -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)
}
}

6
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 @@ -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

1
libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/store/PushDataStore.kt

@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.Flow @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.Flow
interface PushDataStore {
val pushCounterFlow: Flow<Int>
// TODO Move all those settings to the per user store...
fun areNotificationEnabledForDevice(): Boolean
fun setNotificationEnabledForDevice(enabled: Boolean)

1
libraries/push/impl/build.gradle.kts

@ -46,6 +46,7 @@ dependencies { @@ -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") {

10
libraries/push/impl/src/main/AndroidManifest.xml

@ -60,5 +60,15 @@ @@ -60,5 +60,15 @@
<action android:name="org.unifiedpush.android.distributor.REGISTER" />
</intent-filter>
</receiver>
<receiver
android:name=".notifications.TestNotificationReceiver"
android:exported="false" />
<receiver
android:name=".notifications.NotificationBroadcastReceiver"
android:enabled="true"
android:exported="false" />
</application>
</manifest>

8
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt

@ -32,14 +32,6 @@ class DefaultPushService @Inject constructor( @@ -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()
}

8
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt

@ -20,7 +20,13 @@ import android.content.Intent @@ -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
}

1
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 @@ -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)

9
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt

@ -17,6 +17,7 @@ @@ -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( @@ -26,12 +27,16 @@ class NotifiableEventProcessor @Inject constructor(
private val outdatedDetector: OutdatedEventDetector,
) {
fun process(queuedEvents: List<NotifiableEvent>, currentRoomId: String?, currentThreadId: String?, renderedEvents: ProcessedEvents): ProcessedEvents {
fun process(
queuedEvents: List<NotifiableEvent>,
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") }
}

311
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt

@ -15,13 +15,28 @@ @@ -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( @@ -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
}
}.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 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
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
)
}
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
/**
* 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
)
}
else -> null
}
}
*/
}
/*
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<MessageWithAttachmentContent>()?.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<RoomMemberContent>() ?: 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
}
*/
),
),
title = roomId,
subtitle = eventId,
isNoisy = false,
avatarUrl = null,
)
}

1
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt

@ -27,7 +27,6 @@ import javax.inject.Inject @@ -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"

31
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt

@ -21,11 +21,15 @@ import android.content.Context @@ -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() { @@ -41,37 +45,38 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent == null || context == null) return
context.bindings<NotificationBroadcastReceiverBindings>().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() { @@ -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() { @@ -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() { @@ -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() { @@ -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"

2
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 @@ -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,
) {

118
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 @@ -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 @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -211,37 +222,28 @@ class NotificationDrawerManager @Inject constructor(
}
}
private fun renderEvents(/*session: Session, eventsToRender: List<ProcessedEvent<NotifiableEvent>>*/) {
/* 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<ProcessedEvent<NotifiableEvent>>) {
// 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)
}
}

27
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 @@ -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<NotifiableEvent>,
/**
* An in memory FIFO cache of the seen events.
@ -103,7 +103,7 @@ data class NotificationEventQueue( @@ -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( @@ -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<NotifiableEvent> = queue

96
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt

@ -25,18 +25,28 @@ import javax.inject.Inject @@ -25,18 +25,28 @@ import javax.inject.Inject
private typealias ProcessedMessageEvents = List<ProcessedEvent<NotifiableMessageEvent>>
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<String, ProcessedMessageEvents>.toNotifications(myUserDisplayName: String, myUserAvatarUrl: String?): List<RoomNotification> {
fun Map<String, ProcessedMessageEvents>.toNotifications(
sessionId: String,
myUserDisplayName: String,
myUserAvatarUrl: String?
): List<RoomNotification> {
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( @@ -49,46 +59,47 @@ class NotificationFactory @Inject constructor(
private fun NotifiableMessageEvent.canNotBeDisplayed() = isRedacted
@JvmName("toNotificationsInviteNotifiableEvent")
fun List<ProcessedEvent<InviteNotifiableEvent>>.toNotifications(myUserId: String): List<OneShotNotification> {
fun List<ProcessedEvent<InviteNotifiableEvent>>.toNotifications(): List<OneShotNotification> {
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<ProcessedEvent<SimpleNotifiableEvent>>.toNotifications(myUserId: String): List<OneShotNotification> {
fun List<ProcessedEvent<SimpleNotifiableEvent>>.toNotifications(): List<OneShotNotification> {
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<RoomNotification>,
invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>,
useCompleteNotificationFormat: Boolean
sessionId: String,
roomNotifications: List<RoomNotification>,
invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>,
useCompleteNotificationFormat: Boolean
): SummaryNotification {
val roomMeta = roomNotifications.filterIsInstance<RoomNotification.Message>().map { it.meta }
val invitationMeta = invitationNotifications.filterIsInstance<OneShotNotification.Append>().map { it.meta }
@ -96,30 +107,27 @@ class NotificationFactory @Inject constructor( @@ -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 { @@ -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,
)
}
}

52
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProvider.kt

@ -0,0 +1,52 @@ @@ -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
}
}

51
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt

@ -16,10 +16,6 @@ @@ -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 @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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<ProcessedEvent<NotifiableEvent>>.groupByType(): GroupedNotificationEvents {

154
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt

@ -228,7 +228,7 @@ class NotificationUtils @Inject constructor( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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")
val roomIntent = RoomDetailActivity.newIntent(
context = context,
timelineArgs = TimelineArgs(
roomId = roomInfo.roomId,
switchToParentSpace = true
),
firstStartMainActivity = false
threadIntentTap.data = createIgnoredUri("openThread?$sessionId&$roomId&$threadId")
return PendingIntent.getActivity(
context,
clock.epochMillis().toInt(),
threadIntentTap,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
)
// 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)
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,
Random.nextInt(1000),
mainIntent,
clock.epochMillis().toInt(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
)
*/
}
/*
@ -560,12 +525,18 @@ class NotificationUtils @Inject constructor( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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()

3
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 @@ -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

17
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt

@ -33,7 +33,13 @@ class RoomGroupMessageCreator @Inject constructor( @@ -33,7 +33,13 @@ class RoomGroupMessageCreator @Inject constructor(
private val notificationUtils: NotificationUtils
) {
fun createRoomMessage(events: List<NotifiableMessageEvent>, roomId: String, userDisplayName: String, userAvatarUrl: String?): RoomNotification.Message {
fun createRoomMessage(
sessionId: String,
events: List<NotifiableMessageEvent>,
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( @@ -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( @@ -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

5
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt

@ -42,6 +42,7 @@ class SummaryGroupMessageCreator @Inject constructor( @@ -42,6 +42,7 @@ class SummaryGroupMessageCreator @Inject constructor(
) {
fun createSummaryNotification(
sessionId: String,
roomNotifications: List<RoomNotification.Message.Meta>,
invitationNotifications: List<OneShotNotification.Append.Meta>,
simpleNotifications: List<OneShotNotification.Append.Meta>,
@ -71,6 +72,7 @@ class SummaryGroupMessageCreator @Inject constructor( @@ -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( @@ -78,6 +80,7 @@ class SummaryGroupMessageCreator @Inject constructor(
)
} else {
processSimpleGroupSummary(
sessionId,
summaryIsNoisy,
messageCount,
simpleNotifications.size,
@ -89,6 +92,7 @@ class SummaryGroupMessageCreator @Inject constructor( @@ -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( @@ -147,6 +151,7 @@ class SummaryGroupMessageCreator @Inject constructor(
}
}
return notificationUtils.buildSummaryListNotification(
sessionId = sessionId,
style = null,
compatSummary = privacyTitle,
noisy = summaryIsNoisy,

28
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/InviteNotifiableEvent.kt

@ -16,18 +16,18 @@ @@ -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

2
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt

@ -21,6 +21,8 @@ import java.io.Serializable @@ -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?

17
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt

@ -16,8 +16,14 @@ @@ -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( @@ -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( @@ -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
}
}

25
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/SimpleNotifiableEvent.kt

@ -16,16 +16,17 @@ @@ -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

101
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt

@ -20,15 +20,12 @@ import android.content.Context @@ -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) @@ -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( @@ -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( @@ -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,
)
}
// TODO Remove
Timber.w("Notification: $notificationData")
// TODO Display notification
notificationDrawerManager.displayTemporaryNotification()
/* TODO EAx
- get the event
- display the notif
val session = activeSessionHolder.getOrInitializeSession()
val notificationData = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId)
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()
}
if (notificationData == null) {
Timber.w("Unable to get a notification data")
return
}
*/
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
}
}

1
libraries/push/impl/src/main/res/values/temporary.xml

@ -25,7 +25,6 @@ @@ -25,7 +25,6 @@
<string name="notification_silent_notifications">Silent notifications</string>
<string name="call">Call</string>
<string name="notification_new_messages">New Messages</string>
<string name="notification_new_messages_temporary">You have new message(s)</string>
<string name="action_mark_room_read">Mark as read</string>
<string name="action_join">Join</string>
<string name="action_reject">Reject</string>

Loading…
Cancel
Save