diff --git a/CHANGES.md b/CHANGES.md index 13385123e6..86ba91c9df 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt index 4da1662681..d9bf5cfd13 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt @@ -39,6 +39,8 @@ interface PushRuleService { fun removePushRuleListener(listener: PushRuleListener) + fun getActions(event: Event): List + // fun fulfilledBingRule(event: Event, rules: List): PushRule? interface PushRuleListener { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index 7a24ccac11..a15799d862 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -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, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/EventService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/EventService.kt new file mode 100644 index 0000000000..297f277497 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/EventService.kt @@ -0,0 +1,29 @@ +/* + * 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 +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 844e8dbbab..89b873febb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -289,3 +289,7 @@ fun Event.getRelationContent(): RelationDefaultContent? { fun Event.isReply(): Boolean { return getRelationContent()?.inReplyTo?.eventId != null } + +fun Event.isEdition(): Boolean { + return getRelationContent()?.takeIf { it.type == RelationType.REPLACE }?.eventId != null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt index a3c741ad55..5423025823 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt @@ -38,16 +38,21 @@ 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 { - val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() - ?: return@use - val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.displayIndex - ?: Int.MIN_VALUE - val eventToCheckIndex = eventToCheck.displayIndex + 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 + ?: Int.MIN_VALUE + val eventToCheckIndex = eventToCheck.displayIndex - eventToCheckIndex <= readReceiptIndex + eventToCheckIndex <= readReceiptIndex + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index 45fcc5af2d..821a9cba8c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -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, private val _sharedSecretStorageService: Lazy, private val accountService: Lazy, + private val eventService: Lazy, private val defaultIdentityService: DefaultIdentityService, private val integrationManagerService: IntegrationManagerService, private val thirdPartyService: Lazy, @@ -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(), diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index f10eb67921..e61e4ecd89 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt index 4887351709..a190ff62ac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt @@ -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}") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt index 7e54301f63..8d7e9e819a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt @@ -56,25 +56,25 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa fun onCallEvent(event: Event) { when (event.getClearType()) { - EventType.CALL_ANSWER -> { + EventType.CALL_ANSWER -> { handleCallAnswerEvent(event) } - EventType.CALL_INVITE -> { + EventType.CALL_INVITE -> { handleCallInviteEvent(event) } - EventType.CALL_HANGUP -> { + EventType.CALL_HANGUP -> { handleCallHangupEvent(event) } - EventType.CALL_REJECT -> { + EventType.CALL_REJECT -> { handleCallRejectEvent(event) } - EventType.CALL_CANDIDATES -> { + EventType.CALL_CANDIDATES -> { handleCallCandidatesEvent(event) } EventType.CALL_SELECT_ANSWER -> { handleCallSelectAnswerEvent(event) } - EventType.CALL_NEGOTIATE -> { + EventType.CALL_NEGOTIATE -> { handleCallNegotiateEvent(event) } } @@ -168,6 +168,14 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa return } val content = event.getClearContent().toModel() ?: 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, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/DefaultEventService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/DefaultEventService.kt new file mode 100644 index 0000000000..d7e9ef2ee0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/DefaultEventService.kt @@ -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 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt index e00d2ff26c..38f6b08b43 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt @@ -16,8 +16,10 @@ package org.matrix.android.sdk.internal.session.notification import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.pushrules.Action import org.matrix.android.sdk.api.pushrules.PushRuleService import org.matrix.android.sdk.api.pushrules.RuleKind +import org.matrix.android.sdk.api.pushrules.RuleScope import org.matrix.android.sdk.api.pushrules.RuleSetKey import org.matrix.android.sdk.api.pushrules.getActions import org.matrix.android.sdk.api.pushrules.rest.PushRule @@ -45,6 +47,7 @@ internal class DefaultPushRuleService @Inject constructor( private val addPushRuleTask: AddPushRuleTask, private val updatePushRuleActionsTask: UpdatePushRuleActionsTask, private val removePushRuleTask: RemovePushRuleTask, + private val pushRuleFinder: PushRuleFinder, private val taskExecutor: TaskExecutor, @SessionDatabase private val monarchy: Monarchy ) : PushRuleService { @@ -130,6 +133,12 @@ internal class DefaultPushRuleService @Inject constructor( } } + override fun getActions(event: Event): List { + val rules = getPushRules(RuleScope.GLOBAL).getAllRules() + + return pushRuleFinder.fulfilledBingRule(event, rules)?.getActions().orEmpty() + } + // fun processEvents(events: List) { // var hasDoneSomething = false // events.forEach { event -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt index 54883b51e6..0ece07fc15 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt @@ -16,9 +16,7 @@ package org.matrix.android.sdk.internal.session.notification -import org.matrix.android.sdk.api.pushrules.ConditionResolver import org.matrix.android.sdk.api.pushrules.rest.PushRule -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.di.UserId import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse @@ -35,7 +33,7 @@ internal interface ProcessEventForPushTask : Task - fulfilledBingRule(event, params.rules)?.let { + pushRuleFinder.fulfilledBingRule(event, params.rules)?.let { Timber.v("[PushRules] Rule $it match for event ${event.eventId}") defaultPushRuleService.dispatchBing(event, it) } @@ -94,13 +92,4 @@ internal class DefaultProcessEventForPushTask @Inject constructor( defaultPushRuleService.dispatchFinish() } - - private fun fulfilledBingRule(event: Event, rules: List): PushRule? { - return rules.firstOrNull { rule -> - // All conditions must hold true for an event in order to apply the action for the event. - rule.enabled && rule.conditions?.all { - it.asExecutableCondition(rule)?.isSatisfied(event, conditionResolver) ?: false - } ?: false - } - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/PushRuleFinder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/PushRuleFinder.kt new file mode 100644 index 0000000000..6e302d373d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/PushRuleFinder.kt @@ -0,0 +1,35 @@ +/* + * 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.notification + +import org.matrix.android.sdk.api.pushrules.ConditionResolver +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.api.session.events.model.Event +import javax.inject.Inject + +internal class PushRuleFinder @Inject constructor( + private val conditionResolver: ConditionResolver +) { + fun fulfilledBingRule(event: Event, rules: List): PushRule? { + return rules.firstOrNull { rule -> + // All conditions must hold true for an event in order to apply the action for the event. + rule.enabled && rule.conditions?.all { + it.asExecutableCondition(rule)?.isSatisfied(event, conditionResolver) ?: false + } ?: false + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 66b7272360..5133f72932 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -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 } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt index a9b8683a28..cbbc54e90d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt @@ -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 { - - internal data class Params( +internal interface GetEventTask : Task { + data class Params( val roomId: 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 } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt index 3ea32b3bb4..2bb606e921 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt @@ -408,6 +408,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle private fun decryptIfNeeded(event: Event, roomId: String) { try { + // Event from sync does not have roomId, so add it to the event first val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "") event.mxDecryptionResult = OlmDecryptionResult( payload = result.clearEvent, diff --git a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt index 4d2cbecfe4..4cefeadb62 100755 --- a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt @@ -31,6 +31,7 @@ import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.vectorComponent +import im.vector.app.core.network.WifiDetector import im.vector.app.core.pushers.PushersManager import im.vector.app.features.badge.BadgeProxy import im.vector.app.features.notifications.NotifiableEventResolver @@ -40,6 +41,10 @@ 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.extensions.tryOrNull 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 @@ -55,6 +60,9 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { private lateinit var pusherManager: PushersManager private lateinit var activeSessionHolder: ActiveSessionHolder private lateinit var vectorPreferences: VectorPreferences + private lateinit var wifiDetector: WifiDetector + + private val coroutineScope = CoroutineScope(SupervisorJob()) // UI handler private val mUIHandler by lazy { @@ -69,6 +77,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { pusherManager = pusherManager() activeSessionHolder = activeSessionHolder() vectorPreferences = vectorPreferences() + wifiDetector = wifiDetector() } } @@ -78,6 +87,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 +104,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 +150,9 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { private fun onMessageReceivedInternal(data: Map) { 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 +168,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 +183,36 @@ 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 + } + + if (wifiDetector.isConnectedToWifi().not()) { + Timber.d("No WiFi network, do not get Event") + return + } + + coroutineScope.launch { + Timber.d("Fast lane: start request") + val event = tryOrNull { session.getEvent(roomId, eventId) } ?: return@launch + + val resolvedEvent = notifiableEventResolver.resolveInMemoryEvent(session, event) + + 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 { diff --git a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt index cae7a2ece6..4b88ff6767 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt @@ -26,6 +26,7 @@ import im.vector.app.EmojiCompatWrapper import im.vector.app.VectorApplication import im.vector.app.core.dialogs.UnrecognizedCertificateDialog import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.network.WifiDetector import im.vector.app.core.pushers.PushersManager import im.vector.app.core.utils.AssetReader import im.vector.app.core.utils.DimensionConverter @@ -140,6 +141,8 @@ interface VectorComponent { fun vectorPreferences(): VectorPreferences + fun wifiDetector(): WifiDetector + fun vectorFileLogger(): VectorFileLogger fun uiStateRepository(): UiStateRepository diff --git a/vector/src/main/java/im/vector/app/core/extensions/LiveData.kt b/vector/src/main/java/im/vector/app/core/extensions/LiveData.kt index 588063e2a4..5a6599acff 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/LiveData.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/LiveData.kt @@ -39,7 +39,7 @@ inline fun LiveData>.observeEventFirstThrottle(owner: Lifecycle val firstThrottler = FirstThrottler(minimumInterval) this.observe(owner, EventObserver { - if (firstThrottler.canHandle()) { + if (firstThrottler.canHandle() is FirstThrottler.CanHandlerResult.Yes) { it.run(observer) } }) diff --git a/vector/src/main/java/im/vector/app/core/network/WifiDetector.kt b/vector/src/main/java/im/vector/app/core/network/WifiDetector.kt new file mode 100644 index 0000000000..34b2a7590d --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/network/WifiDetector.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2021 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 im.vector.app.core.network + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.Build +import androidx.core.content.getSystemService +import org.matrix.android.sdk.api.extensions.orFalse +import timber.log.Timber +import javax.inject.Inject + +class WifiDetector @Inject constructor( + context: Context +) { + private val connectivityManager = context.getSystemService()!! + + fun isConnectedToWifi(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + connectivityManager.activeNetwork + ?.let { connectivityManager.getNetworkCapabilities(it) } + ?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + .orFalse() + } else { + @Suppress("DEPRECATION") + connectivityManager.activeNetworkInfo?.type == ConnectivityManager.TYPE_WIFI + } + .also { Timber.d("isConnected to WiFi: $it") } + } +} diff --git a/vector/src/main/java/im/vector/app/core/utils/FirstThrottler.kt b/vector/src/main/java/im/vector/app/core/utils/FirstThrottler.kt index 3d52ca99db..004f500c4e 100644 --- a/vector/src/main/java/im/vector/app/core/utils/FirstThrottler.kt +++ b/vector/src/main/java/im/vector/app/core/utils/FirstThrottler.kt @@ -24,14 +24,27 @@ import android.os.SystemClock class FirstThrottler(private val minimumInterval: Long = 800) { private var lastDate = 0L - fun canHandle(): Boolean { + sealed class CanHandlerResult { + object Yes : CanHandlerResult() + data class No(val shouldWaitMillis: Long) : CanHandlerResult() + + fun waitMillis(): Long { + return when (this) { + Yes -> 0 + is No -> shouldWaitMillis + } + } + } + + fun canHandle(): CanHandlerResult { val now = SystemClock.elapsedRealtime() - if (now > lastDate + minimumInterval) { + val delaySinceLast = now - lastDate + if (delaySinceLast > minimumInterval) { lastDate = now - return true + return CanHandlerResult.Yes } // Too soon - return false + return CanHandlerResult.No(minimumInterval - delaySinceLast) } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index a4f617bf5b..494c30aab9 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -26,9 +26,11 @@ import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.crypto.MXCryptoError 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.api.session.events.model.isEdition 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,9 +44,10 @@ 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, - private val noticeEventFormatter: NoticeEventFormatter, - private val displayableEventFormatter: DisplayableEventFormatter) { +class NotifiableEventResolver @Inject constructor( + private val stringProvider: StringProvider, + private val noticeEventFormatter: NoticeEventFormatter, + private val displayableEventFormatter: DisplayableEventFormatter) { // private val eventDisplay = RiotEventDisplay(context) @@ -84,6 +87,47 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St } } + fun resolveInMemoryEvent(session: Session, event: Event): NotifiableEvent? { + if (event.getClearType() != EventType.MESSAGE) return null + + // Ignore message edition + if (event.isEdition()) return null + + val actions = session.getActions(event) + val notificationAction = actions.toNotificationAction() + + return if (notificationAction.shouldNotify) { + 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 + ) + ) + + val notifiableEvent = resolveMessageEvent(timelineEvent, session) + + if (notifiableEvent == null) { + Timber.d("## Failed to resolve event") + // TODO + null + } else { + notifiableEvent.noisy = !notificationAction.soundName.isNullOrBlank() + notifiableEvent + } + } else { + Timber.d("Matched push rule is set to not notify") + null + } + } + 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*/) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index 7f3c0a5beb..7ac9b28b9a 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -26,6 +26,7 @@ import im.vector.app.ActiveSessionDataSource import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.FirstThrottler import im.vector.app.features.settings.VectorPreferences import me.gujun.android.span.span import org.matrix.android.sdk.api.session.Session @@ -88,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 } @@ -194,10 +197,14 @@ class NotificationDrawerManager @Inject constructor(private val context: Context notificationUtils.cancelNotificationMessage(roomId, ROOM_INVITATION_NOTIFICATION_ID) } + private var firstThrottler = FirstThrottler(200) + fun refreshNotificationDrawer() { // Implement last throttler - Timber.v("refreshNotificationDrawer()") + val canHandle = firstThrottler.canHandle() + Timber.v("refreshNotificationDrawer(), delay: ${canHandle.waitMillis()} ms") backgroundHandler.removeCallbacksAndMessages(null) + backgroundHandler.postDelayed( { try { @@ -206,7 +213,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context // It can happen if for instance session has been destroyed. It's a bit ugly to try catch like this, but it's safer Timber.w(throwable, "refreshNotificationDrawerBg failure") } - }, 200) + }, + canHandle.waitMillis()) } @WorkerThread @@ -544,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 } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt index 17c5cad1c2..03b7c16274 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt @@ -101,7 +101,7 @@ class VectorSettingsHelpAboutFragment @Inject constructor( // third party notice findPreference(VectorPreferences.SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY)!! .onPreferenceClickListener = Preference.OnPreferenceClickListener { - if (firstThrottler.canHandle()) { + if (firstThrottler.canHandle() is FirstThrottler.CanHandlerResult.Yes) { activity?.displayInWebView(VectorSettingsUrls.THIRD_PARTY_LICENSES) } false