Get Event after a Push for a faster notification display in some conditions

This commit is contained in:
Benoit Marty 2021-03-24 18:48:25 +01:00 committed by Benoit Marty
parent af023669ba
commit 96153fe92a
14 changed files with 234 additions and 41 deletions

View file

@ -17,6 +17,7 @@ Improvements 🙌:
- Add better support for empty room name fallback (#3106)
- Room list improvements (paging)
- Fix quick click action (#3127)
- Get Event after a Push for a faster notification display in some conditions
Bugfix 🐛:
- Fix bad theme change for the MainActivity

View file

@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.call.CallSignalingService
import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.EventService
import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.group.GroupService
@ -68,6 +69,7 @@ interface Session :
SignOutService,
FilterService,
TermsService,
EventService,
ProfileService,
PushRuleService,
PushersService,

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.session.events
import org.matrix.android.sdk.api.session.events.model.Event
interface EventService {
/**
* Ask the homeserver for an event content. The SDK will try to decrypt it if it is possible
* The result will not be stored into cache
*/
suspend fun getEvent(roomId: String, eventId: String): Event
}

View file

@ -38,9 +38,13 @@ internal fun isEventRead(realmConfiguration: RealmConfiguration,
Realm.getInstance(realmConfiguration).use { realm ->
val liveChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: return@use
val eventToCheck = liveChunk.timelineEvents.find(eventId)
isEventRead = if (eventToCheck == null || eventToCheck.root?.sender == userId) {
true
} else {
isEventRead = when {
eventToCheck == null -> {
// This can happen in case of fast lane Event
false
}
eventToCheck.root?.sender == userId -> true
else -> {
val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst()
?: return@use
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.displayIndex
@ -50,6 +54,7 @@ internal fun isEventRead(realmConfiguration: RealmConfiguration,
eventToCheckIndex <= readReceiptIndex
}
}
}
return isEventRead
}

View file

@ -33,6 +33,7 @@ import org.matrix.android.sdk.api.session.call.CallSignalingService
import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.EventService
import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.group.GroupService
@ -114,6 +115,7 @@ internal class DefaultSession @Inject constructor(
private val accountDataService: Lazy<AccountDataService>,
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
private val accountService: Lazy<AccountService>,
private val eventService: Lazy<EventService>,
private val defaultIdentityService: DefaultIdentityService,
private val integrationManagerService: IntegrationManagerService,
private val thirdPartyService: Lazy<ThirdPartyService>,
@ -129,6 +131,7 @@ internal class DefaultSession @Inject constructor(
FilterService by filterService.get(),
PushRuleService by pushRuleService.get(),
PushersService by pushersService.get(),
EventService by eventService.get(),
TermsService by termsService.get(),
InitialSyncProgressService by initialSyncProgressService.get(),
SecureStorageService by secureStorageService.get(),

View file

@ -32,10 +32,11 @@ import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.SessionParams
import org.matrix.android.sdk.api.auth.data.sessionId
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.accountdata.AccountDataService
import org.matrix.android.sdk.api.session.events.EventService
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
@ -75,6 +76,7 @@ import org.matrix.android.sdk.internal.network.token.AccessTokenProvider
import org.matrix.android.sdk.internal.network.token.HomeserverAccessTokenProvider
import org.matrix.android.sdk.internal.session.call.CallEventProcessor
import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor
import org.matrix.android.sdk.internal.session.events.DefaultEventService
import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapabilitiesService
import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService
import org.matrix.android.sdk.internal.session.initsync.DefaultInitialSyncProgressService
@ -357,6 +359,9 @@ internal abstract class SessionModule {
@Binds
abstract fun bindAccountDataService(service: DefaultAccountDataService): AccountDataService
@Binds
abstract fun bindEventService(service: DefaultEventService): EventService
@Binds
abstract fun bindSharedSecretStorageService(service: DefaultSharedSecretStorageService): SharedSecretStorageService

View file

@ -21,9 +21,11 @@ import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
import org.matrix.android.sdk.internal.session.SessionScope
import timber.log.Timber
import javax.inject.Inject
@SessionScope
internal class CallEventProcessor @Inject constructor(private val callSignalingHandler: CallSignalingHandler)
: EventInsertLiveProcessor {
@ -51,6 +53,15 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH
eventsToPostProcess.add(event)
}
fun shouldProcessFastLane(eventType: String): Boolean {
return eventType == EventType.CALL_INVITE
}
suspend fun processFastLane(event: Event) {
eventsToPostProcess.add(event)
onPostProcess()
}
override suspend fun onPostProcess() {
eventsToPostProcess.forEach {
dispatchToCallSignalingHandlerIfNeeded(it)
@ -60,7 +71,7 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH
private fun dispatchToCallSignalingHandlerIfNeeded(event: Event) {
val now = System.currentTimeMillis()
// TODO might check if an invite is not closed (hangup/answsered) in the same event batch?
// TODO might check if an invite is not closed (hangup/answered) in the same event batch?
event.roomId ?: return Unit.also {
Timber.w("Event with no room id ${event.eventId}")
}

View file

@ -168,6 +168,14 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
return
}
val content = event.getClearContent().toModel<CallInviteContent>() ?: return
content.callId ?: return
if (activeCallHandler.getCallWithId(content.callId) != null) {
// Call is already known, maybe due to fast lane. Ignore
Timber.d("Ignoring already known call invite")
return
}
val incomingCall = mxCallFactory.createIncomingCall(
roomId = event.roomId,
opponentUserId = event.senderId,

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.events
import org.matrix.android.sdk.api.session.events.EventService
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.session.call.CallEventProcessor
import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask
import javax.inject.Inject
internal class DefaultEventService @Inject constructor(
private val getEventTask: GetEventTask,
private val callEventProcessor: CallEventProcessor
) : EventService {
override suspend fun getEvent(roomId: String, eventId: String): Event {
val event = getEventTask.execute(GetEventTask.Params(roomId, eventId))
// Fast lane to the call event processors: try to make the incoming call ring faster
if (callEventProcessor.shouldProcessFastLane(event.getClearType())) {
callEventProcessor.processFastLane(event)
}
return event
}
}

View file

@ -79,9 +79,11 @@ import org.matrix.android.sdk.internal.session.room.tags.DefaultDeleteTagFromRoo
import org.matrix.android.sdk.internal.session.room.tags.DeleteTagFromRoomTask
import org.matrix.android.sdk.internal.session.room.timeline.DefaultFetchTokenAndPaginateTask
import org.matrix.android.sdk.internal.session.room.timeline.DefaultGetContextOfEventTask
import org.matrix.android.sdk.internal.session.room.timeline.DefaultGetEventTask
import org.matrix.android.sdk.internal.session.room.timeline.DefaultPaginationTask
import org.matrix.android.sdk.internal.session.room.timeline.FetchTokenAndPaginateTask
import org.matrix.android.sdk.internal.session.room.timeline.GetContextOfEventTask
import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask
import org.matrix.android.sdk.internal.session.room.timeline.PaginationTask
import org.matrix.android.sdk.internal.session.room.typing.DefaultSendTypingTask
import org.matrix.android.sdk.internal.session.room.typing.SendTypingTask
@ -228,4 +230,7 @@ internal abstract class RoomModule {
@Binds
abstract fun bindPeekRoomTask(task: DefaultPeekRoomTask): PeekRoomTask
@Binds
abstract fun bindGetEventTask(task: DefaultGetEventTask): GetEventTask
}

View file

@ -16,28 +16,49 @@
package org.matrix.android.sdk.internal.session.room.timeline
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.crypto.EventDecryptor
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
// TODO Add parent task
internal class GetEventTask @Inject constructor(
private val roomAPI: RoomAPI,
private val globalErrorReceiver: GlobalErrorReceiver
) : Task<GetEventTask.Params, Event> {
internal data class Params(
internal interface GetEventTask : Task<GetEventTask.Params, Event> {
data class Params(
val roomId: String,
val eventId: String
val eventId: String,
)
}
override suspend fun execute(params: Params): Event {
return executeRequest(globalErrorReceiver) {
internal class DefaultGetEventTask @Inject constructor(
private val roomAPI: RoomAPI,
private val globalErrorReceiver: GlobalErrorReceiver,
private val eventDecryptor: EventDecryptor
) : GetEventTask {
override suspend fun execute(params: GetEventTask.Params): Event {
val event = executeRequest(globalErrorReceiver) {
roomAPI.getEvent(params.roomId, params.eventId)
}
// Try to decrypt the Event
if (event.isEncrypted()) {
tryOrNull(message = "Unable to decrypt the event") {
eventDecryptor.decryptEvent(event, "")
}
?.let { result ->
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
}
}
return event
}
}

View file

@ -40,6 +40,9 @@ import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.features.notifications.SimpleNotifiableEvent
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.push.fcm.FcmHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.pushrules.Action
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.Event
@ -56,6 +59,8 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
private lateinit var activeSessionHolder: ActiveSessionHolder
private lateinit var vectorPreferences: VectorPreferences
private val coroutineScope = CoroutineScope(SupervisorJob())
// UI handler
private val mUIHandler by lazy {
Handler(Looper.getMainLooper())
@ -78,6 +83,11 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
* @param message the message
*/
override fun onMessageReceived(message: RemoteMessage) {
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.d("## onMessageReceived() %s", message.data.toString())
}
Timber.d("## onMessageReceived() from FCM with priority %s", message.priority)
// Diagnostic Push
if (message.data["event_id"] == PushersManager.TEST_EVENT_ID) {
val intent = Intent(NotificationUtils.PUSH_ACTION)
@ -90,14 +100,10 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
return
}
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.i("## onMessageReceived() %s", message.data.toString())
Timber.i("## onMessageReceived() from FCM with priority %s", message.priority)
}
mUIHandler.post {
if (ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
// we are in foreground, let the sync do the things?
Timber.v("PUSH received in a foreground state, ignore")
Timber.d("PUSH received in a foreground state, ignore")
} else {
onMessageReceivedInternal(message.data)
}
@ -140,7 +146,9 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
private fun onMessageReceivedInternal(data: Map<String, String>) {
try {
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.i("## onMessageReceivedInternal() : $data")
Timber.d("## onMessageReceivedInternal() : $data")
} else {
Timber.d("## onMessageReceivedInternal() : $data")
}
// update the badge counter
@ -156,9 +164,13 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
val roomId = data["room_id"]
if (isEventAlreadyKnown(eventId, roomId)) {
Timber.i("Ignoring push, event already known")
Timber.d("Ignoring push, event already known")
} else {
Timber.v("Requesting background sync")
// Try to get the Event content faster
Timber.d("Requesting event in fast lane")
getEventFastLane(session, roomId, eventId)
Timber.d("Requesting background sync")
session.requireBackgroundSync()
}
}
@ -167,6 +179,32 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
}
}
private fun getEventFastLane(session: Session, roomId: String?, eventId: String?) {
roomId?.takeIf { it.isNotEmpty() } ?: return
eventId?.takeIf { it.isNotEmpty() } ?: return
// If the room is currently displayed, we will not show a notification, so no need to get the Event faster
if (notificationDrawerManager.shouldIgnoreMessageEventInRoom(roomId)) {
return
}
coroutineScope.launch {
Timber.d("Fast lane: start request")
val event = session.getEvent(roomId, eventId)
val resolvedEvent = notifiableEventResolver.resolveInMemoryEvent(session, event)
// TODO Test the Event against the push rules
resolvedEvent
?.also { Timber.d("Fast lane: notify drawer") }
?.let {
it.isPushGatewayEvent = true
notificationDrawerManager.onNotifiableEventReceived(it)
notificationDrawerManager.refreshNotificationDrawer()
}
}
}
// check if the event was not yet received
// a previous catchup might have already retrieved the notified event
private fun isEventAlreadyKnown(eventId: String?, roomId: String?): Boolean {

View file

@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getEditedEventId
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
@ -42,7 +43,8 @@ import javax.inject.Inject
* 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.
*/
class NotifiableEventResolver @Inject constructor(private val stringProvider: StringProvider,
class NotifiableEventResolver @Inject constructor(
private val stringProvider: StringProvider,
private val noticeEventFormatter: NoticeEventFormatter,
private val displayableEventFormatter: DisplayableEventFormatter) {
@ -84,6 +86,28 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
}
}
fun resolveInMemoryEvent(session: Session, event: Event): NotifiableEvent? {
if (event.getClearType() != EventType.MESSAGE) return null
// TODO Ignore message edition
val user = session.getUser(event.senderId!!) ?: return null
val timelineEvent = TimelineEvent(
root = event,
localId = -1,
eventId = event.eventId!!,
displayIndex = 0,
senderInfo = SenderInfo(
userId = user.userId,
displayName = user.getBestName(),
isUniqueDisplayName = true,
avatarUrl = user.avatarUrl
)
)
return resolveMessageEvent(timelineEvent, session)
}
private fun resolveMessageEvent(event: TimelineEvent, session: Session): NotifiableEvent? {
// 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*/)

View file

@ -89,7 +89,9 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
// If we support multi session, event list should be per userId
// Currently only manage single session
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.v("%%%%%%%% onNotifiableEventReceived $notifiableEvent")
Timber.d("onNotifiableEventReceived(): $notifiableEvent")
} else {
Timber.d("onNotifiableEventReceived(): is push: ${notifiableEvent.isPushGatewayEvent}")
}
synchronized(eventList) {
val existing = eventList.firstOrNull { it.eventId == notifiableEvent.eventId }
@ -550,7 +552,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
return bitmapLoader.getRoomBitmap(roomAvatarPath)
}
private fun shouldIgnoreMessageEventInRoom(roomId: String?): Boolean {
fun shouldIgnoreMessageEventInRoom(roomId: String?): Boolean {
return currentRoomId != null && roomId == currentRoomId
}