Merge pull request #3055 from vector-im/feature/bma/get_event

Get event
This commit is contained in:
Benoit Marty 2021-04-07 18:38:17 +02:00 committed by GitHub
commit e61a9e75e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 390 additions and 61 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

@ -39,6 +39,8 @@ interface PushRuleService {
fun removePushRuleListener(listener: PushRuleListener)
fun getActions(event: Event): List<Action>
// fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule?
interface PushRuleListener {

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,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
}

View file

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

View file

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

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

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

@ -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<Action> {
val rules = getPushRules(RuleScope.GLOBAL).getAllRules()
return pushRuleFinder.fulfilledBingRule(event, rules)?.getActions().orEmpty()
}
// fun processEvents(events: List<Event>) {
// var hasDoneSomething = false
// events.forEach { event ->

View file

@ -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<ProcessEventForPushTask.Params
internal class DefaultProcessEventForPushTask @Inject constructor(
private val defaultPushRuleService: DefaultPushRuleService,
private val conditionResolver: ConditionResolver,
private val pushRuleFinder: PushRuleFinder,
@UserId private val userId: String
) : ProcessEventForPushTask {
@ -72,7 +70,7 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
Timber.v("[PushRules] Found ${allEvents.size} out of ${(newJoinEvents + inviteEvents).size}" +
" to check for push rules with ${params.rules.size} rules")
allEvents.forEach { event ->
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>): 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
}
}
}

View file

@ -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>): 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
}
}
}

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
)
}
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

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

View file

@ -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<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 +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 {

View file

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

View file

@ -39,7 +39,7 @@ inline fun <T> LiveData<LiveEvent<T>>.observeEventFirstThrottle(owner: Lifecycle
val firstThrottler = FirstThrottler(minimumInterval)
this.observe(owner, EventObserver {
if (firstThrottler.canHandle()) {
if (firstThrottler.canHandle() is FirstThrottler.CanHandlerResult.Yes) {
it.run(observer)
}
})

View file

@ -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<ConnectivityManager>()!!
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") }
}
}

View file

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

View file

@ -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*/)

View file

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

View file

@ -101,7 +101,7 @@ class VectorSettingsHelpAboutFragment @Inject constructor(
// third party notice
findPreference<VectorPreference>(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