saving sync filter changed (#7627)

This commit is contained in:
Nikita Fedrunov 2022-11-28 09:48:28 +01:00 committed by GitHub
parent 9349b1ae15
commit 5aeca1f81a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 736 additions and 159 deletions

2
changelog.d/7626.sdk Normal file
View file

@ -0,0 +1,2 @@
Sync Filter now taking in account homeserver capabilities to not pass unsupported parameters.
Sync Filter is now configured by providing SyncFilterBuilder class instance, instead of Filter to identify Filter changes related to homeserver capabilities

View file

@ -50,6 +50,7 @@ import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
import timber.log.Timber
import java.util.UUID
import java.util.concurrent.CountDownLatch
@ -346,6 +347,10 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
assertTrue(registrationResult is RegistrationResult.Success)
val session = (registrationResult as RegistrationResult.Success).session
session.open()
session.filterService().setSyncFilter(
SyncFilterBuilder()
.lazyLoadMembersForStateEvents(true)
)
if (sessionTestParams.withInitialSync) {
syncSession(session, 120_000)
}

View file

@ -16,19 +16,12 @@
package org.matrix.android.sdk.api.session.sync
import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
interface FilterService {
enum class FilterPreset {
NoFilter,
/**
* Filter for Element, will include only known event type.
*/
ElementFilter
}
/**
* Configure the filter for the sync.
*/
fun setFilter(filterPreset: FilterPreset)
suspend fun setSyncFilter(filterBuilder: SyncFilterBuilder)
}

View file

@ -0,0 +1,129 @@
/*
* Copyright 2022 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.sync.filter
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.internal.session.filter.Filter
import org.matrix.android.sdk.internal.session.filter.RoomEventFilter
import org.matrix.android.sdk.internal.session.filter.RoomFilter
import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
class SyncFilterBuilder {
private var lazyLoadMembersForStateEvents: Boolean? = null
private var lazyLoadMembersForMessageEvents: Boolean? = null
private var useThreadNotifications: Boolean? = null
private var listOfSupportedEventTypes: List<String>? = null
private var listOfSupportedStateEventTypes: List<String>? = null
fun lazyLoadMembersForStateEvents(lazyLoadMembersForStateEvents: Boolean) = apply { this.lazyLoadMembersForStateEvents = lazyLoadMembersForStateEvents }
fun lazyLoadMembersForMessageEvents(lazyLoadMembersForMessageEvents: Boolean) =
apply { this.lazyLoadMembersForMessageEvents = lazyLoadMembersForMessageEvents }
fun useThreadNotifications(useThreadNotifications: Boolean) =
apply { this.useThreadNotifications = useThreadNotifications }
fun listOfSupportedStateEventTypes(listOfSupportedStateEventTypes: List<String>) =
apply { this.listOfSupportedStateEventTypes = listOfSupportedStateEventTypes }
fun listOfSupportedTimelineEventTypes(listOfSupportedEventTypes: List<String>) =
apply { this.listOfSupportedEventTypes = listOfSupportedEventTypes }
internal fun with(currentFilterParams: SyncFilterParams?) =
apply {
currentFilterParams?.let {
useThreadNotifications = currentFilterParams.useThreadNotifications
lazyLoadMembersForMessageEvents = currentFilterParams.lazyLoadMembersForMessageEvents
lazyLoadMembersForStateEvents = currentFilterParams.lazyLoadMembersForStateEvents
listOfSupportedEventTypes = currentFilterParams.listOfSupportedEventTypes?.toList()
listOfSupportedStateEventTypes = currentFilterParams.listOfSupportedStateEventTypes?.toList()
}
}
internal fun extractParams(): SyncFilterParams {
return SyncFilterParams(
useThreadNotifications = useThreadNotifications,
lazyLoadMembersForMessageEvents = lazyLoadMembersForMessageEvents,
lazyLoadMembersForStateEvents = lazyLoadMembersForStateEvents,
listOfSupportedEventTypes = listOfSupportedEventTypes,
listOfSupportedStateEventTypes = listOfSupportedStateEventTypes,
)
}
internal fun build(homeServerCapabilities: HomeServerCapabilities): Filter {
return Filter(
room = buildRoomFilter(homeServerCapabilities)
)
}
private fun buildRoomFilter(homeServerCapabilities: HomeServerCapabilities): RoomFilter {
return RoomFilter(
timeline = buildTimelineFilter(homeServerCapabilities),
state = buildStateFilter()
)
}
private fun buildTimelineFilter(homeServerCapabilities: HomeServerCapabilities): RoomEventFilter? {
val resolvedUseThreadNotifications = if (homeServerCapabilities.canUseThreadReadReceiptsAndNotifications) {
useThreadNotifications
} else {
null
}
return RoomEventFilter(
enableUnreadThreadNotifications = resolvedUseThreadNotifications,
lazyLoadMembers = lazyLoadMembersForMessageEvents
).orNullIfEmpty()
}
private fun buildStateFilter(): RoomEventFilter? =
RoomEventFilter(
lazyLoadMembers = lazyLoadMembersForStateEvents,
types = listOfSupportedStateEventTypes
).orNullIfEmpty()
private fun RoomEventFilter.orNullIfEmpty(): RoomEventFilter? {
return if (hasData()) {
this
} else {
null
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SyncFilterBuilder
if (lazyLoadMembersForStateEvents != other.lazyLoadMembersForStateEvents) return false
if (lazyLoadMembersForMessageEvents != other.lazyLoadMembersForMessageEvents) return false
if (useThreadNotifications != other.useThreadNotifications) return false
if (listOfSupportedEventTypes != other.listOfSupportedEventTypes) return false
if (listOfSupportedStateEventTypes != other.listOfSupportedStateEventTypes) return false
return true
}
override fun hashCode(): Int {
var result = lazyLoadMembersForStateEvents?.hashCode() ?: 0
result = 31 * result + (lazyLoadMembersForMessageEvents?.hashCode() ?: 0)
result = 31 * result + (useThreadNotifications?.hashCode() ?: 0)
result = 31 * result + (listOfSupportedEventTypes?.hashCode() ?: 0)
result = 31 * result + (listOfSupportedStateEventTypes?.hashCode() ?: 0)
return result
}
}

View file

@ -61,6 +61,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo041
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo042
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo043
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo044
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo045
import org.matrix.android.sdk.internal.util.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject
@ -69,7 +70,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer
) : MatrixRealmMigration(
dbName = "Session",
schemaVersion = 44L,
schemaVersion = 45L,
) {
/**
* Forces all RealmSessionStoreMigration instances to be equal.
@ -123,5 +124,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 42) MigrateSessionTo042(realm).perform()
if (oldVersion < 43) MigrateSessionTo043(realm).perform()
if (oldVersion < 44) MigrateSessionTo044(realm).perform()
if (oldVersion < 45) MigrateSessionTo045(realm).perform()
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright 2022 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.database.mapper
import io.realm.RealmList
import org.matrix.android.sdk.internal.database.model.SyncFilterParamsEntity
import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
import javax.inject.Inject
internal class FilterParamsMapper @Inject constructor() {
fun map(entity: SyncFilterParamsEntity): SyncFilterParams {
val eventTypes = if (entity.listOfSupportedEventTypesHasBeenSet) {
entity.listOfSupportedEventTypes?.toList()
} else {
null
}
val stateEventTypes = if (entity.listOfSupportedStateEventTypesHasBeenSet) {
entity.listOfSupportedStateEventTypes?.toList()
} else {
null
}
return SyncFilterParams(
useThreadNotifications = entity.useThreadNotifications,
lazyLoadMembersForMessageEvents = entity.lazyLoadMembersForMessageEvents,
lazyLoadMembersForStateEvents = entity.lazyLoadMembersForStateEvents,
listOfSupportedEventTypes = eventTypes,
listOfSupportedStateEventTypes = stateEventTypes,
)
}
fun map(params: SyncFilterParams): SyncFilterParamsEntity {
return SyncFilterParamsEntity(
useThreadNotifications = params.useThreadNotifications,
lazyLoadMembersForMessageEvents = params.lazyLoadMembersForMessageEvents,
lazyLoadMembersForStateEvents = params.lazyLoadMembersForStateEvents,
listOfSupportedEventTypes = params.listOfSupportedEventTypes.toRealmList(),
listOfSupportedEventTypesHasBeenSet = params.listOfSupportedEventTypes != null,
listOfSupportedStateEventTypes = params.listOfSupportedStateEventTypes.toRealmList(),
listOfSupportedStateEventTypesHasBeenSet = params.listOfSupportedStateEventTypes != null,
)
}
private fun List<String>?.toRealmList(): RealmList<String>? {
return this?.toTypedArray()?.let { RealmList(*it) }
}
}

View file

@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.database.migration
import io.realm.DynamicRealm
import org.matrix.android.sdk.internal.database.model.EditionOfEventFields
import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.util.database.RealmMigrator
internal class MigrateSessionTo043(realm: DynamicRealm) : RealmMigrator(realm, 43) {

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2022 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.database.migration
import io.realm.DynamicRealm
import org.matrix.android.sdk.internal.database.model.SyncFilterParamsEntityFields
import org.matrix.android.sdk.internal.util.database.RealmMigrator
internal class MigrateSessionTo045(realm: DynamicRealm) : RealmMigrator(realm, 45) {
override fun doMigrate(realm: DynamicRealm) {
realm.schema.create("SyncFilterParamsEntity")
.addField(SyncFilterParamsEntityFields.LAZY_LOAD_MEMBERS_FOR_STATE_EVENTS, Boolean::class.java)
.setNullable(SyncFilterParamsEntityFields.LAZY_LOAD_MEMBERS_FOR_STATE_EVENTS, true)
.addField(SyncFilterParamsEntityFields.LAZY_LOAD_MEMBERS_FOR_MESSAGE_EVENTS, Boolean::class.java)
.setNullable(SyncFilterParamsEntityFields.LAZY_LOAD_MEMBERS_FOR_MESSAGE_EVENTS, true)
.addField(SyncFilterParamsEntityFields.LIST_OF_SUPPORTED_EVENT_TYPES_HAS_BEEN_SET, Boolean::class.java)
.addField(SyncFilterParamsEntityFields.LIST_OF_SUPPORTED_STATE_EVENT_TYPES_HAS_BEEN_SET, Boolean::class.java)
.addField(SyncFilterParamsEntityFields.USE_THREAD_NOTIFICATIONS, Boolean::class.java)
.setNullable(SyncFilterParamsEntityFields.USE_THREAD_NOTIFICATIONS, true)
.addRealmListField(SyncFilterParamsEntityFields.LIST_OF_SUPPORTED_EVENT_TYPES.`$`, String::class.java)
.addRealmListField(SyncFilterParamsEntityFields.LIST_OF_SUPPORTED_STATE_EVENT_TYPES.`$`, String::class.java)
}
}

View file

@ -70,7 +70,8 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit
SpaceChildSummaryEntity::class,
SpaceParentSummaryEntity::class,
UserPresenceEntity::class,
ThreadSummaryEntity::class
ThreadSummaryEntity::class,
SyncFilterParamsEntity::class,
]
)
internal class SessionRealmModule

View file

@ -0,0 +1,36 @@
/*
* Copyright 2022 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.database.model
import io.realm.RealmList
import io.realm.RealmObject
/**
* This entity stores Sync Filter configuration data, provided by the client.
*/
internal open class SyncFilterParamsEntity(
var lazyLoadMembersForStateEvents: Boolean? = null,
var lazyLoadMembersForMessageEvents: Boolean? = null,
var useThreadNotifications: Boolean? = null,
var listOfSupportedEventTypes: RealmList<String>? = null,
var listOfSupportedEventTypesHasBeenSet: Boolean = false,
var listOfSupportedStateEventTypes: RealmList<String>? = null,
var listOfSupportedStateEventTypesHasBeenSet: Boolean = false,
) : RealmObject() {
companion object
}

View file

@ -17,74 +17,71 @@
package org.matrix.android.sdk.internal.session.filter
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.kotlin.where
import org.matrix.android.sdk.internal.database.mapper.FilterParamsMapper
import org.matrix.android.sdk.internal.database.model.FilterEntity
import org.matrix.android.sdk.internal.database.model.FilterEntityFields
import org.matrix.android.sdk.internal.database.model.SyncFilterParamsEntity
import org.matrix.android.sdk.internal.database.query.get
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
import org.matrix.android.sdk.internal.util.awaitTransaction
import javax.inject.Inject
internal class DefaultFilterRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : FilterRepository {
internal class DefaultFilterRepository @Inject constructor(
@SessionDatabase private val monarchy: Monarchy,
private val filterParamsMapper: FilterParamsMapper
) : FilterRepository {
override suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean {
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
val filterEntity = FilterEntity.get(realm)
// Filter has changed, or no filter Id yet
filterEntity == null ||
filterEntity.filterBodyJson != filter.toJSONString() ||
filterEntity.filterId.isBlank()
}.also { hasChanged ->
if (hasChanged) {
// Filter is new or has changed, store it and reset the filter Id.
// This has to be done outside of the Realm.use(), because awaitTransaction change the current thread
monarchy.awaitTransaction { realm ->
// We manage only one filter for now
val filterJson = filter.toJSONString()
val roomEventFilterJson = roomEventFilter.toJSONString()
val filterEntity = FilterEntity.getOrCreate(realm)
filterEntity.filterBodyJson = filterJson
filterEntity.roomEventFilterJson = roomEventFilterJson
// Reset filterId
filterEntity.filterId = ""
}
}
}
}
override suspend fun storeFilterId(filter: Filter, filterId: String) {
monarchy.awaitTransaction {
override suspend fun storeSyncFilter(filter: Filter, filterId: String, roomEventFilter: RoomEventFilter) {
monarchy.awaitTransaction { realm ->
// We manage only one filter for now
val filterJson = filter.toJSONString()
val roomEventFilterJson = roomEventFilter.toJSONString()
// Update the filter id, only if the filter body matches
it.where<FilterEntity>()
.equalTo(FilterEntityFields.FILTER_BODY_JSON, filterJson)
?.findFirst()
?.filterId = filterId
val filterEntity = FilterEntity.getOrCreate(realm)
filterEntity.filterBodyJson = filterJson
filterEntity.roomEventFilterJson = roomEventFilterJson
filterEntity.filterId = filterId
}
}
override suspend fun getFilter(): String {
override suspend fun getStoredSyncFilterBody(): String {
return monarchy.awaitTransaction {
val filter = FilterEntity.getOrCreate(it)
if (filter.filterId.isBlank()) {
// Use the Json format
filter.filterBodyJson
FilterEntity.getOrCreate(it).filterBodyJson
}
}
override suspend fun getStoredSyncFilterId(): String? {
return monarchy.awaitTransaction {
val id = FilterEntity.get(it)?.filterId
if (id.isNullOrBlank()) {
null
} else {
// Use FilterId
filter.filterId
id
}
}
}
override suspend fun getRoomFilter(): String {
override suspend fun getRoomFilterBody(): String {
return monarchy.awaitTransaction {
FilterEntity.getOrCreate(it).roomEventFilterJson
}
}
override suspend fun getStoredFilterParams(): SyncFilterParams? {
return monarchy.awaitTransaction { realm ->
realm.where<SyncFilterParamsEntity>().findFirst()?.let {
filterParamsMapper.map(it)
}
}
}
override suspend fun storeFilterParams(params: SyncFilterParams) {
return monarchy.awaitTransaction { realm ->
val entity = filterParamsMapper.map(params)
realm.insertOrUpdate(entity)
}
}
}

View file

@ -17,19 +17,27 @@
package org.matrix.android.sdk.internal.session.filter
import org.matrix.android.sdk.api.session.sync.FilterService
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource
import javax.inject.Inject
internal class DefaultFilterService @Inject constructor(
private val saveFilterTask: SaveFilterTask,
private val taskExecutor: TaskExecutor
private val filterRepository: FilterRepository,
private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource,
) : FilterService {
// TODO Pass a list of support events instead
override fun setFilter(filterPreset: FilterService.FilterPreset) {
saveFilterTask
.configureWith(SaveFilterTask.Params(filterPreset))
.executeBy(taskExecutor)
override suspend fun setSyncFilter(filterBuilder: SyncFilterBuilder) {
filterRepository.storeFilterParams(filterBuilder.extractParams())
// don't upload/store filter until homeserver capabilities are fetched
homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.let { homeServerCapabilities ->
saveFilterTask.execute(
SaveFilterTask.Params(
filter = filterBuilder.build(homeServerCapabilities)
)
)
}
}
}

View file

@ -45,46 +45,7 @@ internal object FilterFactory {
return FilterUtil.enableLazyLoading(Filter(), true)
}
fun createElementFilter(): Filter {
return Filter(
room = RoomFilter(
timeline = createElementTimelineFilter(),
state = createElementStateFilter()
)
)
}
fun createDefaultRoomFilter(): RoomEventFilter {
return RoomEventFilter(lazyLoadMembers = true)
}
fun createElementRoomFilter(): RoomEventFilter {
return RoomEventFilter(
lazyLoadMembers = true,
// TODO Enable this for optimization
// types = (listOfSupportedEventTypes + listOfSupportedStateEventTypes).toMutableList()
)
}
private fun createElementTimelineFilter(): RoomEventFilter? {
// we need to check if homeserver supports thread notifications before setting this param
// return RoomEventFilter(enableUnreadThreadNotifications = true)
return null
}
private fun createElementStateFilter(): RoomEventFilter {
return RoomEventFilter(lazyLoadMembers = true)
}
// Get only managed types by Element
private val listOfSupportedEventTypes = listOf(
// TODO Complete the list
EventType.MESSAGE
)
// Get only managed types by Element
private val listOfSupportedStateEventTypes = listOf(
// TODO Complete the list
EventType.STATE_ROOM_MEMBER
)
}

View file

@ -44,4 +44,7 @@ internal abstract class FilterModule {
@Binds
abstract fun bindSaveFilterTask(task: DefaultSaveFilterTask): SaveFilterTask
@Binds
abstract fun bindGetCurrentFilterTask(task: DefaultGetCurrentFilterTask): GetCurrentFilterTask
}

View file

@ -16,25 +16,42 @@
package org.matrix.android.sdk.internal.session.filter
import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
/**
* Repository for request filters.
*/
internal interface FilterRepository {
/**
* Return true if the filterBody has changed, or need to be sent to the server.
* Stores sync filter and room filter.
* Note: It looks like we could use [Filter.room.timeline] instead of a separate [RoomEventFilter], but it's not clear if it's safe, so research is needed
* @return true if the filterBody has changed, or need to be sent to the server.
*/
suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean
suspend fun storeSyncFilter(filter: Filter, filterId: String, roomEventFilter: RoomEventFilter)
/**
* Set the filterId of this filter.
* Returns stored sync filter's JSON body if it exists.
*/
suspend fun storeFilterId(filter: Filter, filterId: String)
suspend fun getStoredSyncFilterBody(): String?
/**
* Return filter json or filter id.
* Returns stored sync filter's ID if it exists.
*/
suspend fun getFilter(): String
suspend fun getStoredSyncFilterId(): String?
/**
* Return the room filter.
*/
suspend fun getRoomFilter(): String
suspend fun getRoomFilterBody(): String
/**
* Returns filter params stored in local storage if it exists.
*/
suspend fun getStoredFilterParams(): SyncFilterParams?
/**
* Stores filter params to local storage.
*/
suspend fun storeFilterParams(params: SyncFilterParams)
}

View file

@ -0,0 +1,55 @@
/*
* Copyright 2022 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.filter
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface GetCurrentFilterTask : Task<Unit, String>
internal class DefaultGetCurrentFilterTask @Inject constructor(
private val filterRepository: FilterRepository,
private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource,
private val saveFilterTask: SaveFilterTask
) : GetCurrentFilterTask {
override suspend fun execute(params: Unit): String {
val storedFilterId = filterRepository.getStoredSyncFilterId()
val storedFilterBody = filterRepository.getStoredSyncFilterBody()
val homeServerCapabilities = homeServerCapabilitiesDataSource.getHomeServerCapabilities() ?: HomeServerCapabilities()
val currentFilter = SyncFilterBuilder()
.with(filterRepository.getStoredFilterParams())
.build(homeServerCapabilities)
val currentFilterBody = currentFilter.toJSONString()
return when (storedFilterBody) {
currentFilterBody -> storedFilterId ?: storedFilterBody
else -> saveFilter(currentFilter)
}
}
private suspend fun saveFilter(filter: Filter) = saveFilterTask
.execute(
SaveFilterTask.Params(
filter = filter
)
)
}

View file

@ -16,7 +16,6 @@
package org.matrix.android.sdk.internal.session.filter
import org.matrix.android.sdk.api.session.sync.FilterService
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
@ -26,10 +25,10 @@ import javax.inject.Inject
/**
* Save a filter, in db and if any changes, upload to the server.
*/
internal interface SaveFilterTask : Task<SaveFilterTask.Params, Unit> {
internal interface SaveFilterTask : Task<SaveFilterTask.Params, String> {
data class Params(
val filterPreset: FilterService.FilterPreset
val filter: Filter
)
}
@ -37,33 +36,21 @@ internal class DefaultSaveFilterTask @Inject constructor(
@UserId private val userId: String,
private val filterAPI: FilterApi,
private val filterRepository: FilterRepository,
private val globalErrorReceiver: GlobalErrorReceiver
private val globalErrorReceiver: GlobalErrorReceiver,
) : SaveFilterTask {
override suspend fun execute(params: SaveFilterTask.Params) {
val filterBody = when (params.filterPreset) {
FilterService.FilterPreset.ElementFilter -> {
FilterFactory.createElementFilter()
}
FilterService.FilterPreset.NoFilter -> {
FilterFactory.createDefaultFilter()
}
}
val roomFilter = when (params.filterPreset) {
FilterService.FilterPreset.ElementFilter -> {
FilterFactory.createElementRoomFilter()
}
FilterService.FilterPreset.NoFilter -> {
FilterFactory.createDefaultRoomFilter()
}
}
val updated = filterRepository.storeFilter(filterBody, roomFilter)
if (updated) {
val filterResponse = executeRequest(globalErrorReceiver) {
// TODO auto retry
filterAPI.uploadFilter(userId, filterBody)
}
filterRepository.storeFilterId(filterBody, filterResponse.filterId)
override suspend fun execute(params: SaveFilterTask.Params): String {
val filter = params.filter
val filterResponse = executeRequest(globalErrorReceiver) {
// TODO auto retry
filterAPI.uploadFilter(userId, filter)
}
filterRepository.storeSyncFilter(
filter = filter,
filterId = filterResponse.filterId,
roomEventFilter = FilterFactory.createDefaultRoomFilter()
)
return filterResponse.filterId
}
}

View file

@ -47,7 +47,7 @@ internal class DefaultFetchTokenAndPaginateTask @Inject constructor(
) : FetchTokenAndPaginateTask {
override suspend fun execute(params: FetchTokenAndPaginateTask.Params): TokenChunkEventPersistor.Result {
val filter = filterRepository.getRoomFilter()
val filter = filterRepository.getRoomFilterBody()
val response = executeRequest(globalErrorReceiver) {
roomAPI.getContextOfEvent(params.roomId, params.lastKnownEventId, 0, filter)
}

View file

@ -39,7 +39,7 @@ internal class DefaultGetContextOfEventTask @Inject constructor(
) : GetContextOfEventTask {
override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result {
val filter = filterRepository.getRoomFilter()
val filter = filterRepository.getRoomFilterBody()
val response = executeRequest(globalErrorReceiver) {
// We are limiting the response to the event with eventId to be sure we don't have any issue with potential merging process.
roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter)

View file

@ -41,7 +41,7 @@ internal class DefaultPaginationTask @Inject constructor(
) : PaginationTask {
override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result {
val filter = filterRepository.getRoomFilter()
val filter = filterRepository.getRoomFilterBody()
val chunk = executeRequest(
globalErrorReceiver,
canRetry = true

View file

@ -36,7 +36,7 @@ import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.network.toFailure
import org.matrix.android.sdk.internal.session.SessionListeners
import org.matrix.android.sdk.internal.session.dispatchTo
import org.matrix.android.sdk.internal.session.filter.FilterRepository
import org.matrix.android.sdk.internal.session.filter.GetCurrentFilterTask
import org.matrix.android.sdk.internal.session.homeserver.GetHomeServerCapabilitiesTask
import org.matrix.android.sdk.internal.session.sync.parsing.InitialSyncResponseParser
import org.matrix.android.sdk.internal.session.user.UserStore
@ -64,11 +64,9 @@ internal interface SyncTask : Task<SyncTask.Params, SyncResponse> {
internal class DefaultSyncTask @Inject constructor(
private val syncAPI: SyncAPI,
@UserId private val userId: String,
private val filterRepository: FilterRepository,
private val syncResponseHandler: SyncResponseHandler,
private val syncRequestStateTracker: SyncRequestStateTracker,
private val syncTokenStore: SyncTokenStore,
private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask,
private val userStore: UserStore,
private val session: Session,
private val sessionListeners: SessionListeners,
@ -79,6 +77,8 @@ internal class DefaultSyncTask @Inject constructor(
private val syncResponseParser: InitialSyncResponseParser,
private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore,
private val clock: Clock,
private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask,
private val getCurrentFilterTask: GetCurrentFilterTask
) : SyncTask {
private val workingDir = File(fileDirectory, "is")
@ -100,8 +100,13 @@ internal class DefaultSyncTask @Inject constructor(
requestParams["since"] = token
timeout = params.timeout
}
// Maybe refresh the homeserver capabilities data we know
getHomeServerCapabilitiesTask.execute(GetHomeServerCapabilitiesTask.Params(forceRefresh = false))
val filter = getCurrentFilterTask.execute(Unit)
requestParams["timeout"] = timeout.toString()
requestParams["filter"] = filterRepository.getFilter()
requestParams["filter"] = filter
params.presence?.let { requestParams["set_presence"] = it.value }
val isInitialSync = token == null
@ -115,8 +120,6 @@ internal class DefaultSyncTask @Inject constructor(
)
syncRequestStateTracker.startRoot(InitialSyncStep.ImportingAccount, 100)
}
// Maybe refresh the homeserver capabilities data we know
getHomeServerCapabilitiesTask.execute(GetHomeServerCapabilitiesTask.Params(forceRefresh = false))
val readTimeOut = (params.timeout + TIMEOUT_MARGIN).coerceAtLeast(TimeOutInterceptor.DEFAULT_LONG_TIMEOUT)

View file

@ -0,0 +1,25 @@
/*
* Copyright 2022 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.sync.filter
internal data class SyncFilterParams(
val lazyLoadMembersForStateEvents: Boolean? = null,
val lazyLoadMembersForMessageEvents: Boolean? = null,
val useThreadNotifications: Boolean? = null,
val listOfSupportedEventTypes: List<String>? = null,
val listOfSupportedStateEventTypes: List<String>? = null,
)

View file

@ -0,0 +1,100 @@
/*
* Copyright 2022 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.sync
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
import org.matrix.android.sdk.internal.session.filter.DefaultGetCurrentFilterTask
import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
import org.matrix.android.sdk.test.fakes.FakeFilterRepository
import org.matrix.android.sdk.test.fakes.FakeHomeServerCapabilitiesDataSource
import org.matrix.android.sdk.test.fakes.FakeSaveFilterTask
private const val A_FILTER_ID = "filter-id"
private val A_HOMESERVER_CAPABILITIES = HomeServerCapabilities()
private val A_SYNC_FILTER_PARAMS = SyncFilterParams(
lazyLoadMembersForMessageEvents = true,
lazyLoadMembersForStateEvents = true,
useThreadNotifications = true
)
@ExperimentalCoroutinesApi
class DefaultGetCurrentFilterTaskTest {
private val filterRepository = FakeFilterRepository()
private val homeServerCapabilitiesDataSource = FakeHomeServerCapabilitiesDataSource()
private val saveFilterTask = FakeSaveFilterTask()
private val getCurrentFilterTask = DefaultGetCurrentFilterTask(
filterRepository = filterRepository,
homeServerCapabilitiesDataSource = homeServerCapabilitiesDataSource.instance,
saveFilterTask = saveFilterTask
)
@Test
fun `given no filter is stored, when execute, then executes task to save new filter`() = runTest {
filterRepository.givenFilterParamsAreStored(A_SYNC_FILTER_PARAMS)
homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES)
filterRepository.givenFilterStored(null, null)
getCurrentFilterTask.execute(Unit)
val filter = SyncFilterBuilder()
.with(A_SYNC_FILTER_PARAMS)
.build(A_HOMESERVER_CAPABILITIES)
saveFilterTask.verifyExecution(filter)
}
@Test
fun `given filter is stored and didn't change, when execute, then returns stored filter id`() = runTest {
filterRepository.givenFilterParamsAreStored(A_SYNC_FILTER_PARAMS)
homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES)
val filter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(A_HOMESERVER_CAPABILITIES)
filterRepository.givenFilterStored(A_FILTER_ID, filter.toJSONString())
val result = getCurrentFilterTask.execute(Unit)
result shouldBeEqualTo A_FILTER_ID
}
@Test
fun `given filter is set and home server capabilities has changed, when execute, then executes task to save new filter`() = runTest {
filterRepository.givenFilterParamsAreStored(A_SYNC_FILTER_PARAMS)
homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES)
val filter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(A_HOMESERVER_CAPABILITIES)
filterRepository.givenFilterStored(A_FILTER_ID, filter.toJSONString())
val newHomeServerCapabilities = HomeServerCapabilities(canUseThreadReadReceiptsAndNotifications = true)
homeServerCapabilitiesDataSource.givenHomeServerCapabilities(newHomeServerCapabilities)
val newFilter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(newHomeServerCapabilities)
getCurrentFilterTask.execute(Unit)
saveFilterTask.verifyExecution(newFilter)
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright 2022 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.test.fakes
import io.mockk.coEvery
import io.mockk.mockk
import org.matrix.android.sdk.internal.session.filter.FilterRepository
import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
internal class FakeFilterRepository : FilterRepository by mockk() {
fun givenFilterStored(filterId: String?, filterBody: String?) {
coEvery { getStoredSyncFilterId() } returns filterId
coEvery { getStoredSyncFilterBody() } returns filterBody
}
fun givenFilterParamsAreStored(syncFilterParams: SyncFilterParams?) {
coEvery { getStoredFilterParams() } returns syncFilterParams
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright 2022 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.test.fakes
import io.mockk.every
import io.mockk.mockk
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource
internal class FakeHomeServerCapabilitiesDataSource {
val instance = mockk<HomeServerCapabilitiesDataSource>()
fun givenHomeServerCapabilities(homeServerCapabilities: HomeServerCapabilities) {
every { instance.getHomeServerCapabilities() } returns homeServerCapabilities
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright 2022 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.test.fakes
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import io.mockk.slot
import org.amshove.kluent.shouldBeEqualTo
import org.matrix.android.sdk.internal.session.filter.Filter
import org.matrix.android.sdk.internal.session.filter.SaveFilterTask
import java.util.UUID
internal class FakeSaveFilterTask : SaveFilterTask by mockk() {
init {
coEvery { execute(any()) } returns UUID.randomUUID().toString()
}
fun verifyExecution(filter: Filter) {
val slot = slot<SaveFilterTask.Params>()
coVerify { execute(capture(slot)) }
val params = slot.captured
params.filter shouldBeEqualTo filter
}
}

View file

@ -24,9 +24,9 @@ import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.sync.SyncUtils
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.sync.FilterService
import timber.log.Timber
import javax.inject.Inject
@ -41,7 +41,9 @@ class ConfigureAndStartSessionUseCase @Inject constructor(
fun execute(session: Session, startSyncing: Boolean = true) {
Timber.i("Configure and start session for ${session.myUserId}. startSyncing: $startSyncing")
session.open()
session.filterService().setFilter(FilterService.FilterPreset.ElementFilter)
session.coroutineScope.launch {
session.filterService().setSyncFilter(SyncUtils.getSyncFilterBuilder())
}
if (startSyncing) {
session.startSyncing(context)
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2022 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.features.sync
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
object SyncUtils {
// Get only managed types by Element
private val listOfSupportedTimelineEventTypes = listOf(
// TODO Complete the list
EventType.MESSAGE
)
// Get only managed types by Element
private val listOfSupportedStateEventTypes = listOf(
// TODO Complete the list
EventType.STATE_ROOM_MEMBER
)
fun getSyncFilterBuilder(): SyncFilterBuilder {
return SyncFilterBuilder()
.useThreadNotifications(true)
.lazyLoadMembersForStateEvents(true)
/**
* Currently we don't set [lazy_load_members = true] for Filter.room.timeline even though we set it for RoomFilter which is used later to
* fetch messages in a room. It's not clear if it's done so by mistake or intentionally, so changing it could case side effects and need
* careful testing
* */
// .lazyLoadMembersForMessageEvents(true)
// .listOfSupportedStateEventTypes(listOfSupportedStateEventTypes)
// .listOfSupportedTimelineEventTypes(listOfSupportedTimelineEventTypes)
}
}

View file

@ -19,6 +19,7 @@ package im.vector.app.core.session
import im.vector.app.core.extensions.startSyncing
import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase
import im.vector.app.features.session.coroutineScope
import im.vector.app.features.sync.SyncUtils
import im.vector.app.test.fakes.FakeContext
import im.vector.app.test.fakes.FakeEnableNotificationsSettingUpdater
import im.vector.app.test.fakes.FakeSession
@ -38,7 +39,6 @@ import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.session.sync.FilterService
class ConfigureAndStartSessionUseCaseTest {
@ -83,7 +83,7 @@ class ConfigureAndStartSessionUseCaseTest {
// Then
verify { fakeSession.startSyncing(fakeContext.instance) }
fakeSession.fakeFilterService.verifySetFilter(FilterService.FilterPreset.ElementFilter)
fakeSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder())
fakeSession.fakePushersService.verifyRefreshPushers()
fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded()
coVerify { fakeUpdateMatrixClientInfoUseCase.execute(fakeSession) }
@ -105,7 +105,7 @@ class ConfigureAndStartSessionUseCaseTest {
// Then
verify { fakeSession.startSyncing(fakeContext.instance) }
fakeSession.fakeFilterService.verifySetFilter(FilterService.FilterPreset.ElementFilter)
fakeSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder())
fakeSession.fakePushersService.verifyRefreshPushers()
fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded()
coVerify(inverse = true) { fakeUpdateMatrixClientInfoUseCase.execute(fakeSession) }
@ -127,7 +127,7 @@ class ConfigureAndStartSessionUseCaseTest {
// Then
verify(inverse = true) { fakeSession.startSyncing(fakeContext.instance) }
fakeSession.fakeFilterService.verifySetFilter(FilterService.FilterPreset.ElementFilter)
fakeSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder())
fakeSession.fakePushersService.verifyRefreshPushers()
fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded()
coVerify { fakeUpdateMatrixClientInfoUseCase.execute(fakeSession) }

View file

@ -16,20 +16,21 @@
package im.vector.app.test.fakes
import io.mockk.every
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import org.matrix.android.sdk.api.session.sync.FilterService
import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
class FakeFilterService : FilterService by mockk() {
fun givenSetFilterSucceeds() {
every { setFilter(any()) } just runs
coEvery { setSyncFilter(any()) } just runs
}
fun verifySetFilter(filterPreset: FilterService.FilterPreset) {
verify { setFilter(filterPreset) }
fun verifySetSyncFilter(filterBuilder: SyncFilterBuilder) {
coVerify { setSyncFilter(filterBuilder) }
}
}