From b3ba542e09a007d351d4f0674fe32111561a4c2b Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 27 Nov 2018 22:42:46 +0100 Subject: [PATCH 1/7] Start to add a flag (isUnlinked) to work with permalink. Still in progress (break state index at the moment) --- .../timeline/TimelineEventController.kt | 4 ++ .../database/helper/ChunkEntityHelper.kt | 45 ++++++++++----- .../database/helper/RoomEntityHelper.kt | 5 +- .../internal/database/model/EventEntity.kt | 3 +- .../database/query/EventEntityQueries.kt | 1 + .../android/internal/session/SessionModule.kt | 6 +- .../internal/session/room/RoomModule.kt | 8 ++- .../room/timeline/DefaultTimelineHolder.kt | 18 +++++- .../room/timeline/EventContextResponse.kt | 9 +-- .../room/timeline/GetContextOfEventRequest.kt | 22 ++------ .../room/timeline/PaginationRequest.kt | 55 ++++++++++--------- .../room/timeline/TimelineBoundaryCallback.kt | 38 ++++++++----- .../matrix/android/internal/util/Hash.kt | 17 ++++++ 13 files changed, 142 insertions(+), 89 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index 5f5810bb..c4197eef 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -17,6 +17,10 @@ class TimelineEventController(private val roomId: String, EpoxyAsyncUtil.getAsyncBackgroundHandler() ) { + init { + setFilterDuplicates(true) + } + private val pagedListCallback = object : PagedList.Callback() { override fun onChanged(position: Int, count: Int) { buildSnapshotList() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index 15ba170e..4c3abab0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -3,35 +3,51 @@ package im.vector.matrix.android.internal.database.helper import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.internal.database.mapper.asDomain -import im.vector.matrix.android.internal.database.mapper.asEntity +import im.vector.matrix.android.internal.database.mapper.fillWith import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.query.fastContains import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import io.realm.Sort +import io.realm.kotlin.createObject +internal fun ChunkEntity.isUnlinked(): Boolean { + return events.where().equalTo(EventEntityFields.IS_UNLINKED, true).findAll().isNotEmpty() +} -internal fun ChunkEntity.merge(chunkEntity: ChunkEntity, +internal fun ChunkEntity.merge(chunkToMerge: ChunkEntity, direction: PaginationDirection) { + val isChunkToMergeUnlinked = chunkToMerge.isUnlinked() + val isCurrentChunkUnlinked = this.isUnlinked() + val isUnlinked = isCurrentChunkUnlinked && isChunkToMergeUnlinked - chunkEntity.events.forEach { - addOrUpdate(it.asDomain(), direction) + if (isCurrentChunkUnlinked && !isChunkToMergeUnlinked) { + this.events.forEach { it.isUnlinked = false } } + val eventsToMerge: List if (direction == PaginationDirection.FORWARDS) { - nextToken = chunkEntity.nextToken + this.nextToken = chunkToMerge.nextToken + this.isLast = chunkToMerge.isLast + eventsToMerge = chunkToMerge.events.reversed() } else { - prevToken = chunkEntity.prevToken + this.prevToken = chunkToMerge.prevToken + eventsToMerge = chunkToMerge.events + } + eventsToMerge.forEach { + addOrUpdate(it.asDomain(), direction, isUnlinked = isUnlinked) } } internal fun ChunkEntity.addAll(events: List, direction: PaginationDirection, - stateIndexOffset: Int = 0) { + stateIndexOffset: Int = 0, + isUnlinked: Boolean = false) { events.forEach { event -> - addOrUpdate(event, direction, stateIndexOffset) + addOrUpdate(event, direction, stateIndexOffset, isUnlinked) } } @@ -41,7 +57,8 @@ internal fun ChunkEntity.updateDisplayIndexes() { internal fun ChunkEntity.addOrUpdate(event: Event, direction: PaginationDirection, - stateIndexOffset: Int = 0) { + stateIndexOffset: Int = 0, + isUnlinked: Boolean = false) { if (!isManaged) { throw IllegalStateException("Chunk entity should be managed to use fast contains") } @@ -60,15 +77,17 @@ internal fun ChunkEntity.addOrUpdate(event: Event, } } + val eventEntity: EventEntity? if (!events.fastContains(event.eventId)) { - val eventEntity = event.asEntity() - eventEntity.stateIndex = currentStateIndex + eventEntity = realm.createObject() + eventEntity.fillWith(event) val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size events.add(position, eventEntity) } else { - val eventEntity = events.find(event.eventId) - eventEntity?.stateIndex = currentStateIndex + eventEntity = events.find(event.eventId) } + eventEntity?.stateIndex = currentStateIndex + eventEntity?.isUnlinked = isUnlinked } internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt index 07018c35..7c9c223a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt @@ -19,7 +19,9 @@ internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) { } } -internal fun RoomEntity.addStateEvents(stateEvents: List, stateIndex: Int = Int.MIN_VALUE) { +internal fun RoomEntity.addStateEvents(stateEvents: List, + stateIndex: Int = Int.MIN_VALUE, + isUnlinked: Boolean = false) { if (!isManaged) { throw IllegalStateException("Chunk entity should be managed to use fast contains") } @@ -29,6 +31,7 @@ internal fun RoomEntity.addStateEvents(stateEvents: List, stateIndex: Int } val eventEntity = event.asEntity() eventEntity.stateIndex = stateIndex + eventEntity.isUnlinked = isUnlinked untimelinedStateEvents.add(eventEntity) } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt index 96ba986a..9aa040f1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt @@ -14,7 +14,8 @@ internal open class EventEntity(var eventId: String = "", var age: Long? = 0, var redacts: String? = null, var stateIndex: Int = 0, - var displayIndex: Int = 0 + var displayIndex: Int = 0, + var isUnlinked: Boolean = false ) : RealmObject() { companion object { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt index 91a828c7..7f2d69c3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt @@ -27,6 +27,7 @@ internal fun EventEntity.Companion.where(realm: Realm, roomId: String? = null, t if (type != null) { query.equalTo(EventEntityFields.TYPE, type) } + query.notEqualTo(EventEntityFields.IS_UNLINKED, true) return query } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 4b9ddb66..8c88122c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -14,6 +14,7 @@ import im.vector.matrix.android.internal.session.room.RoomAvatarResolver import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver import im.vector.matrix.android.internal.session.room.members.RoomMemberDisplayNameResolver +import im.vector.matrix.android.internal.util.md5 import io.realm.RealmConfiguration import org.koin.dsl.context.ModuleDefinition import org.koin.dsl.module.Module @@ -31,7 +32,8 @@ internal class SessionModule(private val sessionParams: SessionParams) : Module scope(DefaultSession.SCOPE) { val context = get() - val directory = File(context.filesDir, sessionParams.credentials.userId) + val childPath = sessionParams.credentials.userId.md5() + val directory = File(context.filesDir, childPath) RealmConfiguration.Builder() .directory(directory) @@ -47,7 +49,7 @@ internal class SessionModule(private val sessionParams: SessionParams) : Module } scope(DefaultSession.SCOPE) { - val retrofitBuilder = get() as Retrofit.Builder + val retrofitBuilder = get() retrofitBuilder .baseUrl(sessionParams.homeServerConnectionConfig.homeServerUri.toString()) .build() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index e2604ded..7bed7286 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -8,6 +8,7 @@ import im.vector.matrix.android.internal.session.DefaultSession import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersRequest import im.vector.matrix.android.internal.session.room.send.DefaultSendService import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineHolder +import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventRequest import im.vector.matrix.android.internal.session.room.timeline.PaginationRequest import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback import im.vector.matrix.android.internal.util.PagingRequestHelper @@ -35,6 +36,10 @@ class RoomModule : Module { PaginationRequest(get(), get(), get()) } + scope(DefaultSession.SCOPE) { + GetContextOfEventRequest(get(), get(), get()) + } + scope(DefaultSession.SCOPE) { val sessionParams = get() EventFactory(sessionParams.credentials) @@ -43,10 +48,9 @@ class RoomModule : Module { factory { (roomId: String) -> val helper = PagingRequestHelper(Executors.newSingleThreadExecutor()) val timelineBoundaryCallback = TimelineBoundaryCallback(roomId, get(), get(), helper) - DefaultTimelineHolder(roomId, get(), timelineBoundaryCallback) as TimelineHolder + DefaultTimelineHolder(roomId, get(), timelineBoundaryCallback, get()) as TimelineHolder } - factory { (roomId: String) -> DefaultSendService(roomId, get(), get()) as SendService } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt index 5f24e155..65c3edd5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt @@ -4,6 +4,7 @@ import android.arch.lifecycle.LiveData import android.arch.paging.LivePagedListBuilder import android.arch.paging.PagedList import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.events.interceptor.EnrichedEventInterceptor import im.vector.matrix.android.api.session.events.model.EnrichedEvent import im.vector.matrix.android.api.session.room.TimelineHolder @@ -20,7 +21,8 @@ private const val PAGE_SIZE = 30 internal class DefaultTimelineHolder(private val roomId: String, private val monarchy: Monarchy, - private val boundaryCallback: TimelineBoundaryCallback + private val boundaryCallback: TimelineBoundaryCallback, + private val contextOfEventRequest: GetContextOfEventRequest ) : TimelineHolder { private val eventInterceptors = ArrayList() @@ -32,7 +34,7 @@ internal class DefaultTimelineHolder(private val roomId: String, override fun timeline(eventId: String?): LiveData> { if (eventId != null) { - fetchEventIfNeeded() + fetchEventIfNeeded(eventId) } val realmDataSourceFactory = monarchy.createDataSourceFactory { buildDataSourceFactoryQuery(it, eventId) @@ -60,8 +62,18 @@ internal class DefaultTimelineHolder(private val roomId: String, return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder) } - private fun fetchEventIfNeeded() { + private fun fetchEventIfNeeded(eventId: String) { + if (!isEventPersisted(eventId)) { + contextOfEventRequest.execute(roomId, eventId, object : MatrixCallback {}) + } + } + private fun isEventPersisted(eventId: String): Boolean { + var isEventPersisted = false + monarchy.doWithRealm { + isEventPersisted = EventEntity.where(it, eventId = eventId).findFirst() != null + } + return isEventPersisted } private fun buildDataSourceFactoryQuery(realm: Realm, eventId: String?): RealmQuery { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt index 79cfac72..76ea2a8a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt @@ -12,11 +12,4 @@ data class EventContextResponse( @Json(name = "events_after") val eventsAfter: List = emptyList(), @Json(name = "end") val nextToken: String? = null, @Json(name = "state") val stateEvents: List = emptyList() -) { - - val timelineEvents: List by lazy { - eventsBefore + event + eventsAfter - } - - -} +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt index 248c616a..5ea69ba9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt @@ -4,7 +4,6 @@ import arrow.core.Try import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.util.Cancelable -import im.vector.matrix.android.internal.database.helper.addAll import im.vector.matrix.android.internal.database.helper.addOrUpdate import im.vector.matrix.android.internal.database.helper.addStateEvents import im.vector.matrix.android.internal.database.helper.deleteOnCascade @@ -46,7 +45,7 @@ internal class GetContextOfEventRequest(private val roomAPI: RoomAPI, filter: String?) = withContext(coroutineDispatchers.io) { executeRequest { - apiCall = roomAPI.getContextOfEvent(roomId, eventId, 1, filter) + apiCall = roomAPI.getContextOfEvent(roomId, eventId, 0, filter) }.flatMap { response -> insertInDb(response, roomId) } @@ -56,17 +55,14 @@ internal class GetContextOfEventRequest(private val roomAPI: RoomAPI, return monarchy .tryTransactionSync { realm -> val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: throw IllegalStateException("You shouldn't use this method without a room") + ?: throw IllegalStateException("You shouldn't use this method without a room") val currentChunk = realm.createObject().apply { prevToken = response.prevToken nextToken = response.nextToken } - currentChunk.addOrUpdate(response.event, PaginationDirection.FORWARDS) - currentChunk.addAll(response.eventsAfter, PaginationDirection.FORWARDS) - currentChunk.addAll(response.eventsBefore, PaginationDirection.BACKWARDS) - + currentChunk.addOrUpdate(response.event, PaginationDirection.FORWARDS, isUnlinked = true) // Now, handles chunk merge val prevChunk = ChunkEntity.find(realm, roomId, nextToken = response.prevToken) val nextChunk = ChunkEntity.find(realm, roomId, prevToken = response.nextToken) @@ -79,18 +75,8 @@ internal class GetContextOfEventRequest(private val roomAPI: RoomAPI, currentChunk.merge(nextChunk, PaginationDirection.FORWARDS) roomEntity.deleteOnCascade(nextChunk) } - /* - val eventIds = response.timelineEvents.mapNotNull { it.eventId } - ChunkEntity - .findAllIncludingEvents(realm, eventIds) - .filter { it != currentChunk } - .forEach { overlapped -> - currentChunk.merge(overlapped, direction) - roomEntity.deleteOnCascade(overlapped) - } - */ roomEntity.addOrUpdate(currentChunk) - roomEntity.addStateEvents(response.stateEvents) + roomEntity.addStateEvents(response.stateEvents, stateIndex = Int.MIN_VALUE, isUnlinked = true) } .map { response } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationRequest.kt index 29a3e6c9..02f4e823 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationRequest.kt @@ -5,11 +5,7 @@ import arrow.core.failure import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.util.Cancelable -import im.vector.matrix.android.internal.database.helper.addAll -import im.vector.matrix.android.internal.database.helper.addOrUpdate -import im.vector.matrix.android.internal.database.helper.addStateEvents -import im.vector.matrix.android.internal.database.helper.deleteOnCascade -import im.vector.matrix.android.internal.database.helper.merge +import im.vector.matrix.android.internal.database.helper.* import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.find @@ -34,7 +30,7 @@ internal class PaginationRequest(private val roomAPI: RoomAPI, fun execute(roomId: String, from: String?, direction: PaginationDirection, - limit: Int = 10, + limit: Int, callback: MatrixCallback ): Cancelable { val job = GlobalScope.launch(coroutineDispatchers.main) { @@ -48,7 +44,7 @@ internal class PaginationRequest(private val roomAPI: RoomAPI, private suspend fun execute(roomId: String, from: String?, direction: PaginationDirection, - limit: Int = 10, + limit: Int, filter: String?) = withContext(coroutineDispatchers.io) { if (from == null) { @@ -61,38 +57,45 @@ internal class PaginationRequest(private val roomAPI: RoomAPI, } } - private fun insertInDb(receivedChunk: TokenChunkEvent, roomId: String, direction: PaginationDirection): Try { + private fun insertInDb(receivedChunk: TokenChunkEvent, + roomId: String, + direction: PaginationDirection): Try { return monarchy .tryTransactionSync { realm -> val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: throw IllegalStateException("You shouldn't use this method without a room") + ?: throw IllegalStateException("You shouldn't use this method without a room") - val currentChunk = ChunkEntity.find(realm, roomId, prevToken = receivedChunk.nextToken) - ?: realm.createObject() - - currentChunk.prevToken = receivedChunk.prevToken - currentChunk.addAll(receivedChunk.events, direction) + val currentChunk = realm.createObject().apply { + prevToken = receivedChunk.prevToken + nextToken = receivedChunk.nextToken + } + currentChunk.addAll(receivedChunk.events, direction, isUnlinked = true) // Now, handles chunk merge - val prevChunk = ChunkEntity.find(realm, roomId, nextToken = receivedChunk.prevToken) + val nextChunk = ChunkEntity.find(realm, roomId, prevToken = receivedChunk.nextToken) + if (prevChunk != null) { - currentChunk.merge(prevChunk, direction) + currentChunk.merge(prevChunk, PaginationDirection.BACKWARDS) roomEntity.deleteOnCascade(prevChunk) - } else { - val eventIds = receivedChunk.events.mapNotNull { it.eventId } - ChunkEntity - .findAllIncludingEvents(realm, eventIds) - .filter { it != currentChunk } - .forEach { overlapped -> - currentChunk.merge(overlapped, direction) - roomEntity.deleteOnCascade(overlapped) - } } + if (nextChunk != null) { + currentChunk.merge(nextChunk, PaginationDirection.FORWARDS) + roomEntity.deleteOnCascade(nextChunk) + } + val eventIds = receivedChunk.events.mapNotNull { it.eventId } + ChunkEntity + .findAllIncludingEvents(realm, eventIds) + .filter { it != currentChunk } + .forEach { overlapped -> + currentChunk.merge(overlapped, direction) + roomEntity.deleteOnCascade(overlapped) + } roomEntity.addOrUpdate(currentChunk) // TODO : there is an issue with the pagination sending unwanted room member events - roomEntity.addStateEvents(receivedChunk.stateEvents) + val isUnlinked = currentChunk.isUnlinked() + roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = isUnlinked) } .map { receivedChunk } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineBoundaryCallback.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineBoundaryCallback.kt index ff06dbb5..bab802e2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineBoundaryCallback.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineBoundaryCallback.kt @@ -8,7 +8,6 @@ import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.query.findAllIncludingEvents import im.vector.matrix.android.internal.util.PagingRequestHelper import java.util.* -import java.util.concurrent.Executor internal class TimelineBoundaryCallback(private val roomId: String, private val paginationRequest: PaginationRequest, @@ -24,28 +23,37 @@ internal class TimelineBoundaryCallback(private val roomId: String, override fun onItemAtEndLoaded(itemAtEnd: EnrichedEvent) { helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) { - monarchy.doWithRealm { realm -> - if (itemAtEnd.root.eventId == null) { - return@doWithRealm - } - val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(itemAtEnd.root.eventId)).firstOrNull() - paginationRequest.execute(roomId, chunkEntity?.prevToken, PaginationDirection.BACKWARDS, limit, callback = createCallback(it)) - } + runPaginationRequest(it, itemAtEnd, PaginationDirection.BACKWARDS) } } override fun onItemAtFrontLoaded(itemAtFront: EnrichedEvent) { helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE) { - monarchy.doWithRealm { realm -> - if (itemAtFront.root.eventId == null) { - return@doWithRealm - } - val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(itemAtFront.root.eventId)).firstOrNull() - paginationRequest.execute(roomId, chunkEntity?.nextToken, PaginationDirection.FORWARDS, limit, callback = createCallback(it)) - } + runPaginationRequest(it, itemAtFront, PaginationDirection.FORWARDS) } } + private fun runPaginationRequest(requestCallback: PagingRequestHelper.Request.Callback, + item: EnrichedEvent, + direction: PaginationDirection) { + var token: String? = null + monarchy.doWithRealm { realm -> + if (item.root.eventId == null) { + return@doWithRealm + } + val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(item.root.eventId)).firstOrNull() + token = if (direction == PaginationDirection.FORWARDS) chunkEntity?.nextToken else chunkEntity?.prevToken + } + paginationRequest.execute( + roomId = roomId, + from = token, + direction = direction, + limit = limit, + callback = createCallback(requestCallback) + ) + } + + private fun createCallback(pagingRequestCallback: PagingRequestHelper.Request.Callback) = object : MatrixCallback { override fun onSuccess(data: TokenChunkEvent) { pagingRequestCallback.recordSuccess() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt new file mode 100644 index 00000000..fbfad9c2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt @@ -0,0 +1,17 @@ +package im.vector.matrix.android.internal.util + +import java.security.MessageDigest + +fun String.md5() = try { + val digest = MessageDigest.getInstance("md5") + digest.update(toByteArray()) + val bytes = digest.digest() + val sb = StringBuilder() + for (i in bytes.indices) { + sb.append(String.format("%02X", bytes[i])) + } + sb.toString().toLowerCase() +} catch (exc: Exception) { + // Should not happen, but just in case + hashCode().toString() +} From c396c2bec72a9682b861f68b5f39d8be5a924b16 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 28 Nov 2018 18:28:35 +0100 Subject: [PATCH 2/7] WIP on chunk merging : required to merge chunks wherever they are (permalink) --- .../database/helper/ChunkEntityHelper.kt | 45 +++++++++---------- .../database/helper/RoomEntityHelper.kt | 3 +- .../android/internal/session/SessionModule.kt | 2 +- .../room/members/RoomMemberExtractor.kt | 3 +- .../session/room/send/DefaultSendService.kt | 4 +- .../room/timeline/GetContextOfEventRequest.kt | 3 +- .../room/timeline/PaginationDirection.kt | 7 +++ .../room/timeline/PaginationRequest.kt | 40 ++++++++++++----- 8 files changed, 64 insertions(+), 43 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index 4c3abab0..351ae7ae 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -3,15 +3,18 @@ package im.vector.matrix.android.internal.database.helper import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.internal.database.mapper.asDomain -import im.vector.matrix.android.internal.database.mapper.fillWith +import im.vector.matrix.android.internal.database.mapper.asEntity import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.query.fastContains -import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import io.realm.Sort -import io.realm.kotlin.createObject + +internal fun ChunkEntity.deleteOnCascade() { + this.events.deleteAllFromRealm() + this.deleteFromRealm() +} internal fun ChunkEntity.isUnlinked(): Boolean { return events.where().equalTo(EventEntityFields.IS_UNLINKED, true).findAll().isNotEmpty() @@ -37,7 +40,7 @@ internal fun ChunkEntity.merge(chunkToMerge: ChunkEntity, eventsToMerge = chunkToMerge.events } eventsToMerge.forEach { - addOrUpdate(it.asDomain(), direction, isUnlinked = isUnlinked) + add(it.asDomain(), direction, isUnlinked = isUnlinked) } } @@ -47,7 +50,7 @@ internal fun ChunkEntity.addAll(events: List, isUnlinked: Boolean = false) { events.forEach { event -> - addOrUpdate(event, direction, stateIndexOffset, isUnlinked) + add(event, direction, stateIndexOffset, isUnlinked) } } @@ -55,15 +58,15 @@ internal fun ChunkEntity.updateDisplayIndexes() { events.forEachIndexed { index, eventEntity -> eventEntity.displayIndex = index } } -internal fun ChunkEntity.addOrUpdate(event: Event, - direction: PaginationDirection, - stateIndexOffset: Int = 0, - isUnlinked: Boolean = false) { +internal fun ChunkEntity.add(event: Event, + direction: PaginationDirection, + stateIndexOffset: Int = 0, + isUnlinked: Boolean = false) { if (!isManaged) { throw IllegalStateException("Chunk entity should be managed to use fast contains") } - if (event.eventId == null) { + if (event.eventId == null || events.fastContains(event.eventId)) { return } @@ -77,22 +80,16 @@ internal fun ChunkEntity.addOrUpdate(event: Event, } } - val eventEntity: EventEntity? - if (!events.fastContains(event.eventId)) { - eventEntity = realm.createObject() - eventEntity.fillWith(event) - val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size - events.add(position, eventEntity) - } else { - eventEntity = events.find(event.eventId) - } - eventEntity?.stateIndex = currentStateIndex - eventEntity?.isUnlinked = isUnlinked + val eventEntity = event.asEntity() + eventEntity.stateIndex = currentStateIndex + eventEntity.isUnlinked = isUnlinked + val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size + events.add(position, eventEntity) } internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING).findFirst()?.stateIndex - PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.ASCENDING).findFirst()?.stateIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING).findFirst()?.stateIndex + PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.ASCENDING).findFirst()?.stateIndex + } ?: defaultValue } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt index 7c9c223a..4df958b4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt @@ -8,8 +8,7 @@ import im.vector.matrix.android.internal.database.model.RoomEntity internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) { chunks.remove(chunkEntity) - chunkEntity.events.deleteAllFromRealm() - chunkEntity.deleteFromRealm() + chunkEntity.deleteOnCascade() } internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 8c88122c..4dbc8a8c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -38,7 +38,7 @@ internal class SessionModule(private val sessionParams: SessionParams) : Module RealmConfiguration.Builder() .directory(directory) .name("disk_store.realm") - .deleteRealmIfMigrationNeeded() + .inMemory() .build() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt index 08012cb5..67763f26 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt @@ -18,12 +18,13 @@ internal class RoomMemberExtractor(private val realm: Realm, val sender = event.sender ?: return null // When stateIndex is negative, we try to get the next stateEvent prevContent() // If prevContent is null we fallback to the Int.MIN state events content() - return if (event.stateIndex <= 0) { + val roomMember: RoomMember? = if (event.stateIndex <= 0) { baseQuery(realm, roomId, sender).next(from = event.stateIndex)?.asDomain()?.prevContent() ?: baseQuery(realm, roomId, sender).last(since = event.stateIndex)?.asDomain()?.content() } else { baseQuery(realm, roomId, sender).last(since = event.stateIndex)?.asDomain()?.content() } + return roomMember } private fun baseQuery(realm: Realm, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 57e1fc6c..a0feebc0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -7,7 +7,7 @@ import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.SendService import im.vector.matrix.android.api.session.room.send.EventFactory import im.vector.matrix.android.api.util.Cancelable -import im.vector.matrix.android.internal.database.helper.addOrUpdate +import im.vector.matrix.android.internal.database.helper.add import im.vector.matrix.android.internal.database.helper.updateDisplayIndexes import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom @@ -33,7 +33,7 @@ internal class DefaultSendService(private val roomId: String, monarchy.tryTransactionAsync { realm -> val chunkEntity = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return@tryTransactionAsync - chunkEntity.addOrUpdate(event, PaginationDirection.FORWARDS) + chunkEntity.add(event, PaginationDirection.FORWARDS) chunkEntity.updateDisplayIndexes() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt index 5ea69ba9..9ffad74d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt @@ -5,6 +5,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.database.helper.addOrUpdate +import im.vector.matrix.android.internal.database.helper.add import im.vector.matrix.android.internal.database.helper.addStateEvents import im.vector.matrix.android.internal.database.helper.deleteOnCascade import im.vector.matrix.android.internal.database.helper.merge @@ -62,7 +63,7 @@ internal class GetContextOfEventRequest(private val roomAPI: RoomAPI, nextToken = response.nextToken } - currentChunk.addOrUpdate(response.event, PaginationDirection.FORWARDS, isUnlinked = true) + currentChunk.add(response.event, PaginationDirection.FORWARDS, isUnlinked = true) // Now, handles chunk merge val prevChunk = ChunkEntity.find(realm, roomId, nextToken = response.prevToken) val nextChunk = ChunkEntity.find(realm, roomId, prevToken = response.nextToken) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationDirection.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationDirection.kt index b65a0ca0..ffb8dd0b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationDirection.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationDirection.kt @@ -13,4 +13,11 @@ internal enum class PaginationDirection(val value: String) { */ BACKWARDS("b"); + fun reversed(): PaginationDirection { + return when (this) { + FORWARDS -> BACKWARDS + BACKWARDS -> FORWARDS + } + } + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationRequest.kt index 02f4e823..795ce5c2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationRequest.kt @@ -5,7 +5,12 @@ import arrow.core.failure import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.util.Cancelable -import im.vector.matrix.android.internal.database.helper.* +import im.vector.matrix.android.internal.database.helper.addAll +import im.vector.matrix.android.internal.database.helper.addOrUpdate +import im.vector.matrix.android.internal.database.helper.addStateEvents +import im.vector.matrix.android.internal.database.helper.deleteOnCascade +import im.vector.matrix.android.internal.database.helper.isUnlinked +import im.vector.matrix.android.internal.database.helper.merge import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.find @@ -63,36 +68,47 @@ internal class PaginationRequest(private val roomAPI: RoomAPI, return monarchy .tryTransactionSync { realm -> val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: throw IllegalStateException("You shouldn't use this method without a room") + ?: throw IllegalStateException("You shouldn't use this method without a room") - val currentChunk = realm.createObject().apply { + // We create a new chunk with prev and next token as a base + // In case of permalink, we may not encounter other chunks, so it can be added + val newChunk = realm.createObject().apply { prevToken = receivedChunk.prevToken nextToken = receivedChunk.nextToken } - currentChunk.addAll(receivedChunk.events, direction, isUnlinked = true) + newChunk.addAll(receivedChunk.events, direction, isUnlinked = true) - // Now, handles chunk merge + // The current chunk is the one we will keep all along the merge process. + var currentChunk = newChunk val prevChunk = ChunkEntity.find(realm, roomId, nextToken = receivedChunk.prevToken) val nextChunk = ChunkEntity.find(realm, roomId, prevToken = receivedChunk.nextToken) + // We always merge the bottom chunk into top chunk, so we are always merging backwards if (prevChunk != null) { - currentChunk.merge(prevChunk, PaginationDirection.BACKWARDS) + newChunk.merge(prevChunk, PaginationDirection.BACKWARDS) roomEntity.deleteOnCascade(prevChunk) } if (nextChunk != null) { - currentChunk.merge(nextChunk, PaginationDirection.FORWARDS) - roomEntity.deleteOnCascade(nextChunk) + nextChunk.merge(newChunk, PaginationDirection.BACKWARDS) + newChunk.deleteOnCascade() + currentChunk = nextChunk } - val eventIds = receivedChunk.events.mapNotNull { it.eventId } + val newEventIds = receivedChunk.events.mapNotNull { it.eventId } ChunkEntity - .findAllIncludingEvents(realm, eventIds) + .findAllIncludingEvents(realm, newEventIds) .filter { it != currentChunk } .forEach { overlapped -> - currentChunk.merge(overlapped, direction) - roomEntity.deleteOnCascade(overlapped) + if (direction == PaginationDirection.BACKWARDS) { + currentChunk.merge(overlapped, PaginationDirection.BACKWARDS) + roomEntity.deleteOnCascade(overlapped) + } else { + overlapped.merge(currentChunk, PaginationDirection.BACKWARDS) + currentChunk = overlapped + } } roomEntity.addOrUpdate(currentChunk) + // TODO : there is an issue with the pagination sending unwanted room member events val isUnlinked = currentChunk.isUnlinked() roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = isUnlinked) From 9f79a5132df554d505fbad5c226dc967f1917149 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 29 Nov 2018 11:52:36 +0100 Subject: [PATCH 3/7] Pagination/Permalink : extract persistence logic in a dedicated class --- .../android/internal/session/room/RoomAPI.kt | 11 +-- .../internal/session/room/RoomModule.kt | 9 ++- .../room/timeline/EventContextResponse.kt | 13 +++- .../room/timeline/GetContextOfEventRequest.kt | 47 +---------- .../room/timeline/PaginationRequest.kt | 78 ++----------------- .../room/timeline/PaginationResponse.kt | 13 ++++ .../session/room/timeline/TokenChunkEvent.kt | 15 ++-- .../room/timeline/TokenChunkEventPersistor.kt | 73 +++++++++++++++++ 8 files changed, 116 insertions(+), 143 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationResponse.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index 28c9776f..7c17f49f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -2,18 +2,13 @@ package im.vector.matrix.android.internal.session.room import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.room.model.MessageContent import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.session.room.members.RoomMembersResponse import im.vector.matrix.android.internal.session.room.send.SendResponse import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse -import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEvent +import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse import retrofit2.Call -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.PUT -import retrofit2.http.Path -import retrofit2.http.Query +import retrofit2.http.* internal interface RoomAPI { @@ -32,7 +27,7 @@ internal interface RoomAPI { @Query("dir") dir: String, @Query("limit") limit: Int, @Query("filter") filter: String? - ): Call + ): Call /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index 7bed7286..9a7e897e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -7,10 +7,7 @@ import im.vector.matrix.android.api.session.room.send.EventFactory import im.vector.matrix.android.internal.session.DefaultSession import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersRequest import im.vector.matrix.android.internal.session.room.send.DefaultSendService -import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineHolder -import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventRequest -import im.vector.matrix.android.internal.session.room.timeline.PaginationRequest -import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback +import im.vector.matrix.android.internal.session.room.timeline.* import im.vector.matrix.android.internal.util.PagingRequestHelper import org.koin.dsl.context.ModuleDefinition import org.koin.dsl.module.Module @@ -32,6 +29,10 @@ class RoomModule : Module { LoadRoomMembersRequest(get(), get(), get()) } + scope(DefaultSession.SCOPE) { + TokenChunkEventPersistor(get()) + } + scope(DefaultSession.SCOPE) { PaginationRequest(get(), get(), get()) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt index 76ea2a8a..0579b44c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt @@ -7,9 +7,14 @@ import im.vector.matrix.android.api.session.events.model.Event @JsonClass(generateAdapter = true) data class EventContextResponse( @Json(name = "event") val event: Event, - @Json(name = "start") val prevToken: String? = null, + @Json(name = "start") override val prevToken: String? = null, @Json(name = "events_before") val eventsBefore: List = emptyList(), @Json(name = "events_after") val eventsAfter: List = emptyList(), - @Json(name = "end") val nextToken: String? = null, - @Json(name = "state") val stateEvents: List = emptyList() -) + @Json(name = "end") override val nextToken: String? = null, + @Json(name = "state") override val stateEvents: List = emptyList() +) : TokenChunkEvent { + + override val events: List + get() = listOf(event) + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt index 9ffad74d..868d6953 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt @@ -1,31 +1,18 @@ package im.vector.matrix.android.internal.session.room.timeline -import arrow.core.Try -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.util.Cancelable -import im.vector.matrix.android.internal.database.helper.addOrUpdate -import im.vector.matrix.android.internal.database.helper.add -import im.vector.matrix.android.internal.database.helper.addStateEvents -import im.vector.matrix.android.internal.database.helper.deleteOnCascade -import im.vector.matrix.android.internal.database.helper.merge -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.database.query.find -import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.legacy.util.FilterUtil import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.util.CancelableCoroutine import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers -import im.vector.matrix.android.internal.util.tryTransactionSync -import io.realm.kotlin.createObject import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext internal class GetContextOfEventRequest(private val roomAPI: RoomAPI, - private val monarchy: Monarchy, + private val tokenChunkEventPersistor: TokenChunkEventPersistor, private val coroutineDispatchers: MatrixCoroutineDispatchers ) { @@ -48,39 +35,9 @@ internal class GetContextOfEventRequest(private val roomAPI: RoomAPI, executeRequest { apiCall = roomAPI.getContextOfEvent(roomId, eventId, 0, filter) }.flatMap { response -> - insertInDb(response, roomId) + tokenChunkEventPersistor.insertInDb(response, roomId, PaginationDirection.BACKWARDS).map { response } } } - private fun insertInDb(response: EventContextResponse, roomId: String): Try { - return monarchy - .tryTransactionSync { realm -> - val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: throw IllegalStateException("You shouldn't use this method without a room") - - val currentChunk = realm.createObject().apply { - prevToken = response.prevToken - nextToken = response.nextToken - } - - currentChunk.add(response.event, PaginationDirection.FORWARDS, isUnlinked = true) - // Now, handles chunk merge - val prevChunk = ChunkEntity.find(realm, roomId, nextToken = response.prevToken) - val nextChunk = ChunkEntity.find(realm, roomId, prevToken = response.nextToken) - - if (prevChunk != null) { - currentChunk.merge(prevChunk, PaginationDirection.BACKWARDS) - roomEntity.deleteOnCascade(prevChunk) - } - if (nextChunk != null) { - currentChunk.merge(nextChunk, PaginationDirection.FORWARDS) - roomEntity.deleteOnCascade(nextChunk) - } - roomEntity.addOrUpdate(currentChunk) - roomEntity.addStateEvents(response.stateEvents, stateIndex = Int.MIN_VALUE, isUnlinked = true) - } - .map { response } - } - } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationRequest.kt index 795ce5c2..4f603b9b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationRequest.kt @@ -1,34 +1,19 @@ package im.vector.matrix.android.internal.session.room.timeline -import arrow.core.Try import arrow.core.failure -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.util.Cancelable -import im.vector.matrix.android.internal.database.helper.addAll -import im.vector.matrix.android.internal.database.helper.addOrUpdate -import im.vector.matrix.android.internal.database.helper.addStateEvents -import im.vector.matrix.android.internal.database.helper.deleteOnCascade -import im.vector.matrix.android.internal.database.helper.isUnlinked -import im.vector.matrix.android.internal.database.helper.merge -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.database.query.find -import im.vector.matrix.android.internal.database.query.findAllIncludingEvents -import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.legacy.util.FilterUtil import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.util.CancelableCoroutine import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers -import im.vector.matrix.android.internal.util.tryTransactionSync -import io.realm.kotlin.createObject import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext internal class PaginationRequest(private val roomAPI: RoomAPI, - private val monarchy: Monarchy, + private val tokenChunkEventPersistor: TokenChunkEventPersistor, private val coroutineDispatchers: MatrixCoroutineDispatchers ) { @@ -55,66 +40,13 @@ internal class PaginationRequest(private val roomAPI: RoomAPI, if (from == null) { return@withContext RuntimeException("From token shouldn't be null").failure() } - executeRequest { + executeRequest { apiCall = roomAPI.getRoomMessagesFrom(roomId, from, direction.value, limit, filter) }.flatMap { chunk -> - insertInDb(chunk, roomId, direction) + tokenChunkEventPersistor + .insertInDb(chunk, roomId, direction) + .map { chunk } } } - private fun insertInDb(receivedChunk: TokenChunkEvent, - roomId: String, - direction: PaginationDirection): Try { - return monarchy - .tryTransactionSync { realm -> - val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: throw IllegalStateException("You shouldn't use this method without a room") - - // We create a new chunk with prev and next token as a base - // In case of permalink, we may not encounter other chunks, so it can be added - val newChunk = realm.createObject().apply { - prevToken = receivedChunk.prevToken - nextToken = receivedChunk.nextToken - } - newChunk.addAll(receivedChunk.events, direction, isUnlinked = true) - - // The current chunk is the one we will keep all along the merge process. - var currentChunk = newChunk - val prevChunk = ChunkEntity.find(realm, roomId, nextToken = receivedChunk.prevToken) - val nextChunk = ChunkEntity.find(realm, roomId, prevToken = receivedChunk.nextToken) - - // We always merge the bottom chunk into top chunk, so we are always merging backwards - if (prevChunk != null) { - newChunk.merge(prevChunk, PaginationDirection.BACKWARDS) - roomEntity.deleteOnCascade(prevChunk) - } - if (nextChunk != null) { - nextChunk.merge(newChunk, PaginationDirection.BACKWARDS) - newChunk.deleteOnCascade() - currentChunk = nextChunk - } - val newEventIds = receivedChunk.events.mapNotNull { it.eventId } - ChunkEntity - .findAllIncludingEvents(realm, newEventIds) - .filter { it != currentChunk } - .forEach { overlapped -> - if (direction == PaginationDirection.BACKWARDS) { - currentChunk.merge(overlapped, PaginationDirection.BACKWARDS) - roomEntity.deleteOnCascade(overlapped) - } else { - overlapped.merge(currentChunk, PaginationDirection.BACKWARDS) - currentChunk = overlapped - } - } - - roomEntity.addOrUpdate(currentChunk) - - // TODO : there is an issue with the pagination sending unwanted room member events - val isUnlinked = currentChunk.isUnlinked() - roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = isUnlinked) - } - .map { receivedChunk } - } - - } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationResponse.kt new file mode 100644 index 00000000..edf56c28 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationResponse.kt @@ -0,0 +1,13 @@ +package im.vector.matrix.android.internal.session.room.timeline + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.Event + +@JsonClass(generateAdapter = true) +internal data class PaginationResponse( + @Json(name = "start") override val nextToken: String? = null, + @Json(name = "end") override val prevToken: String? = null, + @Json(name = "chunk") override val events: List = emptyList(), + @Json(name = "state") override val stateEvents: List = emptyList() +) : TokenChunkEvent \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt index 9e25da14..bf5fdd99 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt @@ -1,13 +1,10 @@ package im.vector.matrix.android.internal.session.room.timeline -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.events.model.Event -@JsonClass(generateAdapter = true) -internal data class TokenChunkEvent( - @Json(name = "start") val nextToken: String? = null, - @Json(name = "end") val prevToken: String? = null, - @Json(name = "chunk") val events: List = emptyList(), - @Json(name = "state") val stateEvents: List = emptyList() -) \ No newline at end of file +internal interface TokenChunkEvent { + val nextToken: String? + val prevToken: String? + val events: List + val stateEvents: List +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt new file mode 100644 index 00000000..71148157 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -0,0 +1,73 @@ +package im.vector.matrix.android.internal.session.room.timeline + +import arrow.core.Try +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.internal.database.helper.* +import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.query.find +import im.vector.matrix.android.internal.database.query.findAllIncludingEvents +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.util.tryTransactionSync +import io.realm.kotlin.createObject + + +internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { + + fun insertInDb(receivedChunk: TokenChunkEvent, + roomId: String, + direction: PaginationDirection): Try { + + return monarchy + .tryTransactionSync { realm -> + val roomEntity = RoomEntity.where(realm, roomId).findFirst() + ?: throw IllegalStateException("You shouldn't use this method without a room") + + // We create a new chunk with prev and next token as a base + // In case of permalink, we may not encounter other chunks, so it can be added + // By default, it's an unlinked chunk + val newChunk = realm.createObject().apply { + prevToken = receivedChunk.prevToken + nextToken = receivedChunk.nextToken + } + newChunk.addAll(receivedChunk.events, direction, isUnlinked = true) + + // The current chunk is the one we will keep all along the merge process. + var currentChunk = newChunk + val prevChunk = ChunkEntity.find(realm, roomId, nextToken = receivedChunk.prevToken) + val nextChunk = ChunkEntity.find(realm, roomId, prevToken = receivedChunk.nextToken) + + // We always merge the bottom chunk into top chunk, so we are always merging backwards + if (prevChunk != null) { + newChunk.merge(prevChunk, PaginationDirection.BACKWARDS) + roomEntity.deleteOnCascade(prevChunk) + } + if (nextChunk != null) { + nextChunk.merge(newChunk, PaginationDirection.BACKWARDS) + roomEntity.deleteOnCascade(newChunk) + currentChunk = nextChunk + } + val newEventIds = receivedChunk.events.mapNotNull { it.eventId } + ChunkEntity + .findAllIncludingEvents(realm, newEventIds) + .filter { it != currentChunk } + .forEach { overlapped -> + if (direction == PaginationDirection.BACKWARDS) { + currentChunk.merge(overlapped, PaginationDirection.BACKWARDS) + roomEntity.deleteOnCascade(overlapped) + } else { + overlapped.merge(currentChunk, PaginationDirection.BACKWARDS) + roomEntity.deleteOnCascade(currentChunk) + currentChunk = overlapped + } + } + roomEntity.addOrUpdate(currentChunk) + + // TODO : there is an issue with the pagination sending unwanted room member events + val isUnlinked = currentChunk.isUnlinked() + roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = isUnlinked) + } + } + + +} \ No newline at end of file From 0611661c464c31534aece563059018170adb0fcb Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 29 Nov 2018 18:35:24 +0100 Subject: [PATCH 4/7] WIP: Start to make permalink works --- .../database/helper/ChunkEntityHelper.kt | 9 +- .../internal/database/model/EventEntity.kt | 6 ++ .../database/query/ChunkEntityQueries.kt | 8 ++ .../database/query/EventEntityQueries.kt | 13 ++- .../room/members/RoomMemberExtractor.kt | 17 ++-- .../room/timeline/DefaultTimelineHolder.kt | 16 +++- .../room/timeline/EventContextResponse.kt | 4 +- .../room/timeline/PaginationResponse.kt | 4 +- .../session/room/timeline/TokenChunkEvent.kt | 4 +- .../room/timeline/TokenChunkEventPersistor.kt | 88 +++++++++++-------- 10 files changed, 111 insertions(+), 58 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index 351ae7ae..06a60d5e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -16,8 +16,9 @@ internal fun ChunkEntity.deleteOnCascade() { this.deleteFromRealm() } +// By default if a chunk is empty we consider it unlinked internal fun ChunkEntity.isUnlinked(): Boolean { - return events.where().equalTo(EventEntityFields.IS_UNLINKED, true).findAll().isNotEmpty() + return events.where().equalTo(EventEntityFields.IS_UNLINKED, false).findAll().isEmpty() } internal fun ChunkEntity.merge(chunkToMerge: ChunkEntity, @@ -89,7 +90,7 @@ internal fun ChunkEntity.add(event: Event, internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING).findFirst()?.stateIndex - PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.ASCENDING).findFirst()?.stateIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING).findFirst()?.stateIndex + PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.ASCENDING).findFirst()?.stateIndex + } ?: defaultValue } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt index 9aa040f1..2ea581bf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt @@ -18,6 +18,12 @@ internal open class EventEntity(var eventId: String = "", var isUnlinked: Boolean = false ) : RealmObject() { + enum class LinkFilterMode { + LINKED_ONLY, + UNLINKED_ONLY, + BOTH + } + companion object { const val DEFAULT_STATE_INDEX = Int.MIN_VALUE } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt index e3b071a2..2035af90 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt @@ -6,6 +6,7 @@ import im.vector.matrix.android.internal.database.model.RoomEntityFields import io.realm.Realm import io.realm.RealmQuery import io.realm.RealmResults +import io.realm.kotlin.createObject import io.realm.kotlin.where internal fun ChunkEntity.Companion.where(realm: Realm, roomId: String): RealmQuery { @@ -34,4 +35,11 @@ internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds return realm.where() .`in`(ChunkEntityFields.EVENTS.EVENT_ID, eventIds.toTypedArray()) .findAll() +} + +internal fun ChunkEntity.Companion.create(realm: Realm, prevToken: String?, nextToken: String?): ChunkEntity { + return realm.createObject().apply { + this.prevToken = prevToken + this.nextToken = nextToken + } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt index 7f2d69c3..7dfcc8ab 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt @@ -2,6 +2,7 @@ package im.vector.matrix.android.internal.database.query import im.vector.matrix.android.internal.database.model.ChunkEntityFields import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.* import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.RoomEntityFields import io.realm.Realm @@ -15,7 +16,10 @@ internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQu .equalTo(EventEntityFields.EVENT_ID, eventId) } -internal fun EventEntity.Companion.where(realm: Realm, roomId: String? = null, type: String? = null): RealmQuery { +internal fun EventEntity.Companion.where(realm: Realm, + roomId: String? = null, + type: String? = null, + linkFilterMode: EventEntity.LinkFilterMode = LINKED_ONLY): RealmQuery { val query = realm.where() if (roomId != null) { query.beginGroup() @@ -27,8 +31,11 @@ internal fun EventEntity.Companion.where(realm: Realm, roomId: String? = null, t if (type != null) { query.equalTo(EventEntityFields.TYPE, type) } - query.notEqualTo(EventEntityFields.IS_UNLINKED, true) - return query + return when (linkFilterMode) { + LINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, false) + UNLINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, true) + BOTH -> query + } } internal fun RealmQuery.next(from: Int? = null, strict: Boolean = true): EventEntity? { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt index 67763f26..4d473ff3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt @@ -16,23 +16,26 @@ internal class RoomMemberExtractor(private val realm: Realm, fun extractFrom(event: EventEntity): RoomMember? { val sender = event.sender ?: return null + // If the event is unlinked we want to fetch unlinked state events + val unlinked = event.isUnlinked // When stateIndex is negative, we try to get the next stateEvent prevContent() // If prevContent is null we fallback to the Int.MIN state events content() - val roomMember: RoomMember? = if (event.stateIndex <= 0) { - baseQuery(realm, roomId, sender).next(from = event.stateIndex)?.asDomain()?.prevContent() - ?: baseQuery(realm, roomId, sender).last(since = event.stateIndex)?.asDomain()?.content() + return if (event.stateIndex <= 0) { + baseQuery(realm, roomId, sender, unlinked).next(from = event.stateIndex)?.asDomain()?.prevContent() + ?: baseQuery(realm, roomId, sender, unlinked).last(since = event.stateIndex)?.asDomain()?.content() } else { - baseQuery(realm, roomId, sender).last(since = event.stateIndex)?.asDomain()?.content() + baseQuery(realm, roomId, sender, unlinked).last(since = event.stateIndex)?.asDomain()?.content() } - return roomMember } private fun baseQuery(realm: Realm, roomId: String, - sender: String): RealmQuery { + sender: String, + isUnlinked: Boolean): RealmQuery { + val filterMode = if (isUnlinked) EventEntity.LinkFilterMode.UNLINKED_ONLY else EventEntity.LinkFilterMode.LINKED_ONLY return EventEntity - .where(realm, roomId = roomId, type = EventType.STATE_ROOM_MEMBER) + .where(realm, roomId = roomId, type = EventType.STATE_ROOM_MEMBER, linkFilterMode = filterMode) .equalTo(EventEntityFields.STATE_KEY, sender) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt index 65c3edd5..ba7acc2a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt @@ -14,6 +14,7 @@ import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.events.interceptor.MessageEventInterceptor +import im.vector.matrix.android.internal.util.tryTransactionSync import io.realm.Realm import io.realm.RealmQuery @@ -33,6 +34,7 @@ internal class DefaultTimelineHolder(private val roomId: String, } override fun timeline(eventId: String?): LiveData> { + clearUnlinkedEvents() if (eventId != null) { fetchEventIfNeeded(eventId) } @@ -62,6 +64,16 @@ internal class DefaultTimelineHolder(private val roomId: String, return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder) } + private fun clearUnlinkedEvents() { + monarchy.tryTransactionSync { realm -> + val unlinkedEvents = EventEntity + .where(realm, roomId = roomId) + .equalTo(EventEntityFields.IS_UNLINKED, true) + .findAll() + unlinkedEvents.deleteAllFromRealm() + } + } + private fun fetchEventIfNeeded(eventId: String) { if (!isEventPersisted(eventId)) { contextOfEventRequest.execute(roomId, eventId, object : MatrixCallback {}) @@ -79,11 +91,11 @@ internal class DefaultTimelineHolder(private val roomId: String, private fun buildDataSourceFactoryQuery(realm: Realm, eventId: String?): RealmQuery { val query = if (eventId == null) { EventEntity - .where(realm, roomId = roomId) + .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY) .equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST}", true) } else { EventEntity - .where(realm, roomId = roomId) + .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH) .`in`("${EventEntityFields.CHUNK}.${ChunkEntityFields.EVENTS.EVENT_ID}", arrayOf(eventId)) } return query.sort(EventEntityFields.DISPLAY_INDEX) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt index 0579b44c..99fac915 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt @@ -7,10 +7,10 @@ import im.vector.matrix.android.api.session.events.model.Event @JsonClass(generateAdapter = true) data class EventContextResponse( @Json(name = "event") val event: Event, - @Json(name = "start") override val prevToken: String? = null, + @Json(name = "start") override val start: String? = null, @Json(name = "events_before") val eventsBefore: List = emptyList(), @Json(name = "events_after") val eventsAfter: List = emptyList(), - @Json(name = "end") override val nextToken: String? = null, + @Json(name = "end") override val end: String? = null, @Json(name = "state") override val stateEvents: List = emptyList() ) : TokenChunkEvent { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationResponse.kt index edf56c28..eb1aad80 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationResponse.kt @@ -6,8 +6,8 @@ import im.vector.matrix.android.api.session.events.model.Event @JsonClass(generateAdapter = true) internal data class PaginationResponse( - @Json(name = "start") override val nextToken: String? = null, - @Json(name = "end") override val prevToken: String? = null, + @Json(name = "start") override val start: String? = null, + @Json(name = "end") override val end: String? = null, @Json(name = "chunk") override val events: List = emptyList(), @Json(name = "state") override val stateEvents: List = emptyList() ) : TokenChunkEvent \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt index bf5fdd99..64995181 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt @@ -3,8 +3,8 @@ package im.vector.matrix.android.internal.session.room.timeline import im.vector.matrix.android.api.session.events.model.Event internal interface TokenChunkEvent { - val nextToken: String? - val prevToken: String? + val start: String? + val end: String? val events: List val stateEvents: List } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt index 71148157..4e744969 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -5,11 +5,11 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.database.helper.* import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.query.create import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findAllIncludingEvents import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.util.tryTransactionSync -import io.realm.kotlin.createObject internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { @@ -23,51 +23,67 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: throw IllegalStateException("You shouldn't use this method without a room") - // We create a new chunk with prev and next token as a base - // In case of permalink, we may not encounter other chunks, so it can be added - // By default, it's an unlinked chunk - val newChunk = realm.createObject().apply { - prevToken = receivedChunk.prevToken - nextToken = receivedChunk.nextToken + val nextToken: String? + val prevToken: String? + if (direction == PaginationDirection.FORWARDS) { + nextToken = receivedChunk.end + prevToken = receivedChunk.start + } else { + nextToken = receivedChunk.start + prevToken = receivedChunk.end } - newChunk.addAll(receivedChunk.events, direction, isUnlinked = true) + val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken) + val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken) // The current chunk is the one we will keep all along the merge process. - var currentChunk = newChunk - val prevChunk = ChunkEntity.find(realm, roomId, nextToken = receivedChunk.prevToken) - val nextChunk = ChunkEntity.find(realm, roomId, prevToken = receivedChunk.nextToken) + // We try to look for a chunk next to the token, + // otherwise we create a whole new one - // We always merge the bottom chunk into top chunk, so we are always merging backwards - if (prevChunk != null) { - newChunk.merge(prevChunk, PaginationDirection.BACKWARDS) - roomEntity.deleteOnCascade(prevChunk) + var currentChunk = if (direction == PaginationDirection.FORWARDS) { + prevChunk?.apply { this.nextToken = nextToken } + ?: ChunkEntity.create(realm, prevToken, nextToken) + } else { + nextChunk?.apply { this.prevToken = prevToken } + ?: ChunkEntity.create(realm, prevToken, nextToken) } - if (nextChunk != null) { - nextChunk.merge(newChunk, PaginationDirection.BACKWARDS) - roomEntity.deleteOnCascade(newChunk) - currentChunk = nextChunk - } - val newEventIds = receivedChunk.events.mapNotNull { it.eventId } - ChunkEntity - .findAllIncludingEvents(realm, newEventIds) - .filter { it != currentChunk } - .forEach { overlapped -> - if (direction == PaginationDirection.BACKWARDS) { - currentChunk.merge(overlapped, PaginationDirection.BACKWARDS) - roomEntity.deleteOnCascade(overlapped) - } else { - overlapped.merge(currentChunk, PaginationDirection.BACKWARDS) - roomEntity.deleteOnCascade(currentChunk) - currentChunk = overlapped - } - } - roomEntity.addOrUpdate(currentChunk) - // TODO : there is an issue with the pagination sending unwanted room member events val isUnlinked = currentChunk.isUnlinked() + currentChunk.addAll(receivedChunk.events, direction, isUnlinked = isUnlinked) + + // Then we merge chunks if needed + if (currentChunk != prevChunk && prevChunk != null) { + currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk) + } else if (currentChunk != nextChunk && nextChunk != null) { + currentChunk = handleMerge(roomEntity, direction, currentChunk, nextChunk) + } else { + val newEventIds = receivedChunk.events.mapNotNull { it.eventId } + ChunkEntity + .findAllIncludingEvents(realm, newEventIds) + .filter { it != currentChunk } + .forEach { overlapped -> + currentChunk = handleMerge(roomEntity, direction, currentChunk, overlapped) + } + } + roomEntity.addOrUpdate(currentChunk) roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = isUnlinked) } } + private fun handleMerge(roomEntity: RoomEntity, + direction: PaginationDirection, + currentChunk: ChunkEntity, + otherChunk: ChunkEntity): ChunkEntity { + + // We always merge the bottom chunk into top chunk, so we are always merging backwards + return if (direction == PaginationDirection.BACKWARDS) { + currentChunk.merge(otherChunk, PaginationDirection.BACKWARDS) + roomEntity.deleteOnCascade(otherChunk) + currentChunk + } else { + otherChunk.merge(currentChunk, PaginationDirection.BACKWARDS) + roomEntity.deleteOnCascade(currentChunk) + otherChunk + } + } } \ No newline at end of file From 683305030a5aa00d38bed1fe9b6051b8681e84a7 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 11 Dec 2018 15:35:46 +0100 Subject: [PATCH 5/7] Timeline : check isUnlinked before and after merge --- .idea/dictionaries/ganfra.xml | 1 + .../room/timeline/TokenChunkEventPersistor.kt | 18 +++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.idea/dictionaries/ganfra.xml b/.idea/dictionaries/ganfra.xml index 1ca7a97a..7e1fdcdd 100644 --- a/.idea/dictionaries/ganfra.xml +++ b/.idea/dictionaries/ganfra.xml @@ -5,6 +5,7 @@ coroutine merlins moshi + persistor synchronizer untimelined diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt index 4e744969..bf4e9b49 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -2,7 +2,12 @@ package im.vector.matrix.android.internal.session.room.timeline import arrow.core.Try import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.internal.database.helper.* +import im.vector.matrix.android.internal.database.helper.addAll +import im.vector.matrix.android.internal.database.helper.addOrUpdate +import im.vector.matrix.android.internal.database.helper.addStateEvents +import im.vector.matrix.android.internal.database.helper.deleteOnCascade +import im.vector.matrix.android.internal.database.helper.isUnlinked +import im.vector.matrix.android.internal.database.helper.merge import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.create @@ -21,7 +26,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { return monarchy .tryTransactionSync { realm -> val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: throw IllegalStateException("You shouldn't use this method without a room") + ?: throw IllegalStateException("You shouldn't use this method without a room") val nextToken: String? val prevToken: String? @@ -41,14 +46,13 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { var currentChunk = if (direction == PaginationDirection.FORWARDS) { prevChunk?.apply { this.nextToken = nextToken } - ?: ChunkEntity.create(realm, prevToken, nextToken) + ?: ChunkEntity.create(realm, prevToken, nextToken) } else { nextChunk?.apply { this.prevToken = prevToken } - ?: ChunkEntity.create(realm, prevToken, nextToken) + ?: ChunkEntity.create(realm, prevToken, nextToken) } - val isUnlinked = currentChunk.isUnlinked() - currentChunk.addAll(receivedChunk.events, direction, isUnlinked = isUnlinked) + currentChunk.addAll(receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked()) // Then we merge chunks if needed if (currentChunk != prevChunk && prevChunk != null) { @@ -65,7 +69,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { } } roomEntity.addOrUpdate(currentChunk) - roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = isUnlinked) + roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = currentChunk.isUnlinked()) } } From 0266380485c89199634a7a896c78acd35ff0ec57 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 11 Dec 2018 15:36:09 +0100 Subject: [PATCH 6/7] Start introducing tests --- matrix-sdk-android/build.gradle | 9 ++ .../android/ExampleInstrumentedTest.java | 26 ---- .../vector/matrix/android/InstrumentedTest.kt | 15 ++ .../session/room/timeline/ChunkEntityTest.kt | 147 ++++++++++++++++++ .../database/helper/ChunkEntityHelper.kt | 21 ++- .../database/query/EventEntityQueries.kt | 3 +- .../matrix/android/ExampleUnitTest.java | 17 -- 7 files changed, 186 insertions(+), 52 deletions(-) delete mode 100644 matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/ExampleInstrumentedTest.java create mode 100644 matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt delete mode 100644 matrix-sdk-android/src/test/java/im/vector/matrix/android/ExampleUnitTest.java diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 5417dc13..49a9a3b7 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -20,6 +20,7 @@ repositories { android { compileSdkVersion 28 + testOptions.unitTests.includeAndroidResources = true defaultConfig { minSdkVersion 21 @@ -45,6 +46,7 @@ dependencies { def support_version = '28.0.0' def moshi_version = '1.8.0' def lifecycle_version = "1.1.1" + def powermock_version = "2.0.0-RC.4" implementation fileTree(dir: 'libs', include: ['*.aar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" @@ -94,7 +96,14 @@ dependencies { testImplementation 'junit:junit:4.12' + testImplementation 'org.robolectric:robolectric:4.0.2' + testImplementation 'org.robolectric:shadows-support-v4:3.0' + testImplementation "io.mockk:mockk:1.8.13.kotlin13" + testImplementation 'org.amshove.kluent:kluent-android:1.44' + androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + androidTestImplementation 'org.amshove.kluent:kluent-android:1.44' + } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/ExampleInstrumentedTest.java b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/ExampleInstrumentedTest.java deleted file mode 100644 index f9b3c62a..00000000 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package im.vector.matrix.android; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("im.vector.matrix.android.test", appContext.getPackageName()); - } -} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt new file mode 100644 index 00000000..c726b7eb --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt @@ -0,0 +1,15 @@ +package im.vector.matrix.android + +import android.content.Context +import android.support.test.InstrumentationRegistry +import java.io.File + +abstract class InstrumentedTest { + fun context(): Context { + return InstrumentationRegistry.getTargetContext() + } + + fun cacheDir(): File { + return context().cacheDir + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt new file mode 100644 index 00000000..146bc752 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt @@ -0,0 +1,147 @@ +package im.vector.matrix.android.session.room.timeline + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.internal.database.helper.add +import im.vector.matrix.android.internal.database.helper.addAll +import im.vector.matrix.android.internal.database.helper.isUnlinked +import im.vector.matrix.android.internal.database.helper.lastStateIndex +import im.vector.matrix.android.internal.database.helper.merge +import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.kotlin.createObject +import org.amshove.kluent.shouldEqual +import org.junit.Before +import org.junit.Test +import kotlin.random.Random + + +internal class ChunkEntityTest : InstrumentedTest() { + + private lateinit var monarchy: Monarchy + + @Before + fun setup() { + Realm.init(context()) + val testConfig = RealmConfiguration.Builder().inMemory().name("test-realm").build() + monarchy = Monarchy.Builder().setRealmConfiguration(testConfig).build() + } + + + @Test + fun add_shouldAdd_whenNotAlreadyIncluded() { + monarchy.runTransactionSync { realm -> + val chunk: ChunkEntity = realm.createObject() + val fakeEvent = createFakeEvent(false) + chunk.add(fakeEvent, PaginationDirection.FORWARDS) + chunk.events.size shouldEqual 1 + } + } + + @Test + fun add_shouldNotAdd_whenAlreadyIncluded() { + monarchy.runTransactionSync { realm -> + val chunk: ChunkEntity = realm.createObject() + val fakeEvent = createFakeEvent(false) + chunk.add(fakeEvent, PaginationDirection.FORWARDS) + chunk.add(fakeEvent, PaginationDirection.FORWARDS) + chunk.events.size shouldEqual 1 + } + } + + @Test + fun add_shouldStateIndexIncremented_whenStateEventIsAddedForward() { + monarchy.runTransactionSync { realm -> + val chunk: ChunkEntity = realm.createObject() + val fakeEvent = createFakeEvent(true) + chunk.add(fakeEvent, PaginationDirection.FORWARDS) + chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 1 + } + } + + @Test + fun add_shouldStateIndexNotIncremented_whenNoStateEventIsAdded() { + monarchy.runTransactionSync { realm -> + val chunk: ChunkEntity = realm.createObject() + val fakeEvent = createFakeEvent(false) + chunk.add(fakeEvent, PaginationDirection.FORWARDS) + chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 0 + } + } + + @Test + fun addAll_shouldStateIndexIncremented_whenStateEventsAreAddedForward() { + monarchy.runTransactionSync { realm -> + val chunk: ChunkEntity = realm.createObject() + val fakeEvents = createFakeListOfEvents(30) + val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size + chunk.addAll(fakeEvents, PaginationDirection.FORWARDS) + chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual numberOfStateEvents + } + } + + @Test + fun addAll_shouldStateIndexDecremented_whenStateEventsAreAddedBackward() { + monarchy.runTransactionSync { realm -> + val chunk: ChunkEntity = realm.createObject() + val fakeEvents = createFakeListOfEvents(30) + val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size + val lastIsState = fakeEvents.last().isStateEvent() + val expectedStateIndex = if (lastIsState) -numberOfStateEvents + 1 else -numberOfStateEvents + chunk.addAll(fakeEvents, PaginationDirection.BACKWARDS) + chunk.lastStateIndex(PaginationDirection.BACKWARDS) shouldEqual expectedStateIndex + } + } + + @Test + fun merge_shouldAddEvents_whenMergingBackward() { + monarchy.runTransactionSync { realm -> + val chunk1: ChunkEntity = realm.createObject() + val chunk2: ChunkEntity = realm.createObject() + chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk1.merge(chunk2, PaginationDirection.BACKWARDS) + chunk1.events.size shouldEqual 60 + } + } + + @Test + fun merge_shouldEventsBeLinked_whenMergingLinkedWithUnlinked() { + monarchy.runTransactionSync { realm -> + val chunk1: ChunkEntity = realm.createObject() + val chunk2: ChunkEntity = realm.createObject() + chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = false) + chunk1.merge(chunk2, PaginationDirection.BACKWARDS) + chunk1.isUnlinked() shouldEqual false + } + } + + @Test + fun merge_shouldEventsBeUnlinked_whenMergingUnlinkedWithUnlinked() { + monarchy.runTransactionSync { realm -> + val chunk1: ChunkEntity = realm.createObject() + val chunk2: ChunkEntity = realm.createObject() + chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk1.merge(chunk2, PaginationDirection.BACKWARDS) + chunk1.isUnlinked() shouldEqual true + } + } + + + private fun createFakeListOfEvents(size: Int = 10): List { + return (0 until size).map { createFakeEvent(Random.nextBoolean()) } + } + + private fun createFakeEvent(asStateEvent: Boolean = false): Event { + val eventId = Random.nextLong(System.currentTimeMillis()).toString() + val type = if (asStateEvent) EventType.STATE_ROOM_NAME else EventType.MESSAGE + return Event(type, eventId) + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index 06a60d5e..f8d8207a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -2,7 +2,6 @@ package im.vector.matrix.android.internal.database.helper import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asEntity import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventEntity @@ -41,7 +40,7 @@ internal fun ChunkEntity.merge(chunkToMerge: ChunkEntity, eventsToMerge = chunkToMerge.events } eventsToMerge.forEach { - add(it.asDomain(), direction, isUnlinked = isUnlinked) + add(it, direction, isUnlinked = isUnlinked) } } @@ -63,16 +62,23 @@ internal fun ChunkEntity.add(event: Event, direction: PaginationDirection, stateIndexOffset: Int = 0, isUnlinked: Boolean = false) { + add(event.asEntity(), direction, stateIndexOffset, isUnlinked) +} + +internal fun ChunkEntity.add(eventEntity: EventEntity, + direction: PaginationDirection, + stateIndexOffset: Int = 0, + isUnlinked: Boolean = false) { if (!isManaged) { throw IllegalStateException("Chunk entity should be managed to use fast contains") } - if (event.eventId == null || events.fastContains(event.eventId)) { + if (eventEntity.eventId.isEmpty() || events.fastContains(eventEntity.eventId)) { return } var currentStateIndex = lastStateIndex(direction, defaultValue = stateIndexOffset) - if (direction == PaginationDirection.FORWARDS && event.isStateEvent()) { + if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(eventEntity.type)) { currentStateIndex += 1 } else if (direction == PaginationDirection.BACKWARDS && events.isNotEmpty()) { val lastEventType = events.last()?.type ?: "" @@ -81,7 +87,6 @@ internal fun ChunkEntity.add(event: Event, } } - val eventEntity = event.asEntity() eventEntity.stateIndex = currentStateIndex eventEntity.isUnlinked = isUnlinked val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size @@ -90,7 +95,7 @@ internal fun ChunkEntity.add(event: Event, internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING).findFirst()?.stateIndex - PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.ASCENDING).findFirst()?.stateIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING).findFirst()?.stateIndex + PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.ASCENDING).findFirst()?.stateIndex + } ?: defaultValue } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt index 7dfcc8ab..7b582287 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt @@ -69,6 +69,7 @@ internal fun RealmList.find(eventId: String): EventEntity? { return this.where().equalTo(EventEntityFields.EVENT_ID, eventId).findFirst() } -internal fun RealmList.fastContains(eventId: String): Boolean { +internal fun RealmList. + fastContains(eventId: String): Boolean { return this.find(eventId) != null } diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/ExampleUnitTest.java b/matrix-sdk-android/src/test/java/im/vector/matrix/android/ExampleUnitTest.java deleted file mode 100644 index 86ea905e..00000000 --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package im.vector.matrix.android; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file From 6ee272c3769e5aa63bce1fd80eca6d98a4955f5d Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 11 Dec 2018 16:39:44 +0100 Subject: [PATCH 7/7] Chunk : add some tests on merge --- .../session/room/timeline/ChunkEntityTest.kt | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt index 146bc752..b1c89935 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt @@ -14,6 +14,8 @@ import im.vector.matrix.android.internal.session.room.timeline.PaginationDirecti import io.realm.Realm import io.realm.RealmConfiguration import io.realm.kotlin.createObject +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue import org.amshove.kluent.shouldEqual import org.junit.Before import org.junit.Test @@ -117,7 +119,7 @@ internal class ChunkEntityTest : InstrumentedTest() { chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = false) chunk1.merge(chunk2, PaginationDirection.BACKWARDS) - chunk1.isUnlinked() shouldEqual false + chunk1.isUnlinked().shouldBeFalse() } } @@ -129,7 +131,35 @@ internal class ChunkEntityTest : InstrumentedTest() { chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) chunk1.merge(chunk2, PaginationDirection.BACKWARDS) - chunk1.isUnlinked() shouldEqual true + chunk1.isUnlinked().shouldBeTrue() + } + } + + @Test + fun merge_shouldPrevTokenMerged_whenMergingForwards() { + monarchy.runTransactionSync { realm -> + val chunk1: ChunkEntity = realm.createObject() + val chunk2: ChunkEntity = realm.createObject() + val prevToken = "prev_token" + chunk1.prevToken = prevToken + chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk1.merge(chunk2, PaginationDirection.FORWARDS) + chunk1.prevToken shouldEqual prevToken + } + } + + @Test + fun merge_shouldNextTokenMerged_whenMergingBackwards() { + monarchy.runTransactionSync { realm -> + val chunk1: ChunkEntity = realm.createObject() + val chunk2: ChunkEntity = realm.createObject() + val nextToken = "next_token" + chunk1.nextToken = nextToken + chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk1.merge(chunk2, PaginationDirection.BACKWARDS) + chunk1.nextToken shouldEqual nextToken } }