Fix issues on Notification Event resolver

This commit is contained in:
Valere 2019-06-22 17:08:45 +02:00 committed by Benoit Marty
parent 6743dc6273
commit 4e6b34b9d1
11 changed files with 151 additions and 131 deletions

View file

@ -23,7 +23,7 @@ import io.reactivex.Observable
class RxRoom(private val room: Room) { class RxRoom(private val room: Room) {
fun liveRoomSummary(): Observable<RoomSummary> { fun liveRoomSummary(): Observable<RoomSummary> {
return room.roomSummary.asObservable() return room.liveRoomSummary.asObservable()
} }
fun liveRoomMemberIds(): Observable<List<String>> { fun liveRoomMemberIds(): Observable<List<String>> {

View file

@ -47,6 +47,8 @@ interface Room :
* A live [RoomSummary] associated with the room * A live [RoomSummary] associated with the room
* You can observe this summary to get dynamic data from this room. * You can observe this summary to get dynamic data from this room.
*/ */
val roomSummary: LiveData<RoomSummary> val liveRoomSummary: LiveData<RoomSummary>
val roomSummary: RoomSummary?
} }

View file

@ -39,4 +39,6 @@ data class CallInviteContent(
} }
} }
fun isVideo(): Boolean = offer.sdp.contains(Offer.SDP_VIDEO)
} }

View file

@ -52,7 +52,7 @@ internal class DefaultRoom(
RelationService by relationService, RelationService by relationService,
MembershipService by roomMembersService { MembershipService by roomMembersService {
override val roomSummary: LiveData<RoomSummary> by lazy { override val liveRoomSummary: LiveData<RoomSummary> by lazy {
val liveRealmData = RealmLiveData<RoomSummaryEntity>(monarchy.realmConfiguration) { realm -> val liveRealmData = RealmLiveData<RoomSummaryEntity>(monarchy.realmConfiguration) { realm ->
RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
} }
@ -68,6 +68,15 @@ internal class DefaultRoom(
} }
} }
override val roomSummary: RoomSummary?
get() {
var sum: RoomSummaryEntity? = null
monarchy.runTransactionSync {
sum = RoomSummaryEntity.where(it, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME).findFirst()
}
return sum?.asDomain()
}
override fun isEncrypted(): Boolean { override fun isEncrypted(): Boolean {
return cryptoService.isRoomEncrypted(roomId) return cryptoService.isRoomEncrypted(roomId)
} }

View file

@ -175,7 +175,10 @@ class PushrulesConditionTest {
return _numberOfJoinedMembers return _numberOfJoinedMembers
} }
override val roomSummary: LiveData<RoomSummary> override val liveRoomSummary: LiveData<RoomSummary>
get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates.
override val roomSummary: RoomSummary?
get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates. get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates.
override fun createTimeline(eventId: String?, allowedTypes: List<String>?): Timeline { override fun createTimeline(eventId: String?, allowedTypes: List<String>?): Timeline {

View file

@ -49,10 +49,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
private val notificationDrawerManager by inject<NotificationDrawerManager>() private val notificationDrawerManager by inject<NotificationDrawerManager>()
private val pusherManager by inject<PushersManager>() private val pusherManager by inject<PushersManager>()
private val notifiableEventResolver by lazy { private val notifiableEventResolver by inject<NotifiableEventResolver>()
NotifiableEventResolver(this)
}
// UI handler // UI handler
private val mUIHandler by lazy { private val mUIHandler by lazy {
Handler(Looper.getMainLooper()) Handler(Looper.getMainLooper())
@ -69,8 +66,8 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
return return
} }
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.i("## onMessageReceived()" + message.data.toString()) Timber.i("## onMessageReceived() %s", message.data.toString())
Timber.i("## onMessageReceived() from FCM with priority " + message.priority) Timber.i("## onMessageReceived() from FCM with priority %s", message.priority)
} }
mUIHandler.post { mUIHandler.post {
if (ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { if (ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
@ -168,7 +165,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
val room = session.getRoom(roomId) ?: return false val room = session.getRoom(roomId) ?: return false
return room.getTimeLineEvent(eventId) != null return room.getTimeLineEvent(eventId) != null
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined " + e.message) Timber.e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined")
} }
} }
@ -209,44 +206,39 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
return return
} else { } else {
val event = parseEvent(data) val event = parseEvent(data) ?: return
if (event?.roomId == null) {
//unsupported event val notifiableEvent = notifiableEventResolver.resolveEvent(event, session)
Timber.e("Received an event with no room id")
return if (notifiableEvent == null) {
Timber.e("Unsupported notifiable event ${eventId}")
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.e("--> ${event}")
}
} else { } else {
var notifiableEvent = notifiableEventResolver.resolveEvent(event, session)
if (notifiableEvent == null) { if (notifiableEvent is NotifiableMessageEvent) {
Timber.e("Unsupported notifiable event ${eventId}") if (TextUtils.isEmpty(notifiableEvent.senderName)) {
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { notifiableEvent.senderName = data["sender_display_name"]
Timber.e("--> ${event}") ?: data["sender"] ?: ""
} }
} else { if (TextUtils.isEmpty(notifiableEvent.roomName)) {
notifiableEvent.roomName = findRoomNameBestEffort(data, session) ?: ""
if (notifiableEvent is NotifiableMessageEvent) {
if (TextUtils.isEmpty(notifiableEvent.senderName)) {
notifiableEvent.senderName = data["sender_display_name"]
?: data["sender"] ?: ""
}
if (TextUtils.isEmpty(notifiableEvent.roomName)) {
notifiableEvent.roomName = findRoomNameBestEffort(data, session) ?: ""
}
} }
notifiableEvent.isPushGatewayEvent = true
notifiableEvent.matrixID = session.sessionParams.credentials.userId
notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
notificationDrawerManager.refreshNotificationDrawer()
} }
notifiableEvent.isPushGatewayEvent = true
notifiableEvent.matrixID = session.sessionParams.credentials.userId
notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
notificationDrawerManager.refreshNotificationDrawer()
} }
} }
} }
private fun findRoomNameBestEffort(data: Map<String, String>, session: Session?): String? { private fun findRoomNameBestEffort(data: Map<String, String>, session: Session?): String? {
var roomName: String? = data["room_name"] val roomName: String? = data["room_name"]
val roomId = data["room_id"] val roomId = data["room_id"]
if (null == roomName && null != roomId) { if (null == roomName && null != roomId) {
// Try to get the room name from our store // Try to get the room name from our store
@ -281,7 +273,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
// TODO content = data.getValue("content"), // TODO content = data.getValue("content"),
originServerTs = System.currentTimeMillis()) originServerTs = System.currentTimeMillis())
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "buildEvent fails " + e.localizedMessage) Timber.e(e, "buildEvent fails ")
} }
return null return null

View file

@ -90,7 +90,7 @@ class AppModule(private val context: Context) {
} }
single { single {
OutdatedEventDetector(context) OutdatedEventDetector()
} }
single { single {
@ -98,7 +98,7 @@ class AppModule(private val context: Context) {
} }
single { single {
NotifiableEventResolver(context) NotifiableEventResolver(get(), get())
} }
factory { factory {

View file

@ -46,6 +46,22 @@ class NoticeEventFormatter(private val stringProvider: StringProvider) {
} }
} }
fun format(event: Event, senderName: String?): CharSequence? {
return when (val type = event.getClearType()) {
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName)
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName)
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName)
EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName)
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> formatCallEvent(event, senderName)
else -> {
Timber.v("Type $type not handled by this formatter")
null
}
}
}
private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? { private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? {
val content = event.getClearContent().toModel<RoomNameContent>() ?: return null val content = event.getClearContent().toModel<RoomNameContent>() ?: return null
return if (!TextUtils.isEmpty(content.name)) { return if (!TextUtils.isEmpty(content.name)) {

View file

@ -15,16 +15,19 @@
*/ */
package im.vector.riotredesign.features.notifications package im.vector.riotredesign.features.notifications
import android.content.Context import androidx.core.app.NotificationCompat
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.BuildConfig
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.preference.BingRule import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter
import timber.log.Timber import timber.log.Timber
// TODO Remove // TODO Remove
@ -39,46 +42,44 @@ class RoomState {
* The NotifiableEventResolver is the only aware of session/store, the NotificationDrawerManager has no knowledge of that, * The NotifiableEventResolver is the only aware of session/store, the NotificationDrawerManager has no knowledge of that,
* this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk. * this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk.
*/ */
class NotifiableEventResolver(val context: Context) { class NotifiableEventResolver(val stringProvider: StringProvider,
val noticeEventFormatter: NoticeEventFormatter) {
//private val eventDisplay = RiotEventDisplay(context) //private val eventDisplay = RiotEventDisplay(context)
fun resolveEvent(event: Event/*, roomState: RoomState?, bingRule: PushRule?*/, session: Session): NotifiableEvent? { fun resolveEvent(event: Event/*, roomState: RoomState?, bingRule: PushRule?*/, session: Session): NotifiableEvent? {
val roomID = event.roomId ?: return null
val eventId = event.eventId ?: return null
// val store = session.dataHandler.store val timelineEvent = session.getRoom(roomID)?.getTimeLineEvent(eventId) ?: return null
// if (store == null) {
// Log.e("## NotifiableEventResolver, unable to get store")
// //TODO notify somehow that something did fail?
// return null
// }
when (event.getClearType()) { when (event.getClearType()) {
EventType.MESSAGE -> { EventType.MESSAGE -> {
return resolveMessageEvent(event, session) return resolveMessageEvent(timelineEvent, session)
} }
// EventType.ENCRYPTED -> { EventType.ENCRYPTED -> {
// val messageEvent = resolveMessageEvent(event, bingRule, session, store) val messageEvent = resolveMessageEvent(timelineEvent, session)
// messageEvent?.lockScreenVisibility = NotificationCompat.VISIBILITY_PRIVATE messageEvent?.lockScreenVisibility = NotificationCompat.VISIBILITY_PRIVATE
// return messageEvent return messageEvent
// } }
// EventType.STATE_ROOM_MEMBER -> { EventType.STATE_ROOM_MEMBER -> {
// return resolveStateRoomEvent(event, bingRule, session, store) return resolveStateRoomEvent(event, session)
// } }
else -> { else -> {
//If the event can be displayed, display it as is //If the event can be displayed, display it as is
// eventDisplay.getTextualDisplay(event, roomState)?.toString()?.let { body -> //TODO Better event text display
// return SimpleNotifiableEvent( val bodyPreview = event.type
// session.myUserId,
// eventId = event.eventId, return SimpleNotifiableEvent(
// noisy = bingRule?.notificationSound != null, session.sessionParams.credentials.userId,
// timestamp = event.originServerTs, eventId = event.eventId!!,
// description = body, noisy = false,//will be updated
// soundName = bingRule?.notificationSound, timestamp = event.originServerTs ?: System.currentTimeMillis(),
// title = context.getString(R.string.notification_unknown_new_event), description = bodyPreview,
// type = event.type) title = stringProvider.getString(R.string.notification_unknown_new_event),
// } soundName = null,
type = event.type)
//Unsupported event //Unsupported event
Timber.w("NotifiableEventResolver Received an unsupported event matching a bing rule") Timber.w("NotifiableEventResolver Received an unsupported event matching a bing rule")
@ -88,57 +89,55 @@ class NotifiableEventResolver(val context: Context) {
} }
private fun resolveMessageEvent(event: Event, session: Session): NotifiableEvent? { private fun resolveMessageEvent(event: TimelineEvent, session: Session): NotifiableEvent? {
//If we are here, that means that the event should be notified to the user, we check now how it should be presented (sound)
// val soundName = pushRule?.notificationSound
//The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...) //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.roomId!! /*roomID cannot be null (see Matrix SDK code)*/) val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/)
if (room == null) { if (room == null) {
Timber.e("## Unable to resolve room for eventId [${event.eventId}] and roomID [${event.roomId}]") Timber.e("## Unable to resolve room for eventId [${event}]")
// Ok room is not known in store, but we can still display something // Ok room is not known in store, but we can still display something
val body = event.content?.toModel<MessageContent>()?.body val body =
?: context.getString(R.string.notification_unknown_new_event) event.annotations?.editSummary?.aggregatedContent?.toModel<MessageContent>()?.body
val roomName = context.getString(R.string.notification_unknown_room_name) ?: event.root.getClearContent().toModel<MessageContent>()?.body
val senderDisplayName = event.senderId ?: "" ?: stringProvider.getString(R.string.notification_unknown_new_event)
val roomName = stringProvider.getString(R.string.notification_unknown_room_name)
val senderDisplayName = event.senderName
val notifiableEvent = NotifiableMessageEvent( val notifiableEvent = NotifiableMessageEvent(
eventId = event.eventId ?: "", eventId = event.root.eventId!!,
timestamp = event.originServerTs ?: 0, timestamp = event.root.originServerTs ?: 0,
noisy = false,//will be updated noisy = false,//will be updated
senderName = senderDisplayName, senderName = senderDisplayName,
senderId = event.senderId, senderId = event.root.senderId,
body = body, body = body,
roomId = event.roomId ?: "", roomId = event.root.roomId!!,
roomName = roomName) roomName = roomName)
notifiableEvent.matrixID = session.sessionParams.credentials.userId notifiableEvent.matrixID = session.sessionParams.credentials.userId
// notifiableEvent.soundName = soundName
return notifiableEvent return notifiableEvent
} else { } else {
val tEvent = room.getTimeLineEvent(event.eventId!!) val body = event.root.getClearContent().toModel<MessageContent>()?.body
val body = event.content?.toModel<MessageContent>()?.body ?: stringProvider.getString(R.string.notification_unknown_new_event)
?: context.getString(R.string.notification_unknown_new_event) val roomName = room.roomSummary?.displayName ?: ""
val roomName = event.roomId ?: "room" val senderDisplayName = event.senderName ?: ""
val senderDisplayName = tEvent?.senderName ?: "?"
val notifiableEvent = NotifiableMessageEvent( val notifiableEvent = NotifiableMessageEvent(
eventId = event.eventId!!, eventId = event.root.eventId!!,
timestamp = event.originServerTs ?: 0, timestamp = event.root.originServerTs ?: 0,
noisy = false,//will be updated noisy = false,//will be updated
senderName = senderDisplayName, senderName = senderDisplayName,
senderId = event.senderId, senderId = event.root.senderId,
body = body, body = body,
roomId = event.roomId ?: "00", roomId = event.root.roomId!!,
roomName = roomName, roomName = roomName,
roomIsDirect = true) roomIsDirect = room.roomSummary?.isDirect ?: false)
notifiableEvent.matrixID = session.sessionParams.credentials.userId notifiableEvent.matrixID = session.sessionParams.credentials.userId
notifiableEvent.soundName = null notifiableEvent.soundName = null
//TODO get the avatar?
// val roomAvatarPath = session.mediaCache?.thumbnailCacheFile(room.avatarUrl, 50) // val roomAvatarPath = session.mediaCache?.thumbnailCacheFile(room.avatarUrl, 50)
// if (roomAvatarPath != null) { // if (roomAvatarPath != null) {
@ -162,21 +161,24 @@ class NotifiableEventResolver(val context: Context) {
return notifiableEvent return notifiableEvent
} }
} }
/*
private fun resolveStateRoomEvent(event: Event, bingRule: BingRule?, session: MXSession, store: IMXStore): NotifiableEvent? {
if (RoomMember.MEMBERSHIP_INVITE == event.contentAsJsonObject?.getAsJsonPrimitive("membership")?.asString) { private fun resolveStateRoomEvent(event: Event, session: Session): NotifiableEvent? {
val room = store.getRoom(event.roomId /*roomID cannot be null (see Matrix SDK code)*/) val content = event.content?.toModel<RoomMember>() ?: return null
val body = eventDisplay.getTextualDisplay(event, room.state)?.toString() val roomId = event.roomId ?: return null
?: context.getString(R.string.notification_new_invitation) val dName = event.senderId?.let { session.getUser(it)?.displayName }
if (Membership.INVITE == content.membership) {
val body = noticeEventFormatter.format(event, dName)
?: stringProvider.getString(R.string.notification_new_invitation)
return InviteNotifiableEvent( return InviteNotifiableEvent(
session.myUserId, session.sessionParams.credentials.userId,
eventId = event.eventId, eventId = event.eventId!!,
roomId = event.roomId, roomId = roomId,
timestamp = event.originServerTs, timestamp = event.originServerTs ?: 0,
noisy = bingRule?.notificationSound != null, noisy = false,//will be set later
title = context.getString(R.string.notification_new_invitation), title = stringProvider.getString(R.string.notification_new_invitation),
description = body, description = body.toString(),
soundName = bingRule?.notificationSound, soundName = null, //will be set later
type = event.getClearType(), type = event.getClearType(),
isPushGatewayEvent = false) isPushGatewayEvent = false)
} else { } else {
@ -187,6 +189,6 @@ class NotifiableEventResolver(val context: Context) {
//TODO generic handling? //TODO generic handling?
} }
return null return null
} */ }
} }

View file

@ -22,7 +22,8 @@ import android.graphics.BitmapFactory
import android.text.TextUtils import android.text.TextUtils
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.Person import androidx.core.app.Person
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.riotredesign.BuildConfig import im.vector.riotredesign.BuildConfig
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.utils.SecretStoringUtils import im.vector.riotredesign.core.utils.SecretStoringUtils
@ -36,14 +37,14 @@ import java.io.FileOutputStream
* organise them in order to display them in the notification drawer. * organise them in order to display them in the notification drawer.
* Events can be grouped into the same notification, old (already read) events can be removed to do some cleaning. * Events can be grouped into the same notification, old (already read) events can be removed to do some cleaning.
*/ */
class NotificationDrawerManager(val context: Context, private val outdatedDetector: OutdatedEventDetector?) { class NotificationDrawerManager(val context: Context,
private val outdatedDetector: OutdatedEventDetector?) {
//The first time the notification drawer is refreshed, we force re-render of all notifications //The first time the notification drawer is refreshed, we force re-render of all notifications
private var firstTime = true private var firstTime = true
private var eventList = loadEventInfo() private var eventList = loadEventInfo()
private var myUserDisplayName: String = "Todo"
private var myUserAvatarUrl: String = ""
private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size) private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size)
@ -156,17 +157,11 @@ class NotificationDrawerManager(val context: Context, private val outdatedDetect
fun refreshNotificationDrawer() { fun refreshNotificationDrawer() {
if (myUserDisplayName.isBlank()) {
// TODO
// initWithSession(Matrix.getInstance(context).defaultSession)
}
if (myUserDisplayName.isBlank()) {
// Should not happen, but myUserDisplayName cannot be blank if used to create a Person
//TODO
// return
}
val session = Matrix.getInstance().currentSession ?: return
val user = session.getUser(session.sessionParams.credentials.userId)
val myUserDisplayName = user?.displayName ?: ""
val myUserAvatarUrl = session.contentUrlResolver().resolveThumbnail(user?.avatarUrl, avatarSize, avatarSize, ContentUrlResolver.ThumbnailMethod.SCALE)
synchronized(eventList) { synchronized(eventList) {
Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER ") Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER ")

View file

@ -15,10 +15,9 @@
*/ */
package im.vector.riotredesign.features.notifications package im.vector.riotredesign.features.notifications
import android.content.Context
import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.Matrix
class OutdatedEventDetector(val context: Context) { class OutdatedEventDetector() {
/** /**
* Returns true if the given event is outdated. * Returns true if the given event is outdated.