From 820709d43347edfac81645c345fc46944399aa13 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 13 Mar 2019 22:30:05 +0100 Subject: [PATCH 01/12] Timeline rework : initial commit - to amend. --- .../api/session/room/timeline/Timeline.kt | 55 +++++ .../session/room/timeline/TimelineService.kt | 2 + .../database/RealmLiveEntityObserver.kt | 15 +- .../database/helper/ChunkEntityHelper.kt | 35 ++-- .../database/helper/RoomEntityHelper.kt | 8 +- .../internal/database/mapper/EventMapper.kt | 5 - .../database/query/EventEntityQueries.kt | 2 +- .../session/group/GroupSummaryUpdater.kt | 3 +- .../session/room/RoomSummaryUpdater.kt | 4 +- .../session/room/prune/EventsPruner.kt | 2 +- .../session/room/read/SetReadMarkersTask.kt | 17 +- .../session/room/send/DefaultSendService.kt | 2 - .../session/room/timeline/DefaultTimeline.kt | 189 ++++++++++++++++++ .../room/timeline/DefaultTimelineService.kt | 12 +- .../room/timeline/TokenChunkEventPersistor.kt | 2 +- .../session/user/UserEntityUpdater.kt | 2 +- 16 files changed, 307 insertions(+), 48 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt new file mode 100644 index 00000000..82601b50 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -0,0 +1,55 @@ +/* + * + * * Copyright 2019 New Vector Ltd + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package im.vector.matrix.android.api.session.room.timeline + +interface Timeline { + + fun paginate(direction: Direction, count: Int) + fun addListener(listener: Listener) + fun removeListener(listener: Listener) + fun removeAllListeners() + + interface Listener { + + } + + enum class Direction(val value: String) { + /** + * Forwards when the event is added to the end of the timeline. + * These events come from the /sync stream or from forwards pagination. + */ + FORWARDS("f"), + + /** + * Backwards when the event is added to the start of the timeline. + * These events come from a back pagination. + */ + BACKWARDS("b"); + + fun reversed(): Direction { + 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/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt index 0c5102bd..9e7edc61 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt @@ -32,4 +32,6 @@ interface TimelineService { */ fun timeline(eventId: String? = null): LiveData + fun createTimeline(eventId: String?): Timeline + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt index 5c66c4ed..27ff501f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt @@ -42,6 +42,7 @@ internal abstract class RealmLiveEntityObserver(protected val m queryResults.addChangeListener { t, changeSet -> onChanged(t, changeSet) } + processInitialResults(queryResults) results = AtomicReference(queryResults) } } @@ -55,18 +56,22 @@ internal abstract class RealmLiveEntityObserver(protected val m } } - // PRIVATE - - private fun onChanged(realmResults: RealmResults, changeSet: OrderedCollectionChangeSet) { + protected open fun onChanged(realmResults: RealmResults, changeSet: OrderedCollectionChangeSet) { val insertionIndexes = changeSet.insertions val updateIndexes = changeSet.changes val deletionIndexes = changeSet.deletions val inserted = realmResults.filterIndexed { index, _ -> insertionIndexes.contains(index) } val updated = realmResults.filterIndexed { index, _ -> updateIndexes.contains(index) } val deleted = realmResults.filterIndexed { index, _ -> deletionIndexes.contains(index) } - process(inserted, updated, deleted) + processChanges(inserted, updated, deleted) } - abstract fun process(inserted: List, updated: List, deleted: List) + protected open fun processInitialResults(results: RealmResults) { + // no-op + } + + protected open fun processChanges(inserted: List, updated: List, deleted: List) { + //no-op + } } \ 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 03dc9439..b1883d3f 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 @@ -20,7 +20,6 @@ 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.toEntity -import im.vector.matrix.android.internal.database.mapper.updateWith 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 @@ -76,10 +75,6 @@ internal fun ChunkEntity.addAll(roomId: String, } } -internal fun ChunkEntity.updateDisplayIndexes() { - events.forEachIndexed { index, eventEntity -> eventEntity.displayIndex = index } -} - internal fun ChunkEntity.add(roomId: String, event: Event, direction: PaginationDirection, @@ -90,6 +85,12 @@ internal fun ChunkEntity.add(roomId: String, if (event.eventId.isNullOrEmpty() || events.fastContains(event.eventId)) { return } + var currentDisplayIndex = lastDisplayIndex(direction, 0) + if (direction == PaginationDirection.FORWARDS) { + currentDisplayIndex += 1 + } else { + currentDisplayIndex -= 1 + } var currentStateIndex = lastStateIndex(direction, defaultValue = stateIndexOffset) if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(event.type)) { currentStateIndex += 1 @@ -99,10 +100,13 @@ internal fun ChunkEntity.add(roomId: String, currentStateIndex -= 1 } } - val eventEntity = event.toEntity(roomId) - eventEntity.updateWith(currentStateIndex, isUnlinked) - val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size - events.add(position, eventEntity) + val eventEntity = event.toEntity(roomId).apply { + this.stateIndex = currentStateIndex + this.isUnlinked = isUnlinked + this.displayIndex = currentDisplayIndex + } + // We are not using the order of the list, but will be sorting with displayIndex field + events.add(eventEntity) } private fun ChunkEntity.assertIsManaged() { @@ -111,9 +115,16 @@ private fun ChunkEntity.assertIsManaged() { } } +internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { + return when (direction) { + PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findFirst()?.displayIndex + PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING).findFirst()?.displayIndex + } ?: defaultValue +} + 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 cec664c2..5321df47 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 @@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.database.helper import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.internal.database.mapper.toEntity -import im.vector.matrix.android.internal.database.mapper.updateWith 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.fastContains @@ -30,7 +29,6 @@ internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) { } internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) { - chunkEntity.updateDisplayIndexes() if (!chunks.contains(chunkEntity)) { chunks.add(chunkEntity) } @@ -47,8 +45,10 @@ internal fun RoomEntity.addStateEvents(stateEvents: List, if (event.eventId == null || (filterDuplicates && fastContains(event.eventId))) { return@forEach } - val eventEntity = event.toEntity(roomId) - eventEntity.updateWith(stateIndex, isUnlinked) + val eventEntity = event.toEntity(roomId).apply { + this.stateIndex = stateIndex + this.isUnlinked = isUnlinked + } untimelinedStateEvents.add(eventEntity) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt index 7bd091b0..3d83881a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt @@ -57,11 +57,6 @@ internal object EventMapper { } -internal fun EventEntity.updateWith(stateIndex: Int, isUnlinked: Boolean) { - this.stateIndex = stateIndex - this.isUnlinked = isUnlinked -} - internal fun EventEntity.asDomain(): Event { return EventMapper.map(this) } 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 4aa90884..a86ec2c0 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 @@ -59,7 +59,7 @@ internal fun EventEntity.Companion.latestEvent(realm: Realm, query?.not()?.`in`(EventEntityFields.TYPE, excludedTypes.toTypedArray()) } return query - ?.sort(EventEntityFields.DISPLAY_INDEX) + ?.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) ?.findFirst() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt index e0eef92c..d756b73c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt @@ -23,6 +23,7 @@ import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.database.RealmLiveEntityObserver +import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.GroupEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.util.WorkerParamsFactory @@ -38,7 +39,7 @@ internal class GroupSummaryUpdater(monarchy: Monarchy .setRequiredNetworkType(NetworkType.CONNECTED) .build() - override fun process(inserted: List, updated: List, deleted: List) { + override fun processChanges(inserted: List, updated: List, deleted: List) { val newGroupIds = inserted.map { it.groupId } val getGroupDataWorkerParams = GetGroupDataWorker.Params(newGroupIds) val workData = WorkerParamsFactory.toData(getGroupDataWorkerParams) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index 4d690832..590d6ce8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -44,7 +44,7 @@ internal class RoomSummaryUpdater(monarchy: Monarchy, override val query = Monarchy.Query { RoomEntity.where(it) } - override fun process(inserted: List, updated: List, deleted: List) { + override fun processChanges(inserted: List, updated: List, deleted: List) { val rooms = (inserted + updated).map { it.roomId } monarchy.writeAsync { realm -> rooms.forEach { updateRoom(realm, it) } @@ -56,7 +56,7 @@ internal class RoomSummaryUpdater(monarchy: Monarchy, return } val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) val lastEvent = EventEntity.latestEvent(realm, roomId, includedTypes = listOf(EventType.MESSAGE)) val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).last()?.asDomain() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt index c01cd273..e4bf7770 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt @@ -34,7 +34,7 @@ internal class EventsPruner(monarchy: Monarchy) : override val query = Monarchy.Query { EventEntity.where(it, type = EventType.REDACTION) } - override fun process(inserted: List, updated: List, deleted: List) { + override fun processChanges(inserted: List, updated: List, deleted: List) { val redactionEvents = inserted.map { it.asDomain() } val pruneEventWorkerParams = PruneEventWorker.Params(redactionEvents) val workData = WorkerParamsFactory.toData(pruneEventWorkerParams) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index ba14d83c..fa57577c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -56,8 +56,8 @@ internal class DefaultSetReadMarkersTask(private val roomAPI: RoomAPI, markers[READ_MARKER] = params.fullyReadEventId } if (params.readReceiptEventId != null - && MatrixPatterns.isEventId(params.readReceiptEventId) - && !isEventRead(params.roomId, params.readReceiptEventId)) { + && MatrixPatterns.isEventId(params.readReceiptEventId) + && !isEventRead(params.roomId, params.readReceiptEventId)) { updateNotificationCountIfNecessary(params.roomId, params.readReceiptEventId) markers[READ_RECEIPT] = params.readReceiptEventId @@ -76,7 +76,7 @@ internal class DefaultSetReadMarkersTask(private val roomAPI: RoomAPI, val isLatestReceived = EventEntity.latestEvent(realm, roomId)?.eventId == eventId if (isLatestReceived) { val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: return@tryTransactionAsync + ?: return@tryTransactionAsync roomSummary.notificationCount = 0 roomSummary.highlightCount = 0 } @@ -87,13 +87,14 @@ internal class DefaultSetReadMarkersTask(private val roomAPI: RoomAPI, var isEventRead = false monarchy.doWithRealm { val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst() - ?: return@doWithRealm + ?: return@doWithRealm val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId) - ?: return@doWithRealm + ?: return@doWithRealm val readReceiptIndex = liveChunk.events.find(readReceipt.eventId)?.displayIndex - ?: Int.MAX_VALUE - val eventToCheckIndex = liveChunk.events.find(eventId)?.displayIndex ?: -1 - isEventRead = eventToCheckIndex >= readReceiptIndex + ?: Int.MIN_VALUE + val eventToCheckIndex = liveChunk.events.find(eventId)?.displayIndex + ?: Int.MAX_VALUE + isEventRead = eventToCheckIndex <= readReceiptIndex } return isEventRead } 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 c3912ab8..ef8569e6 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 @@ -23,7 +23,6 @@ import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.util.Cancelable 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 import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection @@ -49,7 +48,6 @@ internal class DefaultSendService(private val roomId: String, val chunkEntity = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return@tryTransactionAsync chunkEntity.add(roomId, event, PaginationDirection.FORWARDS) - chunkEntity.updateDisplayIndexes() } val sendContentWorkerParams = SendEventWorker.Params(roomId, event) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt new file mode 100644 index 00000000..e08a9a65 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -0,0 +1,189 @@ +/* + * + * * Copyright 2019 New Vector Ltd + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package im.vector.matrix.android.internal.session.room.timeline + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.room.timeline.Timeline +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.internal.database.RealmLiveEntityObserver +import im.vector.matrix.android.internal.database.mapper.asDomain +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.EventEntityFields +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor +import im.vector.matrix.android.internal.task.TaskExecutor +import io.realm.OrderedCollectionChangeSet +import io.realm.RealmResults +import io.realm.Sort + + +private const val INITIAL_LOAD_SIZE = 30 + +internal class DefaultTimeline( + private val roomId: String, + private val initialEventId: String? = null, + private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor, + private val boundaryCallback: TimelineBoundaryCallback, + private val contextOfEventTask: GetContextOfEventTask, + private val roomMemberExtractor: RoomMemberExtractor +) : Timeline { + + private var prevDisplayIndex: Int = 0 + private var nextDisplayIndex: Int = 0 + private val isLive = initialEventId == null + + private val listeners = mutableListOf() + + private val builtEvents = mutableListOf() + private lateinit var liveResults: RealmResults + + private val entityObserver = object : RealmLiveEntityObserver(monarchy) { + + override val query: Monarchy.Query + get() = buildQuery(initialEventId) + + override fun onChanged(realmResults: RealmResults, changeSet: OrderedCollectionChangeSet) { + changeSet.insertionRanges.forEach { + val (startIndex, direction) = if (it.startIndex == 0) { + Pair(realmResults[it.length]!!.displayIndex, Timeline.Direction.FORWARDS) + } else { + Pair(realmResults[it.startIndex]!!.displayIndex, Timeline.Direction.FORWARDS) + } + addFromLiveResults(startIndex, direction, it.length.toLong()) + } + } + + override fun processInitialResults(results: RealmResults) { + // Results are ordered DESCENDING, so first items is the most recent + liveResults = results + val initialDisplayIndex = if (isLive) { + results.first()?.displayIndex + } else { + results.where().equalTo(EventEntityFields.EVENT_ID, initialEventId).findFirst()?.displayIndex + } ?: 0 + prevDisplayIndex = initialDisplayIndex + nextDisplayIndex = initialDisplayIndex + val count = Math.min(INITIAL_LOAD_SIZE, results.size).toLong() + if (isLive) { + addFromLiveResults(initialDisplayIndex, Timeline.Direction.BACKWARDS, count) + } else { + val forwardCount = count / 2L + val backwardCount = count - forwardCount + addFromLiveResults(initialDisplayIndex, Timeline.Direction.BACKWARDS, backwardCount) + addFromLiveResults(initialDisplayIndex, Timeline.Direction.BACKWARDS, forwardCount) + } + } + } + + override fun paginate(direction: Timeline.Direction, count: Int) { + monarchy.postToMonarchyThread { + val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex + val shouldHitNetwork = addFromLiveResults(startDisplayIndex, direction, count.toLong()).not() + if (shouldHitNetwork) { + if (direction == Timeline.Direction.BACKWARDS) { + val itemAtEnd = builtEvents.last() + boundaryCallback.onItemAtEndLoaded(itemAtEnd) + } else { + val itemAtFront = builtEvents.first() + boundaryCallback.onItemAtFrontLoaded(itemAtFront) + } + } + } + } + + override fun addListener(listener: Timeline.Listener) { + if (listeners.isEmpty()) { + entityObserver.start() + } + listeners.add(listener) + } + + override fun removeListener(listener: Timeline.Listener) { + listeners.remove(listener) + if (listeners.isEmpty()) { + entityObserver.dispose() + } + } + + override fun removeAllListeners() { + listeners.clear() + if (listeners.isEmpty()) { + entityObserver.dispose() + } + } + + /** + * @return true if count items has been added + */ + private fun addFromLiveResults(startDisplayIndex: Int, + direction: Timeline.Direction, + count: Long): Boolean { + val offsetResults = getOffsetResults(startDisplayIndex, direction, count) + if (offsetResults.isEmpty()) { + return false + } + val offsetIndex = offsetResults.last()!!.displayIndex + if (direction == Timeline.Direction.BACKWARDS) { + prevDisplayIndex = offsetIndex - 1 + } else { + nextDisplayIndex = offsetIndex + 1 + } + offsetResults.forEach { eventEntity -> + val roomMember = roomMemberExtractor.extractFrom(eventEntity) + val timelineEvent = TimelineEvent(eventEntity.asDomain(), eventEntity.localId, roomMember) + val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size + builtEvents.add(position, timelineEvent) + } + return offsetResults.size.toLong() == count + } + + private fun getOffsetResults(startDisplayIndex: Int, + direction: Timeline.Direction, + count: Long): RealmResults { + val offsetQuery = liveResults.where() + if (direction == Timeline.Direction.BACKWARDS) { + offsetQuery + .sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) + .lessThanOrEqualTo(EventEntityFields.DISPLAY_INDEX, startDisplayIndex) + } else { + offsetQuery + .sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) + .greaterThanOrEqualTo(EventEntityFields.DISPLAY_INDEX, startDisplayIndex) + } + return offsetQuery.limit(count).findAll() + } + + private fun buildQuery(eventId: String?): Monarchy.Query { + return Monarchy.Query { realm -> + val query = if (eventId == null) { + EventEntity + .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY) + .equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST}", true) + } else { + EventEntity + .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH) + .`in`("${EventEntityFields.CHUNK}.${ChunkEntityFields.EVENTS.EVENT_ID}", arrayOf(eventId)) + } + query.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) + } + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index 5f0ea001..9cf5dd47 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -20,10 +20,7 @@ import androidx.lifecycle.LiveData import androidx.paging.LivePagedListBuilder import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.session.room.timeline.TimelineEventInterceptor -import im.vector.matrix.android.api.session.room.timeline.TimelineEvent -import im.vector.matrix.android.api.session.room.timeline.TimelineData -import im.vector.matrix.android.api.session.room.timeline.TimelineService +import im.vector.matrix.android.api.session.room.timeline.* import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.ChunkEntityFields import im.vector.matrix.android.internal.database.model.EventEntity @@ -37,6 +34,7 @@ import im.vector.matrix.android.internal.util.PagingRequestHelper import im.vector.matrix.android.internal.util.tryTransactionAsync import io.realm.Realm import io.realm.RealmQuery +import io.realm.Sort private const val PAGE_SIZE = 100 private const val PREFETCH_DISTANCE = 30 @@ -79,6 +77,10 @@ internal class DefaultTimelineService(private val roomId: String, } } + override fun createTimeline(eventId: String?): Timeline { + return DefaultTimeline(roomId, eventId, monarchy, taskExecutor, boundaryCallback, contextOfEventTask, roomMemberExtractor) + } + // PRIVATE FUNCTIONS *************************************************************************** private fun getInitialLoadKey(eventId: String?): Int { @@ -137,7 +139,7 @@ internal class DefaultTimelineService(private val roomId: String, .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH) .`in`("${EventEntityFields.CHUNK}.${ChunkEntityFields.EVENTS.EVENT_ID}", arrayOf(eventId)) } - return query.sort(EventEntityFields.DISPLAY_INDEX) + return query.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) } 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 9289ee11..bb74f2cc 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 @@ -54,7 +54,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { 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. + // The current chunk is the one we will keep all along the merge processChanges. // We try to look for a chunk next to the token, // otherwise we create a whole new one diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityUpdater.kt index 24529ee1..b09fa911 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityUpdater.kt @@ -42,7 +42,7 @@ internal class UserEntityUpdater(monarchy: Monarchy, } - override fun process(inserted: List, updated: List, deleted: List) { + override fun processChanges(inserted: List, updated: List, deleted: List) { val roomMembersEvents = inserted.map { it.eventId } val taskParams = UpdateUserTask.Params(roomMembersEvents) updateUserTask From c12bc5e02dfeec13523e26efa7c7cd9acbcaad84 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 15 Mar 2019 19:27:56 +0100 Subject: [PATCH 02/12] Timeline rework : first version working for backward navigation (need more testing) --- .../home/room/detail/RoomDetailActions.kt | 3 +- .../home/room/detail/RoomDetailFragment.kt | 31 +-- .../home/room/detail/RoomDetailViewModel.kt | 21 +- .../home/room/detail/RoomDetailViewState.kt | 2 + .../room/detail/ScrollOnNewMessageCallback.kt | 2 +- .../timeline/TimelineEventController.kt | 106 ++++++--- .../EndlessRecyclerViewScrollListener.java | 147 ++++++++++++ .../helper/TimelineEventDiffUtilCallback.kt | 44 ++++ .../paging/PagedListEpoxyController.kt | 129 ----------- .../timeline/paging/PagedListModelCache.kt | 149 ------------ .../main/java/im/vector/matrix/rx/RxRoom.kt | 5 - .../room/timeline/TimelineHolderTest.kt | 1 - .../api/session/room/timeline/Timeline.kt | 11 +- .../session/room/timeline/TimelineEvent.kt | 1 + .../session/room/timeline/TimelineService.kt | 11 - .../database/RealmLiveEntityObserver.kt | 5 + .../internal/database/model/EventEntity.kt | 3 +- .../internal/session/room/RoomFactory.kt | 6 +- .../room/members/LoadRoomMembersTask.kt | 9 +- .../session/room/timeline/DefaultTimeline.kt | 217 +++++++++++------- .../room/timeline/DefaultTimelineService.kt | 46 +--- .../room/timeline/TimelineEventFactory.kt | 36 +++ .../room/timeline/TokenChunkEventPersistor.kt | 14 +- 23 files changed, 500 insertions(+), 499 deletions(-) create mode 100644 app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.java create mode 100644 app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventDiffUtilCallback.kt delete mode 100644 app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListEpoxyController.kt delete mode 100644 app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListModelCache.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt index f3ca3b3d..47fd8ed2 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt @@ -22,6 +22,7 @@ sealed class RoomDetailActions { data class SendMessage(val text: String) : RoomDetailActions() object IsDisplayed : RoomDetailActions() - data class EventDisplayed(val event: TimelineEvent, val index: Int) : RoomDetailActions() + data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions() + object LoadMore: RoomDetailActions() } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 296918c7..ab128909 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -24,7 +24,6 @@ import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyVisibilityTracker -import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.R @@ -36,6 +35,7 @@ import im.vector.riotredesign.features.home.HomeModule import im.vector.riotredesign.features.home.HomePermalinkHandler import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController import im.vector.riotredesign.features.home.room.detail.timeline.animation.TimelineItemAnimator +import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener import im.vector.riotredesign.features.media.MediaContentRenderer import im.vector.riotredesign.features.media.MediaViewerActivity import kotlinx.android.parcel.Parcelize @@ -80,7 +80,6 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { setupRecyclerView() setupToolbar() setupSendButton() - timelineEventController.requestModelBuild() roomDetailViewModel.subscribe { renderState(it) } } @@ -111,6 +110,11 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { it.dispatchTo(stateRestorer) it.dispatchTo(scrollOnNewMessageCallback) } + recyclerView.addOnScrollListener(object : EndlessRecyclerViewScrollListener(layoutManager, EndlessRecyclerViewScrollListener.LoadOnScrollDirection.BOTTOM) { + override fun onLoadMore(page: Int, totalItemsCount: Int) { + roomDetailViewModel.process(RoomDetailActions.LoadMore) + } + }) recyclerView.setController(timelineEventController) timelineEventController.callback = this } @@ -119,29 +123,16 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { sendButton.setOnClickListener { val textMessage = composerEditText.text.toString() if (textMessage.isNotBlank()) { - composerEditText.text = null roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage)) + composerEditText.text = null } } } private fun renderState(state: RoomDetailViewState) { renderRoomSummary(state) - renderTimeline(state) - } - - private fun renderTimeline(state: RoomDetailViewState) { - when (state.asyncTimelineData) { - is Success -> { - val timelineData = state.asyncTimelineData() - val lockAutoScroll = timelineData?.let { - it.events == timelineEventController.currentList && it.isLoadingForward - } ?: true - - scrollOnNewMessageCallback.isLocked.set(lockAutoScroll) - timelineEventController.update(timelineData) - } - } + timelineEventController.setTimeline(state.timeline) + //renderTimeline(state) } private fun renderRoomSummary(state: RoomDetailViewState) { @@ -163,8 +154,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { homePermalinkHandler.launch(url) } - override fun onEventVisible(event: TimelineEvent, index: Int) { - roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event, index)) + override fun onEventVisible(event: TimelineEvent) { + roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event)) } override fun onMediaClicked(mediaData: MediaContentRenderer.Data, view: View) { diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index 4ca21b94..e1e80bcb 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -22,8 +22,8 @@ import com.jakewharton.rxrelay2.BehaviorRelay import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.rx.rx -import im.vector.riotredesign.core.extensions.lastMinBy import im.vector.riotredesign.core.platform.RiotViewModel import im.vector.riotredesign.features.home.room.VisibleRoomStore import io.reactivex.rxkotlin.subscribeBy @@ -38,8 +38,8 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, private val room = session.getRoom(initialState.roomId)!! private val roomId = initialState.roomId private val eventId = initialState.eventId - private val displayedEventsObservable = BehaviorRelay.create() + private val timeline = room.createTimeline(eventId) companion object : MvRxViewModelFactory { @@ -53,9 +53,10 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, init { observeRoomSummary() - observeTimeline() observeDisplayedEvents() room.loadRoomMembersIfNeeded() + timeline.start() + setState { copy(timeline = this@RoomDetailViewModel.timeline) } } fun process(action: RoomDetailActions) { @@ -63,6 +64,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, is RoomDetailActions.SendMessage -> handleSendMessage(action) is RoomDetailActions.IsDisplayed -> handleIsDisplayed() is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action) + is RoomDetailActions.LoadMore -> timeline.paginate(Timeline.Direction.BACKWARDS, 50) } } @@ -83,11 +85,11 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, private fun observeDisplayedEvents() { // We are buffering scroll events for one second // and keep the most recent one to set the read receipt on. - displayedEventsObservable.hide() + displayedEventsObservable .buffer(1, TimeUnit.SECONDS) .filter { it.isNotEmpty() } .subscribeBy(onNext = { actions -> - val mostRecentEvent = actions.lastMinBy { it.index } + val mostRecentEvent = actions.maxBy { it.event.displayIndex } mostRecentEvent?.event?.root?.eventId?.let { eventId -> room.setReadReceipt(eventId, callback = object : MatrixCallback {}) } @@ -102,12 +104,9 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, } } - private fun observeTimeline() { - room.rx().timeline(eventId) - .execute { timelineData -> - copy(asyncTimelineData = timelineData) - } + override fun onCleared() { + super.onCleared() + timeline.dispose() } - } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt index ca2f39b7..4df1551c 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt @@ -20,11 +20,13 @@ import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineData data class RoomDetailViewState( val roomId: String, val eventId: String?, + val timeline: Timeline? = null, val asyncRoomSummary: Async = Uninitialized, val asyncTimelineData: Async = Uninitialized ) : MvRxState { diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/ScrollOnNewMessageCallback.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/ScrollOnNewMessageCallback.kt index 719a570a..6c93997b 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/ScrollOnNewMessageCallback.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/ScrollOnNewMessageCallback.kt @@ -25,7 +25,7 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager) var isLocked = AtomicBoolean(true) override fun onInserted(position: Int, count: Int) { - if (isLocked.compareAndSet(false, true) && position == 0 && layoutManager.findFirstVisibleItemPosition() == 0) { + if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0) { layoutManager.scrollToPosition(0) } } 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 d8558e6a..55bd6e0c 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,44 +17,79 @@ package im.vector.riotredesign.features.home.room.detail.timeline import android.view.View +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyAsyncUtil +import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.VisibilityState -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.room.timeline.TimelineData +import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent -import im.vector.riotredesign.core.epoxy.LoadingItemModel_ import im.vector.riotredesign.core.epoxy.RiotEpoxyModel import im.vector.riotredesign.core.extensions.localDateTime import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter +import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem_ -import im.vector.riotredesign.features.home.room.detail.timeline.paging.PagedListEpoxyController import im.vector.riotredesign.features.media.MediaContentRenderer class TimelineEventController(private val dateFormatter: TimelineDateFormatter, private val timelineItemFactory: TimelineItemFactory, private val timelineMediaSizeProvider: TimelineMediaSizeProvider -) : PagedListEpoxyController( +) : EpoxyController( EpoxyAsyncUtil.getAsyncBackgroundHandler(), EpoxyAsyncUtil.getAsyncBackgroundHandler() -) { +), Timeline.Listener { + + private val modelCache = arrayListOf>>() + private var currentSnapshot: List = emptyList() + + override fun onUpdated(snapshot: List) { + submitSnapshot(snapshot) + } + + private val listUpdateCallback = object : ListUpdateCallback { + override fun onChanged(position: Int, count: Int, payload: Any?) { + (position until (position + count)).forEach { + modelCache[it] = emptyList() + } + requestModelBuild() + } + + override fun onMoved(fromPosition: Int, toPosition: Int) { + //no-op + } + + override fun onInserted(position: Int, count: Int) { + if (modelCache.isNotEmpty() && position == modelCache.size) { + modelCache[position - 1] = emptyList() + } + (0 until count).forEach { + modelCache.add(position, emptyList()) + } + requestModelBuild() + } + + override fun onRemoved(position: Int, count: Int) { + //no-op + } + + } private var isLoadingForward: Boolean = false private var isLoadingBackward: Boolean = false private var hasReachedEnd: Boolean = true + private var timeline: Timeline? = null var callback: Callback? = null - fun update(timelineData: TimelineData?) { - timelineData?.let { - isLoadingForward = it.isLoadingForward - isLoadingBackward = it.isLoadingBackward - hasReachedEnd = it.events.lastOrNull()?.root?.type == EventType.STATE_ROOM_CREATE - submitList(it.events) - requestModelBuild() + fun setTimeline(timeline: Timeline?) { + if (this.timeline != timeline) { + this.timeline = timeline + this.timeline?.listener = this + submitSnapshot(timeline?.snapshot() ?: emptyList()) } } @@ -63,12 +98,22 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, timelineMediaSizeProvider.recyclerView = recyclerView } - override fun buildItemModels(currentPosition: Int, items: List): List> { - if (items.isNullOrEmpty()) { - return emptyList() + override fun buildModels() { + add(getModels()) + } + + private fun getModels(): List> { + (0 until modelCache.size).forEach { position -> + if (modelCache[position].isEmpty()) { + modelCache[position] = buildItemModels(position, currentSnapshot) + } } + return modelCache.flatten() + } + + private fun buildItemModels(currentPosition: Int, items: List): List> { val epoxyModels = ArrayList>() - val event = items[currentPosition] ?: return emptyList() + val event = items[currentPosition] val nextEvent = if (currentPosition + 1 < items.size) items[currentPosition + 1] else null val date = event.root.localDateTime() @@ -77,7 +122,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, timelineItemFactory.create(event, nextEvent, callback).also { it.id(event.localId) - it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event, currentPosition)) + it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) epoxyModels.add(it) } if (addDaySeparator) { @@ -88,21 +133,17 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, return epoxyModels } - override fun addModels(models: List>) { - LoadingItemModel_() - .id("forward_loading_item") - .addIf(isLoadingForward, this) - - super.add(models) - - LoadingItemModel_() - .id("backward_loading_item") - .addIf(!hasReachedEnd, this) + private fun submitSnapshot(newSnapshot: List) { + EpoxyAsyncUtil.getAsyncBackgroundHandler().post { + val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot) + currentSnapshot = newSnapshot + val diffResult = DiffUtil.calculateDiff(diffCallback) + diffResult.dispatchUpdatesTo(listUpdateCallback) + } } - interface Callback { - fun onEventVisible(event: TimelineEvent, index: Int) + fun onEventVisible(event: TimelineEvent) fun onUrlClicked(url: String) fun onMediaClicked(mediaData: MediaContentRenderer.Data, view: View) } @@ -110,13 +151,12 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, } private class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?, - private val event: TimelineEvent, - private val currentPosition: Int) + private val event: TimelineEvent) : RiotEpoxyModel.OnVisibilityStateChangedListener { override fun onVisibilityStateChanged(visibilityState: Int) { if (visibilityState == VisibilityState.VISIBLE) { - callback?.onEventVisible(event, currentPosition) + callback?.onEventVisible(event) } } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.java b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.java new file mode 100644 index 00000000..5c1c11b2 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.java @@ -0,0 +1,147 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.features.home.room.detail.timeline.helper; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +// Todo rework that, it has been copy/paste at the moment +public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnScrollListener { + // Sets the starting page index + private static final int startingPageIndex = 0; + // The minimum amount of items to have below your current scroll position + // before loading more. + private int visibleThreshold = 30; + // The current offset index of data you have loaded + private int currentPage = 0; + // The total number of items in the dataset after the last load + private int previousTotalItemCount = 0; + // True if we are still waiting for the last set of data to load. + private boolean loading = true; + private LinearLayoutManager mLayoutManager; + private LoadOnScrollDirection mDirection; + + public EndlessRecyclerViewScrollListener(LinearLayoutManager layoutManager, LoadOnScrollDirection direction) { + this.mLayoutManager = layoutManager; + mDirection = direction; + } + + + // This happens many times a second during a scroll, so be wary of the code you place here. + // We are given a few useful parameters to help us work out if we need to load some more data, + // but first we check if we are waiting for the previous load to finish. + @Override + public void onScrolled(@NonNull RecyclerView view, int dx, int dy) { + int lastVisibleItemPosition = 0; + int firstVisibleItemPosition = 0; + int totalItemCount = mLayoutManager.getItemCount(); + + lastVisibleItemPosition = mLayoutManager.findLastVisibleItemPosition(); + firstVisibleItemPosition = mLayoutManager.findFirstVisibleItemPosition(); + + switch (mDirection) { + case BOTTOM: + // If the total item count is zero and the previous isn't, assume the + // list is invalidated and should be reset back to initial state + if (totalItemCount < previousTotalItemCount) { + this.currentPage = startingPageIndex; + this.previousTotalItemCount = totalItemCount; + if (totalItemCount == 0) { + this.loading = true; + } + } + // If it’s still loading, we check to see if the dataset count has + // changed, if so we conclude it has finished loading and update the current page + // number and total item count. + if (loading && (totalItemCount > previousTotalItemCount)) { + loading = false; + previousTotalItemCount = totalItemCount; + } + + // If it isn’t currently loading, we check to see if we have breached + // the visibleThreshold and need to reload more data. + // If we do need to reload some more data, we execute onLoadMore to fetch the data. + // threshold should reflect how many total columns there are too + if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) { + currentPage++; + onLoadMore(currentPage, totalItemCount); + loading = true; + } + break; + case TOP: + // If the total item count is zero and the previous isn't, assume the + // list is invalidated and should be reset back to initial state + if (totalItemCount < previousTotalItemCount) { + this.currentPage = startingPageIndex; + this.previousTotalItemCount = totalItemCount; + if (totalItemCount == 0) { + this.loading = true; + } + } + // If it’s still loading, we check to see if the dataset count has + // changed, if so we conclude it has finished loading and update the current page + // number and total item count. + if (loading && (totalItemCount > previousTotalItemCount)) { + loading = false; + previousTotalItemCount = totalItemCount; + } + + // If it isn’t currently loading, we check to see if we have breached + // the visibleThreshold and need to reload more data. + // If we do need to reload some more data, we execute onLoadMore to fetch the data. + // threshold should reflect how many total columns there are too + if (!loading && firstVisibleItemPosition < visibleThreshold) { + currentPage++; + onLoadMore(currentPage, totalItemCount); + loading = true; + } + break; + } + } + + private int getLastVisibleItem(int[] lastVisibleItemPositions) { + int maxSize = 0; + for (int i = 0; i < lastVisibleItemPositions.length; i++) { + if (i == 0) { + maxSize = lastVisibleItemPositions[i]; + } else if (lastVisibleItemPositions[i] > maxSize) { + maxSize = lastVisibleItemPositions[i]; + } + } + return maxSize; + } + + private int getFirstVisibleItem(int[] firstVisibleItemPositions) { + int maxSize = 0; + for (int i = 0; i < firstVisibleItemPositions.length; i++) { + if (i == 0) { + maxSize = firstVisibleItemPositions[i]; + } else if (firstVisibleItemPositions[i] > maxSize) { + maxSize = firstVisibleItemPositions[i]; + } + } + return maxSize; + } + + // Defines the process for actually loading more data based on page + public abstract void onLoadMore(int page, int totalItemsCount); + + public enum LoadOnScrollDirection { + TOP, BOTTOM + } +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventDiffUtilCallback.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventDiffUtilCallback.kt new file mode 100644 index 00000000..cd62b511 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventDiffUtilCallback.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.features.home.room.detail.timeline.helper + +import androidx.recyclerview.widget.DiffUtil +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent + +class TimelineEventDiffUtilCallback(private val oldList: List, + private val newList: List) : DiffUtil.Callback() { + + override fun getOldListSize(): Int { + return oldList.size + } + + override fun getNewListSize(): Int { + return newList.size + } + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldList[oldItemPosition] + val newItem = newList[newItemPosition] + return oldItem.localId == newItem.localId + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldList[oldItemPosition] + val newItem = newList[newItemPosition] + return oldItem == newItem + } +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListEpoxyController.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListEpoxyController.kt deleted file mode 100644 index 30f11a8b..00000000 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListEpoxyController.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.riotredesign.features.home.room.detail.timeline.paging - -import androidx.paging.PagedList -import android.os.Handler -import androidx.recyclerview.widget.DiffUtil -import com.airbnb.epoxy.EpoxyController -import com.airbnb.epoxy.EpoxyModel -import com.airbnb.epoxy.EpoxyViewHolder - -/** - * An [EpoxyController] that can work with a [PagedList]. - * - * Internally, it caches the model for each item in the [PagedList]. You should override - * [buildItemModel] method to build the model for the given item. Since [PagedList] might include - * `null` items if placeholders are enabled, this method needs to handle `null` values in the list. - * - * By default, the model for each item is added to the model list. To change this behavior (to - * filter items or inject extra items), you can override [addModels] function and manually add built - * models. - * - * @param T The type of the items in the [PagedList]. - */ -abstract class PagedListEpoxyController( - /** - * The handler to use for building models. By default this uses the main thread, but you can use - * [EpoxyAsyncUtil.getAsyncBackgroundHandler] to do model building in the background. - * - * The notify thread of your PagedList (from setNotifyExecutor in the PagedList Builder) must be - * the same as this thread. Otherwise Epoxy will crash. - */ - modelBuildingHandler: Handler = EpoxyController.defaultModelBuildingHandler, - /** - * The handler to use when calculating the diff between built model lists. - * By default this uses the main thread, but you can use - * [EpoxyAsyncUtil.getAsyncBackgroundHandler] to do diffing in the background. - */ - diffingHandler: Handler = EpoxyController.defaultDiffingHandler, - /** - * [PagedListEpoxyController] uses an [DiffUtil.ItemCallback] to detect changes between - * [PagedList]s. By default, it relies on simple object equality but you can provide a custom - * one if you don't use all fields in the object in your models. - */ - itemDiffCallback: DiffUtil.ItemCallback = DEFAULT_ITEM_DIFF_CALLBACK as DiffUtil.ItemCallback -) : EpoxyController(modelBuildingHandler, diffingHandler) { - // this is where we keep the already built models - protected val modelCache = PagedListModelCache( - modelBuilder = { pos, item -> - buildItemModels(pos, item) - }, - rebuildCallback = { - requestModelBuild() - }, - itemDiffCallback = itemDiffCallback, - modelBuildingHandler = modelBuildingHandler - ) - - var currentList: PagedList? = null - private set - - final override fun buildModels() { - addModels(modelCache.getModels()) - } - - override fun onModelBound( - holder: EpoxyViewHolder, - boundModel: EpoxyModel<*>, - position: Int, - previouslyBoundModel: EpoxyModel<*>? - ) { - modelCache.loadAround(boundModel) - } - - /** - * This function adds all built models to the adapter. You can override this method to add extra - * items into the model list or remove some. - */ - open fun addModels(models: List>) { - super.add(models) - } - - /** - * Builds the model for a given item. This must return a single model for each item. If you want - * to inject headers etc, you can override [addModels] function. - * - * If the `item` is `null`, you should provide the placeholder. If your [PagedList] is configured - * without placeholders, you don't need to handle the `null` case. - */ - abstract fun buildItemModels(currentPosition: Int, items: List): List> - - /** - * Submit a new paged list. - * - * A diff will be calculated between this list and the previous list so you may still get calls - * to [buildItemModel] with items from the previous list. - */ - fun submitList(newList: PagedList?) { - currentList = newList - modelCache.submitList(newList) - } - - companion object { - /** - * [PagedListEpoxyController] calculates a diff on top of the PagedList to check which - * models are invalidated. - * This is the default [DiffUtil.ItemCallback] which uses object equality. - */ - val DEFAULT_ITEM_DIFF_CALLBACK = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Any, newItem: Any) = oldItem == newItem - - override fun areContentsTheSame(oldItem: Any, newItem: Any) = oldItem == newItem - } - } -} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListModelCache.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListModelCache.kt deleted file mode 100644 index cbaf81a7..00000000 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListModelCache.kt +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.riotredesign.features.home.room.detail.timeline.paging - -import android.annotation.SuppressLint -import android.os.Handler -import androidx.paging.AsyncPagedListDiffer -import androidx.paging.PagedList -import androidx.recyclerview.widget.AsyncDifferConfig -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListUpdateCallback -import com.airbnb.epoxy.EpoxyModel -import java.util.concurrent.Executor -import java.util.concurrent.atomic.AtomicBoolean - -/** - * A PagedList stream wrapper that caches models built for each item. It tracks changes in paged lists and caches - * models for each item when they are invalidated to avoid rebuilding models for the whole list when PagedList is - * updated. - */ -class PagedListModelCache( - private val modelBuilder: (itemIndex: Int, items: List) -> List>, - private val rebuildCallback: () -> Unit, - private val itemDiffCallback: DiffUtil.ItemCallback, - private val diffExecutor: Executor? = null, - private val modelBuildingHandler: Handler -) { - - - // Int is the index of the pagedList item - // We have to be able to find the pagedlist position coming from an epoxy model to trigger - // LoadAround with accuracy - private val modelCache = linkedMapOf, Int>() - private var isCacheStale = AtomicBoolean(true) - - /** - * Tracks the last accessed position so that we can report it back to the paged list when models are built. - */ - private var lastPosition: Int? = null - - /** - * Observer for the PagedList changes that invalidates the model cache when data is updated. - */ - private val updateCallback = object : ListUpdateCallback { - override fun onChanged(position: Int, count: Int, payload: Any?) { - invalidate() - rebuildCallback() - } - - override fun onMoved(fromPosition: Int, toPosition: Int) { - invalidate() - rebuildCallback() - } - - override fun onInserted(position: Int, count: Int) { - invalidate() - rebuildCallback() - } - - override fun onRemoved(position: Int, count: Int) { - invalidate() - rebuildCallback() - } - } - - @SuppressLint("RestrictedApi") - private val asyncDiffer = AsyncPagedListDiffer( - updateCallback, - AsyncDifferConfig.Builder( - itemDiffCallback - ).also { builder -> - if (diffExecutor != null) { - builder.setBackgroundThreadExecutor(diffExecutor) - } - // we have to reply on this private API, otherwise, paged list might be changed when models are being built, - // potentially creating concurrent modification problems. - builder.setMainThreadExecutor { runnable: Runnable -> - modelBuildingHandler.post(runnable) - } - }.build() - ) - - fun submitList(pagedList: PagedList?) { - asyncDiffer.submitList(pagedList) - } - - fun getModels(): List> { - if (isCacheStale.compareAndSet(true, false)) { - asyncDiffer.currentList?.forEachIndexed { position, _ -> - buildModel(position) - } - } - lastPosition?.let { - triggerLoadAround(it) - } - return modelCache.keys.toList() - } - - fun loadAround(model: EpoxyModel<*>) { - modelCache[model]?.let { itemPosition -> - triggerLoadAround(itemPosition) - lastPosition = itemPosition - } - } - - // PRIVATE METHODS ***************************************************************************** - - private fun invalidate() { - modelCache.clear() - isCacheStale.set(true) - } - - private fun cacheModelsAtPosition(itemPosition: Int, epoxyModels: Set>) { - epoxyModels.forEach { - modelCache[it] = itemPosition - } - } - - private fun buildModel(pos: Int) { - if (pos >= asyncDiffer.currentList?.size ?: 0) { - return - } - modelBuilder(pos, asyncDiffer.currentList as List).also { - cacheModelsAtPosition(pos, it.toSet()) - } - } - - private fun triggerLoadAround(position: Int) { - asyncDiffer.currentList?.let { - if (it.size > 0) { - it.loadAround(Math.min(position, it.size - 1)) - } - } - } -} diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index 054fb438..2fbed52b 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -18,7 +18,6 @@ package im.vector.matrix.rx import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.model.RoomSummary -import im.vector.matrix.android.api.session.room.timeline.TimelineData import io.reactivex.Observable class RxRoom(private val room: Room) { @@ -27,10 +26,6 @@ class RxRoom(private val room: Room) { return room.roomSummary.asObservable() } - fun timeline(eventId: String? = null): Observable { - return room.timeline(eventId).asObservable() - } - } fun Room.rx(): RxRoom { diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineHolderTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineHolderTest.kt index cf838528..917b955b 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineHolderTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineHolderTest.kt @@ -31,7 +31,6 @@ import im.vector.matrix.android.internal.util.PagingRequestHelper import im.vector.matrix.android.testCoroutineDispatchers import io.realm.Realm import io.realm.RealmConfiguration -import org.amshove.kluent.shouldEqual import org.junit.Before import org.junit.Rule import org.junit.Test diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index 82601b50..9e30d452 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -20,13 +20,16 @@ package im.vector.matrix.android.api.session.room.timeline interface Timeline { + var listener: Timeline.Listener? + + fun size(): Int + fun snapshot(): List fun paginate(direction: Direction, count: Int) - fun addListener(listener: Listener) - fun removeListener(listener: Listener) - fun removeAllListeners() + fun start() + fun dispose() interface Listener { - + fun onUpdated(snapshot: List) } enum class Direction(val value: String) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index 89e1724f..ffd7dbc6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -27,6 +27,7 @@ import im.vector.matrix.android.api.session.room.model.RoomMember data class TimelineEvent( val root: Event, val localId: String, + val displayIndex: Int, val roomMember: RoomMember? ) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt index 9e7edc61..28af1fe2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt @@ -16,22 +16,11 @@ package im.vector.matrix.android.api.session.room.timeline -import androidx.lifecycle.LiveData - /** * This interface defines methods to interact with the timeline. It's implemented at the room level. */ interface TimelineService { - /** - * This is the main method of the service. It allows to listen for live [TimelineData]. - * It's automatically refreshed as soon as timeline data gets updated, through sync or pagination. - * - * @param eventId: an optional eventId to start loading timeline around. - * @return the [LiveData] of [TimelineData] - */ - fun timeline(eventId: String? = null): LiveData - fun createTimeline(eventId: String?): Timeline } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt index 27ff501f..490742ca 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt @@ -26,6 +26,7 @@ import java.util.concurrent.atomic.AtomicReference internal interface LiveEntityObserver { fun start() fun dispose() + fun isStarted(): Boolean } internal abstract class RealmLiveEntityObserver(protected val monarchy: Monarchy) @@ -56,6 +57,10 @@ internal abstract class RealmLiveEntityObserver(protected val m } } + override fun isStarted(): Boolean { + return isStarted.get() + } + protected open fun onChanged(realmResults: RealmResults, changeSet: OrderedCollectionChangeSet) { val insertionIndexes = changeSet.insertions val updateIndexes = changeSet.changes 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 0be7b106..5a7b04d8 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,12 +18,13 @@ package im.vector.matrix.android.internal.database.model import io.realm.RealmObject import io.realm.RealmResults +import io.realm.annotations.Index import io.realm.annotations.LinkingObjects import io.realm.annotations.PrimaryKey import java.util.* internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUID().toString(), - var eventId: String = "", + @Index var eventId: String = "", var roomId: String = "", var type: String = "", var content: String? = null, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 6b2c796c..6183be5c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -28,7 +28,7 @@ import im.vector.matrix.android.internal.session.room.send.EventFactory import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask import im.vector.matrix.android.internal.session.room.timeline.PaginationTask -import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback +import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.PagingRequestHelper import java.util.concurrent.Executors @@ -44,9 +44,9 @@ internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask, fun instantiate(roomId: String): Room { val helper = PagingRequestHelper(Executors.newSingleThreadExecutor()) - val timelineBoundaryCallback = TimelineBoundaryCallback(roomId, taskExecutor, paginationTask, monarchy, helper) val roomMemberExtractor = RoomMemberExtractor(monarchy, roomId) - val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, timelineBoundaryCallback, contextOfEventTask, roomMemberExtractor) + val timelineEventFactory = TimelineEventFactory(roomMemberExtractor) + val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask, helper) val sendService = DefaultSendService(roomId, eventFactory, monarchy) val readService = DefaultReadService(roomId, monarchy, setReadMarkersTask, taskExecutor) return DefaultRoom( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt index ac20e816..788bb1c1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt @@ -27,6 +27,7 @@ import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.tryTransactionSync +import io.realm.kotlin.createObject internal interface LoadRoomMembersTask : Task { @@ -60,7 +61,7 @@ internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI, .tryTransactionSync { realm -> // We ignore all the already known members val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: throw IllegalStateException("You shouldn't use this method without a room") + ?: realm.createObject(roomId) val roomMembers = RoomMembers(realm, roomId).getLoaded() val eventsToInsert = response.roomMemberEvents.filter { !roomMembers.containsKey(it.stateKey) } @@ -73,9 +74,9 @@ internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI, private fun areAllMembersAlreadyLoaded(roomId: String): Boolean { return monarchy - .fetchAllCopiedSync { RoomEntity.where(it, roomId) } - .firstOrNull() - ?.areAllMembersLoaded ?: false + .fetchAllCopiedSync { RoomEntity.where(it, roomId) } + .firstOrNull() + ?.areAllMembersLoaded ?: false } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index e08a9a65..eca061db 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -18,20 +18,26 @@ package im.vector.matrix.android.internal.session.room.timeline -import com.zhuinden.monarchy.Monarchy +import androidx.annotation.UiThread +import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent -import im.vector.matrix.android.internal.database.RealmLiveEntityObserver -import im.vector.matrix.android.internal.database.mapper.asDomain 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.EventEntityFields import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith +import im.vector.matrix.android.internal.util.PagingRequestHelper import io.realm.OrderedCollectionChangeSet +import io.realm.OrderedRealmCollectionChangeListener +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.RealmQuery import io.realm.RealmResults import io.realm.Sort +import java.util.* +import kotlin.collections.ArrayList private const val INITIAL_LOAD_SIZE = 30 @@ -39,103 +45,136 @@ private const val INITIAL_LOAD_SIZE = 30 internal class DefaultTimeline( private val roomId: String, private val initialEventId: String? = null, - private val monarchy: Monarchy, + private val realmConfiguration: RealmConfiguration, private val taskExecutor: TaskExecutor, - private val boundaryCallback: TimelineBoundaryCallback, private val contextOfEventTask: GetContextOfEventTask, - private val roomMemberExtractor: RoomMemberExtractor + private val timelineEventFactory: TimelineEventFactory, + private val paginationTask: PaginationTask, + private val helper: PagingRequestHelper ) : Timeline { + override var listener: Timeline.Listener? = null + + private lateinit var realm: Realm + private lateinit var liveEvents: RealmResults private var prevDisplayIndex: Int = 0 private var nextDisplayIndex: Int = 0 private val isLive = initialEventId == null + private val builtEvents = Collections.synchronizedList(ArrayList()) - private val listeners = mutableListOf() - private val builtEvents = mutableListOf() - private lateinit var liveResults: RealmResults - - private val entityObserver = object : RealmLiveEntityObserver(monarchy) { - - override val query: Monarchy.Query - get() = buildQuery(initialEventId) - - override fun onChanged(realmResults: RealmResults, changeSet: OrderedCollectionChangeSet) { + private val changeListener = OrderedRealmCollectionChangeListener> { _, changeSet -> + if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) { + handleInitialLoad() + } else { changeSet.insertionRanges.forEach { - val (startIndex, direction) = if (it.startIndex == 0) { - Pair(realmResults[it.length]!!.displayIndex, Timeline.Direction.FORWARDS) + val (startDisplayIndex, direction) = if (it.startIndex == 0) { + Pair(liveEvents[it.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS) } else { - Pair(realmResults[it.startIndex]!!.displayIndex, Timeline.Direction.FORWARDS) + Pair(liveEvents[it.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS) } - addFromLiveResults(startIndex, direction, it.length.toLong()) - } - } - - override fun processInitialResults(results: RealmResults) { - // Results are ordered DESCENDING, so first items is the most recent - liveResults = results - val initialDisplayIndex = if (isLive) { - results.first()?.displayIndex - } else { - results.where().equalTo(EventEntityFields.EVENT_ID, initialEventId).findFirst()?.displayIndex - } ?: 0 - prevDisplayIndex = initialDisplayIndex - nextDisplayIndex = initialDisplayIndex - val count = Math.min(INITIAL_LOAD_SIZE, results.size).toLong() - if (isLive) { - addFromLiveResults(initialDisplayIndex, Timeline.Direction.BACKWARDS, count) - } else { - val forwardCount = count / 2L - val backwardCount = count - forwardCount - addFromLiveResults(initialDisplayIndex, Timeline.Direction.BACKWARDS, backwardCount) - addFromLiveResults(initialDisplayIndex, Timeline.Direction.BACKWARDS, forwardCount) + insertFromLiveResults(startDisplayIndex, direction, it.length.toLong()) } } } + @UiThread override fun paginate(direction: Timeline.Direction, count: Int) { - monarchy.postToMonarchyThread { - val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex - val shouldHitNetwork = addFromLiveResults(startDisplayIndex, direction, count.toLong()).not() - if (shouldHitNetwork) { - if (direction == Timeline.Direction.BACKWARDS) { - val itemAtEnd = builtEvents.last() - boundaryCallback.onItemAtEndLoaded(itemAtEnd) - } else { - val itemAtFront = builtEvents.first() - boundaryCallback.onItemAtFrontLoaded(itemAtFront) - } + if (direction == Timeline.Direction.FORWARDS && isLive) { + return + } + val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex + val hasBuiltCountItems = insertFromLiveResults(startDisplayIndex, direction, count.toLong()) + if (hasBuiltCountItems.not()) { + val token = getToken(direction) ?: return + helper.runIfNotRunning(direction.toRequestType()) { + executePaginationTask(it, token, direction.toPaginationDirection(), 30) } } } - override fun addListener(listener: Timeline.Listener) { - if (listeners.isEmpty()) { - entityObserver.start() - } - listeners.add(listener) + @UiThread + override fun start() { + realm = Realm.getInstance(realmConfiguration) + liveEvents = buildQuery(initialEventId).findAllAsync() + liveEvents.addChangeListener(changeListener) } - override fun removeListener(listener: Timeline.Listener) { - listeners.remove(listener) - if (listeners.isEmpty()) { - entityObserver.dispose() + @UiThread + override fun dispose() { + liveEvents.removeAllChangeListeners() + realm.close() + } + + override fun snapshot(): List = synchronized(builtEvents) { + return builtEvents.toList() + } + + override fun size(): Int = synchronized(builtEvents) { + return builtEvents.size + } + + private fun handleInitialLoad() = synchronized(builtEvents) { + val initialDisplayIndex = if (isLive) { + liveEvents.firstOrNull()?.displayIndex + } else { + liveEvents.where().equalTo(EventEntityFields.EVENT_ID, initialEventId).findFirst()?.displayIndex + } ?: 0 + prevDisplayIndex = initialDisplayIndex + nextDisplayIndex = initialDisplayIndex + val count = Math.min(INITIAL_LOAD_SIZE, liveEvents.size).toLong() + if (count == 0L) { + return@synchronized + } + if (isLive) { + insertFromLiveResults(initialDisplayIndex, Timeline.Direction.BACKWARDS, count) + } else { + val forwardCount = count / 2L + val backwardCount = count - forwardCount + insertFromLiveResults(initialDisplayIndex, Timeline.Direction.BACKWARDS, backwardCount) + insertFromLiveResults(initialDisplayIndex, Timeline.Direction.BACKWARDS, forwardCount) } } - override fun removeAllListeners() { - listeners.clear() - if (listeners.isEmpty()) { - entityObserver.dispose() - } + private fun executePaginationTask(requestCallback: PagingRequestHelper.Request.Callback, + from: String, + direction: PaginationDirection, + limit: Int) { + + val params = PaginationTask.Params(roomId = roomId, + from = from, + direction = direction, + limit = limit) + + paginationTask.configureWith(params) + .enableRetry() + .dispatchTo(object : MatrixCallback { + override fun onSuccess(data: Boolean) { + requestCallback.recordSuccess() + } + + override fun onFailure(failure: Throwable) { + requestCallback.recordFailure(failure) + } + }) + .executeBy(taskExecutor) + } + + private fun getToken(direction: Timeline.Direction): String? { + val chunkEntity = liveEvents.firstOrNull()?.chunk?.firstOrNull() ?: return null + return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken } /** + * This has to be called on MonarchyThread as it access realm live results * @return true if count items has been added */ - private fun addFromLiveResults(startDisplayIndex: Int, - direction: Timeline.Direction, - count: Long): Boolean { + private fun insertFromLiveResults(startDisplayIndex: Int, + direction: Timeline.Direction, + count: Long): Boolean = synchronized(builtEvents) { + if (count < 1) { + throw java.lang.IllegalStateException("You should provide a count superior to 0") + } val offsetResults = getOffsetResults(startDisplayIndex, direction, count) if (offsetResults.isEmpty()) { return false @@ -147,18 +186,18 @@ internal class DefaultTimeline( nextDisplayIndex = offsetIndex + 1 } offsetResults.forEach { eventEntity -> - val roomMember = roomMemberExtractor.extractFrom(eventEntity) - val timelineEvent = TimelineEvent(eventEntity.asDomain(), eventEntity.localId, roomMember) + val timelineEvent = timelineEventFactory.create(eventEntity) val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size builtEvents.add(position, timelineEvent) } + listener?.onUpdated(snapshot()) return offsetResults.size.toLong() == count } private fun getOffsetResults(startDisplayIndex: Int, direction: Timeline.Direction, count: Long): RealmResults { - val offsetQuery = liveResults.where() + val offsetQuery = liveEvents.where() if (direction == Timeline.Direction.BACKWARDS) { offsetQuery .sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) @@ -171,19 +210,27 @@ internal class DefaultTimeline( return offsetQuery.limit(count).findAll() } - private fun buildQuery(eventId: String?): Monarchy.Query { - return Monarchy.Query { realm -> - val query = if (eventId == null) { - EventEntity - .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY) - .equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST}", true) - } else { - EventEntity - .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH) - .`in`("${EventEntityFields.CHUNK}.${ChunkEntityFields.EVENTS.EVENT_ID}", arrayOf(eventId)) - } - query.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) + private fun buildQuery(eventId: String?): RealmQuery { + val query = if (eventId == null) { + EventEntity + .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY) + .equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST}", true) + } else { + EventEntity + .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH) + .`in`("${EventEntityFields.CHUNK}.${ChunkEntityFields.EVENTS.EVENT_ID}", arrayOf(eventId)) } + query.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) + return query + } + private fun Timeline.Direction.toRequestType(): PagingRequestHelper.RequestType { + return if (this == Timeline.Direction.BACKWARDS) PagingRequestHelper.RequestType.BEFORE else PagingRequestHelper.RequestType.AFTER + } + + //Todo : remove that + private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { + return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS + } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index 9cf5dd47..5fa891f4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -16,20 +16,17 @@ package im.vector.matrix.android.internal.session.room.timeline -import androidx.lifecycle.LiveData -import androidx.paging.LivePagedListBuilder import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.session.room.timeline.* -import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.api.session.room.timeline.Timeline +import im.vector.matrix.android.api.session.room.timeline.TimelineEventInterceptor +import im.vector.matrix.android.api.session.room.timeline.TimelineService 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.EventEntityFields import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith -import im.vector.matrix.android.internal.util.LiveDataUtils import im.vector.matrix.android.internal.util.PagingRequestHelper import im.vector.matrix.android.internal.util.tryTransactionAsync import io.realm.Realm @@ -43,42 +40,16 @@ private const val EVENT_NOT_FOUND_INDEX = -1 internal class DefaultTimelineService(private val roomId: String, private val monarchy: Monarchy, private val taskExecutor: TaskExecutor, - private val boundaryCallback: TimelineBoundaryCallback, private val contextOfEventTask: GetContextOfEventTask, - private val roomMemberExtractor: RoomMemberExtractor + private val timelineEventFactory: TimelineEventFactory, + private val paginationTask: PaginationTask, + private val helper: PagingRequestHelper ) : TimelineService { private val eventInterceptors = ArrayList() - override fun timeline(eventId: String?): LiveData { - clearUnlinkedEvents() - val initialLoadKey = getInitialLoadKey(eventId) - val realmDataSourceFactory = monarchy.createDataSourceFactory { - buildDataSourceFactoryQuery(it, eventId) - } - val domainSourceFactory = realmDataSourceFactory - .map { eventEntity -> - val roomMember = roomMemberExtractor.extractFrom(eventEntity) - TimelineEvent(eventEntity.asDomain(), eventEntity.localId, roomMember) - } - - val pagedListConfig = buildPagedListConfig() - - val livePagedListBuilder = LivePagedListBuilder(domainSourceFactory, pagedListConfig) - .setBoundaryCallback(boundaryCallback) - .setInitialLoadKey(initialLoadKey) - - val eventsLiveData = monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder) - - return LiveDataUtils.combine(eventsLiveData, boundaryCallback.status) { events, status -> - val isLoadingForward = status.before == PagingRequestHelper.Status.RUNNING - val isLoadingBackward = status.after == PagingRequestHelper.Status.RUNNING - TimelineData(events, isLoadingForward, isLoadingBackward) - } - } - override fun createTimeline(eventId: String?): Timeline { - return DefaultTimeline(roomId, eventId, monarchy, taskExecutor, boundaryCallback, contextOfEventTask, roomMemberExtractor) + return DefaultTimeline(roomId, eventId, monarchy.realmConfiguration, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask, helper) } // PRIVATE FUNCTIONS *************************************************************************** @@ -124,7 +95,8 @@ internal class DefaultTimelineService(private val roomId: String, private fun indexOfEvent(eventId: String): Int { var displayIndex = EVENT_NOT_FOUND_INDEX monarchy.doWithRealm { - displayIndex = EventEntity.where(it, eventId = eventId).findFirst()?.displayIndex ?: EVENT_NOT_FOUND_INDEX + displayIndex = EventEntity.where(it, eventId = eventId).findFirst()?.displayIndex + ?: EVENT_NOT_FOUND_INDEX } return displayIndex } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt new file mode 100644 index 00000000..d748fe7f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.timeline + +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor + +internal class TimelineEventFactory(private val roomMemberExtractor: RoomMemberExtractor) { + + fun create(eventEntity: EventEntity): TimelineEvent { + val roomMember = roomMemberExtractor.extractFrom(eventEntity) + return TimelineEvent( + eventEntity.asDomain(), + eventEntity.localId, + eventEntity.displayIndex, + roomMember + ) + } + +} \ 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 bb74f2cc..e728a140 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 @@ -18,7 +18,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 @@ -26,6 +31,7 @@ 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) { @@ -40,7 +46,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") + ?: realm.createObject(roomId) val nextToken: String? val prevToken: String? @@ -60,10 +66,10 @@ 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) } currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked()) From 0c76178bee7e86adba73f114a2a2127e460519c3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 15 Mar 2019 19:28:13 +0100 Subject: [PATCH 03/12] Markwon : update method name --- .../features/html/EventHtmlRenderer.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/im/vector/riotredesign/features/html/EventHtmlRenderer.kt b/app/src/main/java/im/vector/riotredesign/features/html/EventHtmlRenderer.kt index 2ecee795..15843d8a 100644 --- a/app/src/main/java/im/vector/riotredesign/features/html/EventHtmlRenderer.kt +++ b/app/src/main/java/im/vector/riotredesign/features/html/EventHtmlRenderer.kt @@ -73,40 +73,40 @@ private class MatrixPlugin private constructor(private val glideRequests: GlideR override fun configureHtmlRenderer(builder: MarkwonHtmlRenderer.Builder) { builder - .addHandler( + .setHandler( "img", ImageHandler.create()) - .addHandler( + .setHandler( "a", MxLinkHandler(glideRequests, context, session)) - .addHandler( + .setHandler( "blockquote", BlockquoteHandler()) - .addHandler( + .setHandler( "sub", SubScriptHandler()) - .addHandler( + .setHandler( "sup", SuperScriptHandler()) - .addHandler( + .setHandler( asList("b", "strong"), StrongEmphasisHandler()) - .addHandler( + .setHandler( asList("s", "del"), StrikeHandler()) - .addHandler( + .setHandler( asList("u", "ins"), UnderlineHandler()) - .addHandler( + .setHandler( asList("ul", "ol"), ListHandler()) - .addHandler( + .setHandler( asList("i", "em", "cite", "dfn"), EmphasisHandler()) - .addHandler( + .setHandler( asList("h1", "h2", "h3", "h4", "h5", "h6"), HeadingHandler()) - .addHandler("mx-reply", + .setHandler("mx-reply", MxReplyTagHandler()) } From 2898eae566a5425ea602e0ee7a6de5c453955a81 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 19 Mar 2019 19:45:32 +0100 Subject: [PATCH 04/12] Timeline : reactivate loaders and get off the main thread --- app/build.gradle | 5 +- .../main/java/im/vector/riotredesign/Riot.kt | 4 + .../home/room/detail/RoomDetailActions.kt | 3 +- .../home/room/detail/RoomDetailFragment.kt | 12 +- .../home/room/detail/RoomDetailViewModel.kt | 11 +- .../timeline/TimelineEventController.kt | 51 +++-- .../EndlessRecyclerViewScrollListener.java | 53 +---- .../timeline/helper/TimelineAsyncHelper.kt | 47 +++++ matrix-sdk-android/build.gradle | 3 - .../session/room/timeline/ChunkEntityTest.kt | 6 +- .../timeline/FakeGetContextOfEventTask.kt | 3 +- .../room/timeline/FakePaginationTask.kt | 4 +- .../session/room/timeline/RoomDataHelper.kt | 2 +- .../api/session/room/timeline/Timeline.kt | 10 +- .../matrix/android/api/util/CancelableBag.kt | 35 ++++ .../database/RealmLiveEntityObserver.kt | 11 +- .../database/helper/ChunkEntityHelper.kt | 27 +-- .../internal/database/model/ChunkEntity.kt | 5 +- .../database/query/ChunkEntityQueries.kt | 2 +- .../timeline/DefaultGetContextOfEventTask.kt | 8 +- .../room/timeline/DefaultPaginationTask.kt | 4 +- .../session/room/timeline/DefaultTimeline.kt | 192 +++++++++++++----- .../room/timeline/DefaultTimelineService.kt | 32 --- .../room/timeline/TimelineBoundaryCallback.kt | 4 +- .../room/timeline/TokenChunkEventPersistor.kt | 55 +++-- .../internal/session/sync/RoomSyncHandler.kt | 5 +- 26 files changed, 366 insertions(+), 228 deletions(-) create mode 100644 app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineAsyncHelper.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/CancelableBag.kt diff --git a/app/build.gradle b/app/build.gradle index 570cf611..762fafde 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -58,7 +58,7 @@ android { dependencies { - def epoxy_version = "3.0.0" + def epoxy_version = "3.3.0" def arrow_version = "0.8.2" def coroutines_version = "1.0.1" def markwon_version = '3.0.0-SNAPSHOT' @@ -77,9 +77,6 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.core:core-ktx:1.0.1' - // Paging - implementation 'androidx.paging:paging-runtime:2.0.0' - implementation 'com.jakewharton.threetenabp:threetenabp:1.1.1' implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.facebook.stetho:stetho:1.5.0' diff --git a/app/src/main/java/im/vector/riotredesign/Riot.kt b/app/src/main/java/im/vector/riotredesign/Riot.kt index 9332eec7..fc0f83eb 100644 --- a/app/src/main/java/im/vector/riotredesign/Riot.kt +++ b/app/src/main/java/im/vector/riotredesign/Riot.kt @@ -19,6 +19,8 @@ package im.vector.riotredesign import android.app.Application import android.content.Context import androidx.multidex.MultiDex +import com.airbnb.epoxy.EpoxyAsyncUtil +import com.airbnb.epoxy.EpoxyController import com.facebook.stetho.Stetho import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.loader.glide.GlideImageLoader @@ -41,6 +43,8 @@ class Riot : Application() { } AndroidThreeTen.init(this) BigImageViewer.initialize(GlideImageLoader.with(applicationContext)) + EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() + EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() val appModule = AppModule(applicationContext).definition val homeModule = HomeModule().definition startKoin(listOf(appModule, homeModule), logger = EmptyLogger()) diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt index 47fd8ed2..f7a59f2d 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt @@ -16,6 +16,7 @@ package im.vector.riotredesign.features.home.room.detail +import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent sealed class RoomDetailActions { @@ -23,6 +24,6 @@ sealed class RoomDetailActions { data class SendMessage(val text: String) : RoomDetailActions() object IsDisplayed : RoomDetailActions() data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions() - object LoadMore: RoomDetailActions() + data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions() } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index ab128909..7636cce4 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -25,6 +25,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.mvrx.fragmentViewModel +import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.R import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer @@ -110,9 +111,14 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { it.dispatchTo(stateRestorer) it.dispatchTo(scrollOnNewMessageCallback) } - recyclerView.addOnScrollListener(object : EndlessRecyclerViewScrollListener(layoutManager, EndlessRecyclerViewScrollListener.LoadOnScrollDirection.BOTTOM) { - override fun onLoadMore(page: Int, totalItemsCount: Int) { - roomDetailViewModel.process(RoomDetailActions.LoadMore) + recyclerView.addOnScrollListener(object : EndlessRecyclerViewScrollListener(layoutManager, Timeline.Direction.BACKWARDS) { + override fun onLoadMore() { + roomDetailViewModel.process(RoomDetailActions.LoadMore(Timeline.Direction.BACKWARDS)) + } + }) + recyclerView.addOnScrollListener(object : EndlessRecyclerViewScrollListener(layoutManager, Timeline.Direction.FORWARDS) { + override fun onLoadMore() { + roomDetailViewModel.process(RoomDetailActions.LoadMore(Timeline.Direction.FORWARDS)) } }) recyclerView.setController(timelineEventController) diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index e1e80bcb..99e5b7b9 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -22,12 +22,12 @@ import com.jakewharton.rxrelay2.BehaviorRelay import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.rx.rx import im.vector.riotredesign.core.platform.RiotViewModel import im.vector.riotredesign.features.home.room.VisibleRoomStore import io.reactivex.rxkotlin.subscribeBy import org.koin.android.ext.android.get +import timber.log.Timber import java.util.concurrent.TimeUnit class RoomDetailViewModel(initialState: RoomDetailViewState, @@ -64,7 +64,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, is RoomDetailActions.SendMessage -> handleSendMessage(action) is RoomDetailActions.IsDisplayed -> handleIsDisplayed() is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action) - is RoomDetailActions.LoadMore -> timeline.paginate(Timeline.Direction.BACKWARDS, 50) + is RoomDetailActions.LoadMore -> handleLoadMore(action) } } @@ -82,6 +82,10 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, visibleRoomHolder.post(roomId) } + private fun handleLoadMore(action: RoomDetailActions.LoadMore) { + timeline.paginate(action.direction, 50) + } + private fun observeDisplayedEvents() { // We are buffering scroll events for one second // and keep the most recent one to set the read receipt on. @@ -100,13 +104,14 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, private fun observeRoomSummary() { room.rx().liveRoomSummary() .execute { async -> + Timber.v("Room summary updated: $async") copy(asyncRoomSummary = async) } } override fun onCleared() { - super.onCleared() timeline.dispose() + super.onCleared() } } \ No newline at end of file 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 55bd6e0c..5516ea13 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 @@ -16,19 +16,21 @@ package im.vector.riotredesign.features.home.room.detail.timeline +import android.os.Handler import android.view.View import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.RecyclerView -import com.airbnb.epoxy.EpoxyAsyncUtil import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.VisibilityState import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.riotredesign.core.epoxy.LoadingItemModel_ import im.vector.riotredesign.core.epoxy.RiotEpoxyModel import im.vector.riotredesign.core.extensions.localDateTime import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory +import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineAsyncHelper import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider @@ -37,20 +39,16 @@ import im.vector.riotredesign.features.media.MediaContentRenderer class TimelineEventController(private val dateFormatter: TimelineDateFormatter, private val timelineItemFactory: TimelineItemFactory, - private val timelineMediaSizeProvider: TimelineMediaSizeProvider -) : EpoxyController( - EpoxyAsyncUtil.getAsyncBackgroundHandler(), - EpoxyAsyncUtil.getAsyncBackgroundHandler() -), Timeline.Listener { + private val timelineMediaSizeProvider: TimelineMediaSizeProvider, + private val backgroundHandler: Handler = TimelineAsyncHelper.getBackgroundHandler() +) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { private val modelCache = arrayListOf>>() private var currentSnapshot: List = emptyList() - override fun onUpdated(snapshot: List) { - submitSnapshot(snapshot) - } - private val listUpdateCallback = object : ListUpdateCallback { + + @Synchronized override fun onChanged(position: Int, count: Int, payload: Any?) { (position until (position + count)).forEach { modelCache[it] = emptyList() @@ -62,7 +60,8 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, //no-op } - override fun onInserted(position: Int, count: Int) { + @Synchronized + override fun onInserted(position: Int, count: Int) = synchronized(modelCache) { if (modelCache.isNotEmpty() && position == modelCache.size) { modelCache[position - 1] = emptyList() } @@ -78,10 +77,6 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, } - private var isLoadingForward: Boolean = false - private var isLoadingBackward: Boolean = false - private var hasReachedEnd: Boolean = true - private var timeline: Timeline? = null var callback: Callback? = null @@ -89,7 +84,6 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, if (this.timeline != timeline) { this.timeline = timeline this.timeline?.listener = this - submitSnapshot(timeline?.snapshot() ?: emptyList()) } } @@ -99,9 +93,25 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, } override fun buildModels() { + LoadingItemModel_() + .id("forward_loading_item") + .addWhen(Timeline.Direction.FORWARDS) + add(getModels()) + + LoadingItemModel_() + .id("backward_loading_item") + .addWhen(Timeline.Direction.BACKWARDS) } + private fun LoadingItemModel_.addWhen(direction: Timeline.Direction) { + val shouldAdd = timeline?.let { + it.hasMoreToLoad(direction) || !it.hasReachedEnd(direction) + } ?: false + addIf(shouldAdd, this@TimelineEventController) + } + + @Synchronized private fun getModels(): List> { (0 until modelCache.size).forEach { position -> if (modelCache[position].isEmpty()) { @@ -133,8 +143,14 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, return epoxyModels } + // Timeline.LISTENER *************************************************************************** + + override fun onUpdated(snapshot: List) { + submitSnapshot(snapshot) + } + private fun submitSnapshot(newSnapshot: List) { - EpoxyAsyncUtil.getAsyncBackgroundHandler().post { + backgroundHandler.post { val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot) currentSnapshot = newSnapshot val diffResult = DiffUtil.calculateDiff(diffCallback) @@ -142,6 +158,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, } } + interface Callback { fun onEventVisible(event: TimelineEvent) fun onUrlClicked(url: String) diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.java b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.java index 5c1c11b2..e77ed44f 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.java +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.java @@ -19,26 +19,24 @@ package im.vector.riotredesign.features.home.room.detail.timeline.helper; import androidx.annotation.NonNull; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import im.vector.matrix.android.api.session.room.timeline.Timeline; -// Todo rework that, it has been copy/paste at the moment public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnScrollListener { // Sets the starting page index private static final int startingPageIndex = 0; // The minimum amount of items to have below your current scroll position // before loading more. - private int visibleThreshold = 30; - // The current offset index of data you have loaded - private int currentPage = 0; + private int visibleThreshold = 50; // The total number of items in the dataset after the last load private int previousTotalItemCount = 0; // True if we are still waiting for the last set of data to load. private boolean loading = true; private LinearLayoutManager mLayoutManager; - private LoadOnScrollDirection mDirection; + private Timeline.Direction mDirection; - public EndlessRecyclerViewScrollListener(LinearLayoutManager layoutManager, LoadOnScrollDirection direction) { + public EndlessRecyclerViewScrollListener(LinearLayoutManager layoutManager, Timeline.Direction direction) { this.mLayoutManager = layoutManager; - mDirection = direction; + this.mDirection = direction; } @@ -55,11 +53,10 @@ public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnS firstVisibleItemPosition = mLayoutManager.findFirstVisibleItemPosition(); switch (mDirection) { - case BOTTOM: + case BACKWARDS: // If the total item count is zero and the previous isn't, assume the // list is invalidated and should be reset back to initial state if (totalItemCount < previousTotalItemCount) { - this.currentPage = startingPageIndex; this.previousTotalItemCount = totalItemCount; if (totalItemCount == 0) { this.loading = true; @@ -78,16 +75,14 @@ public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnS // If we do need to reload some more data, we execute onLoadMore to fetch the data. // threshold should reflect how many total columns there are too if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) { - currentPage++; - onLoadMore(currentPage, totalItemCount); + onLoadMore(); loading = true; } break; - case TOP: + case FORWARDS: // If the total item count is zero and the previous isn't, assume the // list is invalidated and should be reset back to initial state if (totalItemCount < previousTotalItemCount) { - this.currentPage = startingPageIndex; this.previousTotalItemCount = totalItemCount; if (totalItemCount == 0) { this.loading = true; @@ -106,42 +101,14 @@ public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnS // If we do need to reload some more data, we execute onLoadMore to fetch the data. // threshold should reflect how many total columns there are too if (!loading && firstVisibleItemPosition < visibleThreshold) { - currentPage++; - onLoadMore(currentPage, totalItemCount); + onLoadMore(); loading = true; } break; } } - private int getLastVisibleItem(int[] lastVisibleItemPositions) { - int maxSize = 0; - for (int i = 0; i < lastVisibleItemPositions.length; i++) { - if (i == 0) { - maxSize = lastVisibleItemPositions[i]; - } else if (lastVisibleItemPositions[i] > maxSize) { - maxSize = lastVisibleItemPositions[i]; - } - } - return maxSize; - } - - private int getFirstVisibleItem(int[] firstVisibleItemPositions) { - int maxSize = 0; - for (int i = 0; i < firstVisibleItemPositions.length; i++) { - if (i == 0) { - maxSize = firstVisibleItemPositions[i]; - } else if (firstVisibleItemPositions[i] > maxSize) { - maxSize = firstVisibleItemPositions[i]; - } - } - return maxSize; - } - // Defines the process for actually loading more data based on page - public abstract void onLoadMore(int page, int totalItemsCount); + public abstract void onLoadMore(); - public enum LoadOnScrollDirection { - TOP, BOTTOM - } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineAsyncHelper.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineAsyncHelper.kt new file mode 100644 index 00000000..f721413b --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineAsyncHelper.kt @@ -0,0 +1,47 @@ +/* + * + * * Copyright 2019 New Vector Ltd + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package im.vector.riotredesign.features.home.room.detail.timeline.helper + +import android.os.Handler +import android.os.HandlerThread + +private const val THREAD_NAME = "Timeline_Building_Thread" + +object TimelineAsyncHelper { + + private var backgroundHandlerThread: HandlerThread? = null + private var backgroundHandler: Handler? = null + + fun getBackgroundHandler(): Handler { + if (backgroundHandler != null) { + backgroundHandler?.removeCallbacksAndMessages(null) + } + if (backgroundHandlerThread != null) { + backgroundHandlerThread?.quit() + } + val handlerThread = HandlerThread(THREAD_NAME) + .also { + backgroundHandlerThread = it + it.start() + } + val looper = handlerThread.looper + return Handler(looper).also { backgroundHandler = it } + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 5d87130f..710be923 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -76,9 +76,6 @@ dependencies { implementation 'com.github.Zhuinden:realm-monarchy:0.5.1' kapt 'dk.ilios:realmfieldnameshelper:1.1.1' - // Paging - implementation 'androidx.paging:paging-runtime:2.0.0' - // Work implementation "android.arch.work:work-runtime-ktx:1.0.0-beta02" 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 62e5b23d..397dcfc8 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 @@ -134,13 +134,13 @@ internal class ChunkEntityTest : InstrumentedTest { val chunk2: ChunkEntity = realm.createObject() val eventsForChunk1 = createFakeListOfEvents(30) val eventsForChunk2 = eventsForChunk1 + createFakeListOfEvents(10) - chunk1.isLast = true - chunk2.isLast = false + chunk1.isLastForward = true + chunk2.isLastForward = false chunk1.addAll("roomId", eventsForChunk1, PaginationDirection.FORWARDS) chunk2.addAll("roomId", eventsForChunk2, PaginationDirection.BACKWARDS) chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) chunk1.events.size shouldEqual 40 - chunk1.isLast.shouldBeTrue() + chunk1.isLastForward.shouldBeTrue() } } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/FakeGetContextOfEventTask.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/FakeGetContextOfEventTask.kt index c6c6400a..cdd0bdcd 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/FakeGetContextOfEventTask.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/FakeGetContextOfEventTask.kt @@ -25,7 +25,7 @@ import kotlin.random.Random internal class FakeGetContextOfEventTask(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : GetContextOfEventTask { - override fun execute(params: GetContextOfEventTask.Params): Try { + override fun execute(params: GetContextOfEventTask.Params): Try { val fakeEvents = RoomDataHelper.createFakeListOfEvents(30) val tokenChunkEvent = FakeTokenChunkEvent( Random.nextLong(System.currentTimeMillis()).toString(), @@ -33,7 +33,6 @@ internal class FakeGetContextOfEventTask(private val tokenChunkEventPersistor: T fakeEvents ) return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, PaginationDirection.BACKWARDS) - .map { tokenChunkEvent } } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/FakePaginationTask.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/FakePaginationTask.kt index fe42fea3..3a0e72a0 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/FakePaginationTask.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/FakePaginationTask.kt @@ -18,17 +18,15 @@ package im.vector.matrix.android.session.room.timeline import arrow.core.Try import im.vector.matrix.android.internal.session.room.timeline.PaginationTask -import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEvent import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor import kotlin.random.Random internal class FakePaginationTask(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : PaginationTask { - override fun execute(params: PaginationTask.Params): Try { + override fun execute(params: PaginationTask.Params): Try { val fakeEvents = RoomDataHelper.createFakeListOfEvents(30) val tokenChunkEvent = FakeTokenChunkEvent(params.from, Random.nextLong(System.currentTimeMillis()).toString(), fakeEvents) return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, params.direction) - .map { tokenChunkEvent } } } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt index 1b0c819f..35e9cb12 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt @@ -48,7 +48,7 @@ object RoomDataHelper { val chunkEntity = realm.createObject().apply { nextToken = null prevToken = Random.nextLong(System.currentTimeMillis()).toString() - isLast = true + isLastForward = true } chunkEntity.addAll("roomId", eventList, PaginationDirection.FORWARDS) roomEntity.addOrUpdate(chunkEntity) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index 9e30d452..1a09c591 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -22,6 +22,8 @@ interface Timeline { var listener: Timeline.Listener? + fun hasMoreToLoad(direction: Direction): Boolean + fun hasReachedEnd(direction: Direction): Boolean fun size(): Int fun snapshot(): List fun paginate(direction: Direction, count: Int) @@ -44,14 +46,6 @@ interface Timeline { * These events come from a back pagination. */ BACKWARDS("b"); - - fun reversed(): Direction { - return when (this) { - FORWARDS -> BACKWARDS - BACKWARDS -> FORWARDS - } - } - } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/CancelableBag.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/CancelableBag.kt new file mode 100644 index 00000000..95fdc3de --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/CancelableBag.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.util + +class CancelableBag : Cancelable { + + private val cancelableList = ArrayList() + + fun add(cancelable: Cancelable) { + cancelableList.add(cancelable) + } + + override fun cancel() { + cancelableList.forEach { it.cancel() } + } + +} + +fun Cancelable.addTo(cancelables: CancelableBag) { + cancelables.add(this) +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt index 490742ca..9f31aa2c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt @@ -43,7 +43,6 @@ internal abstract class RealmLiveEntityObserver(protected val m queryResults.addChangeListener { t, changeSet -> onChanged(t, changeSet) } - processInitialResults(queryResults) results = AtomicReference(queryResults) } } @@ -61,7 +60,7 @@ internal abstract class RealmLiveEntityObserver(protected val m return isStarted.get() } - protected open fun onChanged(realmResults: RealmResults, changeSet: OrderedCollectionChangeSet) { + private fun onChanged(realmResults: RealmResults, changeSet: OrderedCollectionChangeSet) { val insertionIndexes = changeSet.insertions val updateIndexes = changeSet.changes val deletionIndexes = changeSet.deletions @@ -71,12 +70,6 @@ internal abstract class RealmLiveEntityObserver(protected val m processChanges(inserted, updated, deleted) } - protected open fun processInitialResults(results: RealmResults) { - // no-op - } - - protected open fun processChanges(inserted: List, updated: List, deleted: List) { - //no-op - } + protected abstract fun processChanges(inserted: List, updated: List, deleted: List) } \ 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 b1883d3f..ed80750d 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 @@ -27,18 +27,18 @@ import im.vector.matrix.android.internal.database.query.fastContains import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import io.realm.Sort -internal fun ChunkEntity.deleteOnCascade() { - assertIsManaged() - this.events.deleteAllFromRealm() - this.deleteFromRealm() -} - // By default if a chunk is empty we consider it unlinked internal fun ChunkEntity.isUnlinked(): Boolean { assertIsManaged() return events.where().equalTo(EventEntityFields.IS_UNLINKED, false).findAll().isEmpty() } +internal fun ChunkEntity.deleteOnCascade() { + assertIsManaged() + this.events.deleteAllFromRealm() + this.deleteFromRealm() +} + internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, direction: PaginationDirection) { @@ -53,10 +53,11 @@ internal fun ChunkEntity.merge(roomId: String, val eventsToMerge: List if (direction == PaginationDirection.FORWARDS) { this.nextToken = chunkToMerge.nextToken - this.isLast = chunkToMerge.isLast + this.isLastForward = chunkToMerge.isLastForward eventsToMerge = chunkToMerge.events.reversed() } else { this.prevToken = chunkToMerge.prevToken + this.isLastBackward = chunkToMerge.isLastBackward eventsToMerge = chunkToMerge.events } eventsToMerge.forEach { @@ -117,14 +118,14 @@ private fun ChunkEntity.assertIsManaged() { internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findFirst()?.displayIndex - PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING).findFirst()?.displayIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findFirst()?.displayIndex + PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING).findFirst()?.displayIndex + } ?: defaultValue } 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/ChunkEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt index da730135..1c7c755a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt @@ -23,8 +23,9 @@ import io.realm.annotations.LinkingObjects internal open class ChunkEntity(var prevToken: String? = null, var nextToken: String? = null, - var isLast: Boolean = false, - var events: RealmList = RealmList() + var events: RealmList = RealmList(), + var isLastForward: Boolean = false, + var isLastBackward: Boolean = false ) : RealmObject() { @LinkingObjects("chunks") 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 74664346..bfaac609 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 @@ -43,7 +43,7 @@ internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken: internal fun ChunkEntity.Companion.findLastLiveChunkFromRoom(realm: Realm, roomId: String): ChunkEntity? { return where(realm, roomId) - .equalTo(ChunkEntityFields.IS_LAST, true) + .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true) .findFirst() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt index 820b034f..efe7509a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt @@ -17,12 +17,12 @@ package im.vector.matrix.android.internal.session.room.timeline import arrow.core.Try -import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.FilterUtil -internal interface GetContextOfEventTask : Task { +internal interface GetContextOfEventTask : Task { data class Params( val roomId: String, @@ -35,12 +35,12 @@ internal class DefaultGetContextOfEventTask(private val roomAPI: RoomAPI, private val tokenChunkEventPersistor: TokenChunkEventPersistor ) : GetContextOfEventTask { - override fun execute(params: GetContextOfEventTask.Params): Try { + override fun execute(params: GetContextOfEventTask.Params): Try { val filter = FilterUtil.createRoomEventFilter(true)?.toJSONString() return executeRequest { apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter) }.flatMap { response -> - tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS).map { response } + tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt index 1caf18b1..9bebcf7d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt @@ -23,7 +23,7 @@ import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.FilterUtil -internal interface PaginationTask : Task { +internal interface PaginationTask : Task { data class Params( val roomId: String, @@ -38,7 +38,7 @@ internal class DefaultPaginationTask(private val roomAPI: RoomAPI, private val tokenChunkEventPersistor: TokenChunkEventPersistor ) : PaginationTask { - override fun execute(params: PaginationTask.Params): Try { + override fun execute(params: PaginationTask.Params): Try { val filter = FilterUtil.createRoomEventFilter(true)?.toJSONString() return executeRequest { apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index eca061db..b85d8280 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -18,10 +18,15 @@ package im.vector.matrix.android.internal.session.room.timeline -import androidx.annotation.UiThread +import android.os.Handler +import android.os.HandlerThread import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.api.util.CancelableBag +import im.vector.matrix.android.api.util.addTo +import im.vector.matrix.android.internal.database.model.ChunkEntity 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.EventEntityFields @@ -29,18 +34,16 @@ import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.PagingRequestHelper -import io.realm.OrderedCollectionChangeSet -import io.realm.OrderedRealmCollectionChangeListener -import io.realm.Realm -import io.realm.RealmConfiguration -import io.realm.RealmQuery -import io.realm.RealmResults -import io.realm.Sort +import io.realm.* import java.util.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference import kotlin.collections.ArrayList -private const val INITIAL_LOAD_SIZE = 30 +private const val INITIAL_LOAD_SIZE = 20 +private const val THREAD_NAME = "TIMELINE_DB_THREAD" internal class DefaultTimeline( private val roomId: String, @@ -54,16 +57,24 @@ internal class DefaultTimeline( ) : Timeline { override var listener: Timeline.Listener? = null + set(value) { + field = value + listener?.onUpdated(snapshot()) + } - private lateinit var realm: Realm + private val isStarted = AtomicBoolean(false) + private val handlerThread = AtomicReference() + private val handler = AtomicReference() + private val realm = AtomicReference() + + private val cancelableBag = CancelableBag() private lateinit var liveEvents: RealmResults private var prevDisplayIndex: Int = 0 private var nextDisplayIndex: Int = 0 private val isLive = initialEventId == null private val builtEvents = Collections.synchronizedList(ArrayList()) - - private val changeListener = OrderedRealmCollectionChangeListener> { _, changeSet -> + private val eventsChangeListener = OrderedRealmCollectionChangeListener> { _, changeSet -> if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) { handleInitialLoad() } else { @@ -78,32 +89,48 @@ internal class DefaultTimeline( } } - @UiThread override fun paginate(direction: Timeline.Direction, count: Int) { - if (direction == Timeline.Direction.FORWARDS && isLive) { - return - } - val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex - val hasBuiltCountItems = insertFromLiveResults(startDisplayIndex, direction, count.toLong()) - if (hasBuiltCountItems.not()) { - val token = getToken(direction) ?: return - helper.runIfNotRunning(direction.toRequestType()) { - executePaginationTask(it, token, direction.toPaginationDirection(), 30) + handler.get()?.post { + if (!hasMoreToLoadLive(direction) && hasReachedEndLive(direction)) { + return@post + } + val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex + val builtCountItems = insertFromLiveResults(startDisplayIndex, direction, count.toLong()) + if (builtCountItems < count) { + val limit = count - builtCountItems + val token = getTokenLive(direction) ?: return@post + helper.runIfNotRunning(direction.toRequestType()) { executePaginationTask(it, token, direction, limit) } } } } - @UiThread override fun start() { - realm = Realm.getInstance(realmConfiguration) - liveEvents = buildQuery(initialEventId).findAllAsync() - liveEvents.addChangeListener(changeListener) + if (isStarted.compareAndSet(false, true)) { + val handlerThread = HandlerThread(THREAD_NAME) + handlerThread.start() + val handler = Handler(handlerThread.looper) + this.handlerThread.set(handlerThread) + this.handler.set(handler) + handler.post { + val realm = Realm.getInstance(realmConfiguration) + this.realm.set(realm) + liveEvents = buildEventQuery(realm).findAllAsync() + liveEvents.addChangeListener(eventsChangeListener) + } + } + } - @UiThread override fun dispose() { - liveEvents.removeAllChangeListeners() - realm.close() + if (isStarted.compareAndSet(true, false)) { + handler.get()?.post { + cancelableBag.cancel() + liveEvents.removeAllChangeListeners() + realm.getAndSet(null)?.close() + handler.set(null) + handlerThread.getAndSet(null)?.quit() + } + } } override fun snapshot(): List = synchronized(builtEvents) { @@ -114,6 +141,21 @@ internal class DefaultTimeline( return builtEvents.size } + override fun hasReachedEnd(direction: Timeline.Direction): Boolean { + return handler.get()?.postAndWait { + hasReachedEndLive(direction) + } ?: false + } + + override fun hasMoreToLoad(direction: Timeline.Direction): Boolean { + return handler.get()?.postAndWait { + hasMoreToLoadLive(direction) + } ?: false + } + + /** + * This has to be called on TimelineThread as it access realm live results + */ private fun handleInitialLoad() = synchronized(builtEvents) { val initialDisplayIndex = if (isLive) { liveEvents.firstOrNull()?.displayIndex @@ -138,19 +180,22 @@ internal class DefaultTimeline( private fun executePaginationTask(requestCallback: PagingRequestHelper.Request.Callback, from: String, - direction: PaginationDirection, + direction: Timeline.Direction, limit: Int) { val params = PaginationTask.Params(roomId = roomId, - from = from, - direction = direction, - limit = limit) + from = from, + direction = direction.toPaginationDirection(), + limit = limit) paginationTask.configureWith(params) .enableRetry() - .dispatchTo(object : MatrixCallback { - override fun onSuccess(data: Boolean) { + .dispatchTo(object : MatrixCallback { + override fun onSuccess(data: TokenChunkEventPersistor.Result) { requestCallback.recordSuccess() + if (data == TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE) { + paginate(direction, limit) + } } override fun onFailure(failure: Throwable) { @@ -158,26 +203,63 @@ internal class DefaultTimeline( } }) .executeBy(taskExecutor) + .addTo(cancelableBag) } - private fun getToken(direction: Timeline.Direction): String? { - val chunkEntity = liveEvents.firstOrNull()?.chunk?.firstOrNull() ?: return null + /** + * This has to be called on TimelineThread as it access realm live results + */ + private fun getTokenLive(direction: Timeline.Direction): String? { + val chunkEntity = getLiveChunk() ?: return null return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken } /** - * This has to be called on MonarchyThread as it access realm live results - * @return true if count items has been added + * This has to be called on TimelineThread as it access realm live results + */ + private fun hasReachedEndLive(direction: Timeline.Direction): Boolean { + val liveChunk = getLiveChunk() ?: return false + return if (direction == Timeline.Direction.FORWARDS) { + liveChunk.isLastForward + } else { + liveChunk.isLastBackward || liveEvents.lastOrNull()?.type == EventType.STATE_ROOM_CREATE + } + } + + /** + * This has to be called on TimelineThread as it access realm live results + */ + private fun hasMoreToLoadLive(direction: Timeline.Direction): Boolean { + if (liveEvents.isEmpty()) { + return true + } + return if (direction == Timeline.Direction.FORWARDS) { + builtEvents.firstOrNull()?.displayIndex != liveEvents.firstOrNull()?.displayIndex + } else { + builtEvents.lastOrNull()?.displayIndex != liveEvents.lastOrNull()?.displayIndex + } + } + + /** + * This has to be called on TimelineThread as it access realm live results + */ + private fun getLiveChunk(): ChunkEntity? { + return liveEvents.firstOrNull()?.chunk?.firstOrNull() + } + + /** + * This has to be called on TimelineThread as it access realm live results + * @return number of items who have been added */ private fun insertFromLiveResults(startDisplayIndex: Int, direction: Timeline.Direction, - count: Long): Boolean = synchronized(builtEvents) { + count: Long): Int = synchronized(builtEvents) { if (count < 1) { throw java.lang.IllegalStateException("You should provide a count superior to 0") } val offsetResults = getOffsetResults(startDisplayIndex, direction, count) if (offsetResults.isEmpty()) { - return false + return 0 } val offsetIndex = offsetResults.last()!!.displayIndex if (direction == Timeline.Direction.BACKWARDS) { @@ -191,9 +273,12 @@ internal class DefaultTimeline( builtEvents.add(position, timelineEvent) } listener?.onUpdated(snapshot()) - return offsetResults.size.toLong() == count + return offsetResults.size } + /** + * This has to be called on TimelineThread as it access realm live results + */ private fun getOffsetResults(startDisplayIndex: Int, direction: Timeline.Direction, count: Long): RealmResults { @@ -210,21 +295,33 @@ internal class DefaultTimeline( return offsetQuery.limit(count).findAll() } - private fun buildQuery(eventId: String?): RealmQuery { - val query = if (eventId == null) { + private fun buildEventQuery(realm: Realm): RealmQuery { + val query = if (initialEventId == null) { EventEntity .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY) - .equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST}", true) + .equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST_FORWARD}", true) } else { EventEntity .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH) - .`in`("${EventEntityFields.CHUNK}.${ChunkEntityFields.EVENTS.EVENT_ID}", arrayOf(eventId)) + .`in`("${EventEntityFields.CHUNK}.${ChunkEntityFields.EVENTS.EVENT_ID}", arrayOf(initialEventId)) } query.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) return query } + private fun Handler.postAndWait(runnable: () -> T): T { + val lock = CountDownLatch(1) + val atomicReference = AtomicReference() + post { + val result = runnable() + atomicReference.set(result) + lock.countDown() + } + lock.await() + return atomicReference.get() + } + private fun Timeline.Direction.toRequestType(): PagingRequestHelper.RequestType { return if (this == Timeline.Direction.BACKWARDS) PagingRequestHelper.RequestType.BEFORE else PagingRequestHelper.RequestType.AFTER } @@ -233,4 +330,5 @@ internal class DefaultTimeline( private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS } -} \ No newline at end of file + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index 5fa891f4..f112f078 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -16,12 +16,9 @@ package im.vector.matrix.android.internal.session.room.timeline -import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.timeline.Timeline -import im.vector.matrix.android.api.session.room.timeline.TimelineEventInterceptor import im.vector.matrix.android.api.session.room.timeline.TimelineService -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.EventEntityFields import im.vector.matrix.android.internal.database.query.where @@ -29,12 +26,7 @@ import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.PagingRequestHelper import im.vector.matrix.android.internal.util.tryTransactionAsync -import io.realm.Realm -import io.realm.RealmQuery -import io.realm.Sort -private const val PAGE_SIZE = 100 -private const val PREFETCH_DISTANCE = 30 private const val EVENT_NOT_FOUND_INDEX = -1 internal class DefaultTimelineService(private val roomId: String, @@ -46,8 +38,6 @@ internal class DefaultTimelineService(private val roomId: String, private val helper: PagingRequestHelper ) : TimelineService { - private val eventInterceptors = ArrayList() - override fun createTimeline(eventId: String?): Timeline { return DefaultTimeline(roomId, eventId, monarchy.realmConfiguration, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask, helper) } @@ -73,15 +63,6 @@ internal class DefaultTimelineService(private val roomId: String, contextOfEventTask.configureWith(params).executeBy(taskExecutor) } - private fun buildPagedListConfig(): PagedList.Config { - return PagedList.Config.Builder() - .setEnablePlaceholders(false) - .setPageSize(PAGE_SIZE) - .setInitialLoadSizeHint(2 * PAGE_SIZE) - .setPrefetchDistance(PREFETCH_DISTANCE) - .build() - } - private fun clearUnlinkedEvents() { monarchy.tryTransactionAsync { realm -> val unlinkedEvents = EventEntity @@ -101,18 +82,5 @@ internal class DefaultTimelineService(private val roomId: String, return displayIndex } - private fun buildDataSourceFactoryQuery(realm: Realm, eventId: String?): RealmQuery { - val query = if (eventId == null) { - EventEntity - .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY) - .equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST}", true) - } else { - EventEntity - .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH) - .`in`("${EventEntityFields.CHUNK}.${ChunkEntityFields.EVENTS.EVENT_ID}", arrayOf(eventId)) - } - return query.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) - } - } \ No newline at end of file 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 fa5b9e23..aeec3a3f 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 @@ -95,8 +95,8 @@ internal class TimelineBoundaryCallback(private val roomId: String, paginationTask.configureWith(params) .enableRetry() - .dispatchTo(object : MatrixCallback { - override fun onSuccess(data: Boolean) { + .dispatchTo(object : MatrixCallback { + override fun onSuccess(data: TokenChunkEventPersistor.Result) { requestCallback.recordSuccess() } 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 e728a140..187833ef 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 @@ -36,13 +36,15 @@ import io.realm.kotlin.createObject internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { + enum class Result { + SHOULD_FETCH_MORE, + SUCCESS + } + fun insertInDb(receivedChunk: TokenChunkEvent, roomId: String, - direction: PaginationDirection): Try { + direction: PaginationDirection): Try { - if (receivedChunk.events.isEmpty() && receivedChunk.stateEvents.isEmpty()) { - return Try.just(false) - } return monarchy .tryTransactionSync { realm -> val roomEntity = RoomEntity.where(realm, roomId).findFirst() @@ -71,27 +73,36 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { nextChunk?.apply { this.prevToken = prevToken } ?: ChunkEntity.create(realm, prevToken, nextToken) } - - currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.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) + if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) { + currentChunk.isLastBackward = true } else { - val newEventIds = receivedChunk.events.mapNotNull { it.eventId } - ChunkEntity - .findAllIncludingEvents(realm, newEventIds) - .filter { it != currentChunk } - .forEach { overlapped -> - currentChunk = handleMerge(roomEntity, direction, currentChunk, overlapped) - } + currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.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 = currentChunk.isUnlinked()) + } + } + .map { + if (receivedChunk.events.isEmpty() && receivedChunk.stateEvents.isEmpty() && receivedChunk.start != receivedChunk.end) { + Result.SHOULD_FETCH_MORE + } else { + Result.SUCCESS } - roomEntity.addOrUpdate(currentChunk) - roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = currentChunk.isUnlinked()) } - .map { true } } private fun handleMerge(roomEntity: RoomEntity, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index 4e699925..51044646 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -51,7 +51,6 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, data class INVITED(val data: Map) : HandlingStrategy() data class LEFT(val data: Map) : HandlingStrategy() } - fun handle(roomsSyncResponse: RoomsSyncResponse) { monarchy.runTransactionSync { realm -> handleRoomSync(realm, RoomSyncHandler.HandlingStrategy.JOINED(roomsSyncResponse.join)) @@ -164,8 +163,8 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, realm.createObject().apply { this.prevToken = prevToken } } - lastChunk?.isLast = false - chunkEntity.isLast = true + lastChunk?.isLastForward = false + chunkEntity.isLastForward = true chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS, stateIndexOffset) return chunkEntity } From ad243ae41ff6dcc2fe20cd1114caaa5c2417e7e1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 20 Mar 2019 18:24:17 +0100 Subject: [PATCH 05/12] Clean code and update dependencies --- app/build.gradle | 4 ++-- .../features/home/room/detail/RoomDetailFragment.kt | 1 - .../features/home/room/detail/RoomDetailViewModel.kt | 2 -- .../home/room/detail/timeline/TimelineEventController.kt | 4 ++++ matrix-sdk-android/build.gradle | 6 +++--- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 762fafde..1b49857a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -73,7 +73,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" - implementation 'androidx.appcompat:appcompat:1.1.0-alpha01' + implementation 'androidx.appcompat:appcompat:1.1.0-alpha03' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.core:core-ktx:1.0.1' @@ -96,7 +96,7 @@ dependencies { // UI implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' - implementation 'com.google.android.material:material:1.1.0-alpha02' + implementation 'com.google.android.material:material:1.1.0-alpha04' implementation 'me.gujun.android:span:1.7' implementation "ru.noties.markwon:core:$markwon_version" implementation "ru.noties.markwon:html:$markwon_version" diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 7636cce4..b8d1a5dc 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -138,7 +138,6 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { private fun renderState(state: RoomDetailViewState) { renderRoomSummary(state) timelineEventController.setTimeline(state.timeline) - //renderTimeline(state) } private fun renderRoomSummary(state: RoomDetailViewState) { diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index 99e5b7b9..e2e394d2 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -27,7 +27,6 @@ import im.vector.riotredesign.core.platform.RiotViewModel import im.vector.riotredesign.features.home.room.VisibleRoomStore import io.reactivex.rxkotlin.subscribeBy import org.koin.android.ext.android.get -import timber.log.Timber import java.util.concurrent.TimeUnit class RoomDetailViewModel(initialState: RoomDetailViewState, @@ -104,7 +103,6 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, private fun observeRoomSummary() { room.rx().liveRoomSummary() .execute { async -> - Timber.v("Room summary updated: $async") copy(asyncRoomSummary = async) } } 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 5516ea13..c16b9c94 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 @@ -80,6 +80,10 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, private var timeline: Timeline? = null var callback: Callback? = null + init { + requestModelBuild() + } + fun setTimeline(timeline: Timeline?) { if (this.timeline != timeline) { this.timeline = timeline diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 710be923..4653d4b5 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -47,7 +47,7 @@ android { dependencies { def arrow_version = "0.8.0" - def support_version = '1.1.0-alpha01' + def support_version = '1.1.0-alpha03' def moshi_version = '1.8.0' def lifecycle_version = '2.0.0' def coroutines_version = "1.0.1" @@ -66,7 +66,7 @@ dependencies { // Network implementation 'com.squareup.retrofit2:retrofit:2.4.0' implementation 'com.squareup.retrofit2:converter-moshi:2.4.0' - implementation 'com.squareup.okhttp3:okhttp:3.10.0' + implementation 'com.squareup.okhttp3:okhttp:3.11.0' implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0' implementation 'com.novoda:merlin:1.1.6' implementation "com.squareup.moshi:moshi-adapters:$moshi_version" @@ -77,7 +77,7 @@ dependencies { kapt 'dk.ilios:realmfieldnameshelper:1.1.1' // Work - implementation "android.arch.work:work-runtime-ktx:1.0.0-beta02" + implementation "android.arch.work:work-runtime-ktx:1.0.0" // FP implementation "io.arrow-kt:arrow-core:$arrow_version" From 4154df7c21773d05770a905758a51acd672cce66 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 21 Mar 2019 20:21:45 +0100 Subject: [PATCH 06/12] Timeline : stabilize the pagedList replacement. Seems ok for phase0 --- .../riotredesign/features/home/HomeModule.kt | 29 +- .../features/home/HomeNavigator.kt | 3 +- .../home/room/detail/RoomDetailFragment.kt | 18 +- .../home/room/detail/RoomDetailViewModel.kt | 12 +- .../room/detail/ScrollOnNewMessageCallback.kt | 3 - .../timeline/TimelineEventController.kt | 92 +-- .../animation/TimelineItemAnimator.kt | 6 +- .../timeline/factory/MessageItemFactory.kt | 9 +- .../EndlessRecyclerViewScrollListener.java | 114 ---- .../EndlessRecyclerViewScrollListener.kt | 73 +++ .../timeline/helper/TimelineAsyncHelper.kt | 19 +- .../helper/TimelineDisplayableEvents.kt | 57 ++ app/src/main/res/layout/item_empty.xml | 2 +- build.gradle | 2 +- .../room/timeline/TimelineHolderTest.kt | 7 +- .../api/session/room/timeline/Timeline.kt | 60 +- .../session/room/timeline/TimelineService.kt | 9 +- .../internal/database/RealmLiveData.kt | 49 ++ .../database/helper/ChunkEntityHelper.kt | 19 +- .../internal/database/model/ChunkEntity.kt | 9 +- .../internal/database/model/EventEntity.kt | 12 +- .../database/query/EventEntityQueries.kt | 2 +- .../internal/session/room/DefaultRoom.kt | 19 +- .../session/room/RoomAvatarResolver.kt | 4 +- .../internal/session/room/RoomFactory.kt | 7 +- .../session/room/RoomSummaryUpdater.kt | 4 +- .../room/members/RoomDisplayNameResolver.kt | 18 +- .../room/members/RoomMemberExtractor.kt | 26 +- .../session/room/timeline/DefaultTimeline.kt | 337 +++++++---- .../room/timeline/DefaultTimelineService.kt | 56 +- .../room/timeline/TimelineBoundaryCallback.kt | 110 ---- .../room/timeline/TokenChunkEventPersistor.kt | 19 +- .../internal/util/PagingRequestHelper.java | 530 ------------------ 33 files changed, 611 insertions(+), 1125 deletions(-) delete mode 100644 app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.java create mode 100644 app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt create mode 100644 app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveData.kt delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineBoundaryCallback.kt delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/PagingRequestHelper.java diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt index cb299d45..a013a0dc 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt @@ -19,16 +19,9 @@ package im.vector.riotredesign.features.home import androidx.fragment.app.Fragment import im.vector.riotredesign.core.glide.GlideApp import im.vector.riotredesign.features.home.group.GroupSummaryController -import im.vector.riotredesign.features.home.room.detail.timeline.factory.CallItemFactory -import im.vector.riotredesign.features.home.room.detail.timeline.factory.DefaultItemFactory -import im.vector.riotredesign.features.home.room.detail.timeline.factory.MessageItemFactory -import im.vector.riotredesign.features.home.room.detail.timeline.factory.RoomHistoryVisibilityItemFactory -import im.vector.riotredesign.features.home.room.detail.timeline.factory.RoomMemberItemFactory -import im.vector.riotredesign.features.home.room.detail.timeline.factory.RoomNameItemFactory -import im.vector.riotredesign.features.home.room.detail.timeline.factory.RoomTopicItemFactory -import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController -import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory +import im.vector.riotredesign.features.home.room.detail.timeline.factory.* +import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.riotredesign.features.home.room.list.RoomSummaryController import im.vector.riotredesign.features.html.EventHtmlRenderer @@ -57,28 +50,28 @@ class HomeModule { // Fragment scopes - scope(ROOM_DETAIL_SCOPE) { (fragment: Fragment) -> + factory { (fragment: Fragment) -> val eventHtmlRenderer = EventHtmlRenderer(GlideApp.with(fragment), fragment.requireContext(), get()) val timelineDateFormatter = TimelineDateFormatter(get()) val timelineMediaSizeProvider = TimelineMediaSizeProvider() val messageItemFactory = MessageItemFactory(get(), timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer) val timelineItemFactory = TimelineItemFactory(messageItemFactory = messageItemFactory, - roomNameItemFactory = RoomNameItemFactory(get()), - roomTopicItemFactory = RoomTopicItemFactory(get()), - roomMemberItemFactory = RoomMemberItemFactory(get()), - roomHistoryVisibilityItemFactory = RoomHistoryVisibilityItemFactory(get()), - callItemFactory = CallItemFactory(get()), - defaultItemFactory = DefaultItemFactory() + roomNameItemFactory = RoomNameItemFactory(get()), + roomTopicItemFactory = RoomTopicItemFactory(get()), + roomMemberItemFactory = RoomMemberItemFactory(get()), + roomHistoryVisibilityItemFactory = RoomHistoryVisibilityItemFactory(get()), + callItemFactory = CallItemFactory(get()), + defaultItemFactory = DefaultItemFactory() ) TimelineEventController(timelineDateFormatter, timelineItemFactory, timelineMediaSizeProvider) } - scope(ROOM_LIST_SCOPE) { + factory { RoomSummaryController(get()) } - scope(GROUP_LIST_SCOPE) { + factory { GroupSummaryController() } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt index f6583b97..b0d5748a 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt @@ -37,7 +37,8 @@ class HomeNavigator { addToBackstack: Boolean = false) { Timber.v("Open room detail $roomId - $eventId - $addToBackstack") activity?.let { - val args = RoomDetailArgs(roomId, eventId) + //TODO enable eventId permalink. It doesn't work enough at the moment. + val args = RoomDetailArgs(roomId) val roomDetailFragment = RoomDetailFragment.newInstance(args) it.drawerLayout?.closeDrawer(Gravity.LEFT) if (addToBackstack) { diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index b8d1a5dc..8f9d494e 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -25,7 +25,6 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.mvrx.fragmentViewModel -import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.R import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer @@ -111,16 +110,11 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { it.dispatchTo(stateRestorer) it.dispatchTo(scrollOnNewMessageCallback) } - recyclerView.addOnScrollListener(object : EndlessRecyclerViewScrollListener(layoutManager, Timeline.Direction.BACKWARDS) { - override fun onLoadMore() { - roomDetailViewModel.process(RoomDetailActions.LoadMore(Timeline.Direction.BACKWARDS)) - } - }) - recyclerView.addOnScrollListener(object : EndlessRecyclerViewScrollListener(layoutManager, Timeline.Direction.FORWARDS) { - override fun onLoadMore() { - roomDetailViewModel.process(RoomDetailActions.LoadMore(Timeline.Direction.FORWARDS)) - } - }) + + recyclerView.addOnScrollListener( + EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction -> + roomDetailViewModel.process(RoomDetailActions.LoadMore(direction)) + }) recyclerView.setController(timelineEventController) timelineEventController.callback = this } @@ -153,7 +147,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { } } - // TimelineEventController.Callback ************************************************************ +// TimelineEventController.Callback ************************************************************ override fun onUrlClicked(url: String) { homePermalinkHandler.launch(url) diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index e2e394d2..4ebf4984 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -25,6 +25,7 @@ import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.rx.rx import im.vector.riotredesign.core.platform.RiotViewModel import im.vector.riotredesign.features.home.room.VisibleRoomStore +import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDisplayableEvents import io.reactivex.rxkotlin.subscribeBy import org.koin.android.ext.android.get import java.util.concurrent.TimeUnit @@ -38,10 +39,12 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, private val roomId = initialState.roomId private val eventId = initialState.eventId private val displayedEventsObservable = BehaviorRelay.create() - private val timeline = room.createTimeline(eventId) + private val timeline = room.createTimeline(eventId, TimelineDisplayableEvents.DISPLAYABLE_TYPES) companion object : MvRxViewModelFactory { + const val PAGINATION_COUNT = 50 + @JvmStatic override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel? { val currentSession = viewModelContext.activity.get() @@ -52,7 +55,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, init { observeRoomSummary() - observeDisplayedEvents() + observeEventDisplayedActions() room.loadRoomMembersIfNeeded() timeline.start() setState { copy(timeline = this@RoomDetailViewModel.timeline) } @@ -82,10 +85,10 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, } private fun handleLoadMore(action: RoomDetailActions.LoadMore) { - timeline.paginate(action.direction, 50) + timeline.paginate(action.direction, PAGINATION_COUNT) } - private fun observeDisplayedEvents() { + private fun observeEventDisplayedActions() { // We are buffering scroll events for one second // and keep the most recent one to set the read receipt on. displayedEventsObservable @@ -111,5 +114,4 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, timeline.dispose() super.onCleared() } - } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/ScrollOnNewMessageCallback.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/ScrollOnNewMessageCallback.kt index 6c93997b..0888f672 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/ScrollOnNewMessageCallback.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/ScrollOnNewMessageCallback.kt @@ -18,12 +18,9 @@ package im.vector.riotredesign.features.home.room.detail import androidx.recyclerview.widget.LinearLayoutManager import im.vector.riotredesign.core.platform.DefaultListUpdateCallback -import java.util.concurrent.atomic.AtomicBoolean class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager) : DefaultListUpdateCallback { - var isLocked = AtomicBoolean(true) - override fun onInserted(position: Int, count: Int) { if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0) { layoutManager.scrollToPosition(0) 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 c16b9c94..0c6b28e1 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,7 @@ package im.vector.riotredesign.features.home.room.detail.timeline import android.os.Handler +import android.os.Looper import android.view.View import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListUpdateCallback @@ -30,10 +31,7 @@ import im.vector.riotredesign.core.epoxy.LoadingItemModel_ import im.vector.riotredesign.core.epoxy.RiotEpoxyModel import im.vector.riotredesign.core.extensions.localDateTime import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory -import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineAsyncHelper -import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter -import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback -import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider +import im.vector.riotredesign.features.home.room.detail.timeline.helper.* import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem_ import im.vector.riotredesign.features.media.MediaContentRenderer @@ -43,25 +41,41 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, private val backgroundHandler: Handler = TimelineAsyncHelper.getBackgroundHandler() ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { + interface Callback { + fun onEventVisible(event: TimelineEvent) + fun onUrlClicked(url: String) + fun onMediaClicked(mediaData: MediaContentRenderer.Data, view: View) + } + private val modelCache = arrayListOf>>() private var currentSnapshot: List = emptyList() + private var inSubmitList: Boolean = false + private var timeline: Timeline? = null + + var callback: Callback? = null private val listUpdateCallback = object : ListUpdateCallback { @Synchronized override fun onChanged(position: Int, count: Int, payload: Any?) { + assertUpdateCallbacksAllowed() (position until (position + count)).forEach { modelCache[it] = emptyList() } requestModelBuild() } + @Synchronized override fun onMoved(fromPosition: Int, toPosition: Int) { - //no-op + assertUpdateCallbacksAllowed() + val model = modelCache.removeAt(fromPosition) + modelCache.add(toPosition, model) + requestModelBuild() } @Synchronized - override fun onInserted(position: Int, count: Int) = synchronized(modelCache) { + override fun onInserted(position: Int, count: Int) { + assertUpdateCallbacksAllowed() if (modelCache.isNotEmpty() && position == modelCache.size) { modelCache[position - 1] = emptyList() } @@ -71,15 +85,16 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, requestModelBuild() } + @Synchronized override fun onRemoved(position: Int, count: Int) { - //no-op + assertUpdateCallbacksAllowed() + (0 until count).forEach { + modelCache.removeAt(position) + } + requestModelBuild() } - } - private var timeline: Timeline? = null - var callback: Callback? = null - init { requestModelBuild() } @@ -101,18 +116,34 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, .id("forward_loading_item") .addWhen(Timeline.Direction.FORWARDS) - add(getModels()) + + val timelineModels = getModels() + add(timelineModels) LoadingItemModel_() .id("backward_loading_item") .addWhen(Timeline.Direction.BACKWARDS) } - private fun LoadingItemModel_.addWhen(direction: Timeline.Direction) { - val shouldAdd = timeline?.let { - it.hasMoreToLoad(direction) || !it.hasReachedEnd(direction) - } ?: false - addIf(shouldAdd, this@TimelineEventController) + // Timeline.LISTENER *************************************************************************** + + override fun onUpdated(snapshot: List) { + submitSnapshot(snapshot) + } + + private fun submitSnapshot(newSnapshot: List) { + backgroundHandler.post { + inSubmitList = true + val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot) + currentSnapshot = newSnapshot + val diffResult = DiffUtil.calculateDiff(diffCallback) + diffResult.dispatchUpdatesTo(listUpdateCallback) + inSubmitList = false + } + } + + private fun assertUpdateCallbacksAllowed() { + require(inSubmitList || Looper.myLooper() == backgroundHandler.looper) } @Synchronized @@ -128,7 +159,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, private fun buildItemModels(currentPosition: Int, items: List): List> { val epoxyModels = ArrayList>() val event = items[currentPosition] - val nextEvent = if (currentPosition + 1 < items.size) items[currentPosition + 1] else null + val nextEvent = items.nextDisplayableEvent(currentPosition) val date = event.root.localDateTime() val nextDate = nextEvent?.root?.localDateTime() @@ -147,26 +178,11 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, return epoxyModels } - // Timeline.LISTENER *************************************************************************** - - override fun onUpdated(snapshot: List) { - submitSnapshot(snapshot) - } - - private fun submitSnapshot(newSnapshot: List) { - backgroundHandler.post { - val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot) - currentSnapshot = newSnapshot - val diffResult = DiffUtil.calculateDiff(diffCallback) - diffResult.dispatchUpdatesTo(listUpdateCallback) - } - } - - - interface Callback { - fun onEventVisible(event: TimelineEvent) - fun onUrlClicked(url: String) - fun onMediaClicked(mediaData: MediaContentRenderer.Data, view: View) + private fun LoadingItemModel_.addWhen(direction: Timeline.Direction) { + val shouldAdd = timeline?.let { + it.hasMoreToLoad(direction) + } ?: false + addIf(shouldAdd, this@TimelineEventController) } } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/animation/TimelineItemAnimator.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/animation/TimelineItemAnimator.kt index 8a9615db..94fe199c 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/animation/TimelineItemAnimator.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/animation/TimelineItemAnimator.kt @@ -24,9 +24,9 @@ class TimelineItemAnimator : DefaultItemAnimator() { init { addDuration = ANIM_DURATION_IN_MILLIS - removeDuration = ANIM_DURATION_IN_MILLIS - moveDuration = ANIM_DURATION_IN_MILLIS - changeDuration = ANIM_DURATION_IN_MILLIS + removeDuration = 0 + moveDuration = 0 + changeDuration = 0 } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt index a490d8fb..8d03e42b 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -42,8 +42,6 @@ class MessageItemFactory(private val colorProvider: ColorProvider, private val timelineDateFormatter: TimelineDateFormatter, private val htmlRenderer: EventHtmlRenderer) { - private val messagesDisplayedWithInformation = HashSet() - fun create(event: TimelineEvent, nextEvent: TimelineEvent?, callback: TimelineEventController.Callback? @@ -58,15 +56,12 @@ class MessageItemFactory(private val colorProvider: ColorProvider, val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60)) ?: false - if (addDaySeparator + val showInformation = addDaySeparator || nextRoomMember != roomMember || nextEvent?.root?.type != EventType.MESSAGE - || isNextMessageReceivedMoreThanOneHourAgo) { - messagesDisplayedWithInformation.add(event.root.eventId) - } + || isNextMessageReceivedMoreThanOneHourAgo val messageContent: MessageContent = event.root.content.toModel() ?: return null - val showInformation = messagesDisplayedWithInformation.contains(event.root.eventId) val time = timelineDateFormatter.formatMessageHour(date) val avatarUrl = roomMember?.avatarUrl val memberName = roomMember?.displayName ?: event.root.sender diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.java b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.java deleted file mode 100644 index e77ed44f..00000000 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.riotredesign.features.home.room.detail.timeline.helper; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import im.vector.matrix.android.api.session.room.timeline.Timeline; - -public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnScrollListener { - // Sets the starting page index - private static final int startingPageIndex = 0; - // The minimum amount of items to have below your current scroll position - // before loading more. - private int visibleThreshold = 50; - // The total number of items in the dataset after the last load - private int previousTotalItemCount = 0; - // True if we are still waiting for the last set of data to load. - private boolean loading = true; - private LinearLayoutManager mLayoutManager; - private Timeline.Direction mDirection; - - public EndlessRecyclerViewScrollListener(LinearLayoutManager layoutManager, Timeline.Direction direction) { - this.mLayoutManager = layoutManager; - this.mDirection = direction; - } - - - // This happens many times a second during a scroll, so be wary of the code you place here. - // We are given a few useful parameters to help us work out if we need to load some more data, - // but first we check if we are waiting for the previous load to finish. - @Override - public void onScrolled(@NonNull RecyclerView view, int dx, int dy) { - int lastVisibleItemPosition = 0; - int firstVisibleItemPosition = 0; - int totalItemCount = mLayoutManager.getItemCount(); - - lastVisibleItemPosition = mLayoutManager.findLastVisibleItemPosition(); - firstVisibleItemPosition = mLayoutManager.findFirstVisibleItemPosition(); - - switch (mDirection) { - case BACKWARDS: - // If the total item count is zero and the previous isn't, assume the - // list is invalidated and should be reset back to initial state - if (totalItemCount < previousTotalItemCount) { - this.previousTotalItemCount = totalItemCount; - if (totalItemCount == 0) { - this.loading = true; - } - } - // If it’s still loading, we check to see if the dataset count has - // changed, if so we conclude it has finished loading and update the current page - // number and total item count. - if (loading && (totalItemCount > previousTotalItemCount)) { - loading = false; - previousTotalItemCount = totalItemCount; - } - - // If it isn’t currently loading, we check to see if we have breached - // the visibleThreshold and need to reload more data. - // If we do need to reload some more data, we execute onLoadMore to fetch the data. - // threshold should reflect how many total columns there are too - if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) { - onLoadMore(); - loading = true; - } - break; - case FORWARDS: - // If the total item count is zero and the previous isn't, assume the - // list is invalidated and should be reset back to initial state - if (totalItemCount < previousTotalItemCount) { - this.previousTotalItemCount = totalItemCount; - if (totalItemCount == 0) { - this.loading = true; - } - } - // If it’s still loading, we check to see if the dataset count has - // changed, if so we conclude it has finished loading and update the current page - // number and total item count. - if (loading && (totalItemCount > previousTotalItemCount)) { - loading = false; - previousTotalItemCount = totalItemCount; - } - - // If it isn’t currently loading, we check to see if we have breached - // the visibleThreshold and need to reload more data. - // If we do need to reload some more data, we execute onLoadMore to fetch the data. - // threshold should reflect how many total columns there are too - if (!loading && firstVisibleItemPosition < visibleThreshold) { - onLoadMore(); - loading = true; - } - break; - } - } - - // Defines the process for actually loading more data based on page - public abstract void onLoadMore(); - -} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt new file mode 100644 index 00000000..425dd094 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.features.home.room.detail.timeline.helper + +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import im.vector.matrix.android.api.session.room.timeline.Timeline + +class EndlessRecyclerViewScrollListener(private val layoutManager: LinearLayoutManager, + private val visibleThreshold: Int, + private val onLoadMore: (Timeline.Direction) -> Unit +) : RecyclerView.OnScrollListener() { + + // The total number of items in the dataset after the last load + private var previousTotalItemCount = 0 + // True if we are still waiting for the last set of data to load. + private var loadingBackwards = true + private var loadingForwards = true + + // This happens many times a second during a scroll, so be wary of the code you place here. + // We are given a few useful parameters to help us work out if we need to load some more data, + // but first we check if we are waiting for the previous load to finish. + override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { + val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() + val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() + val totalItemCount = layoutManager.itemCount + + // The minimum amount of items to have below your current scroll position + // before loading more. + // If the total item count is zero and the previous isn't, assume the + // list is invalidated and should be reset back to initial state + if (totalItemCount < previousTotalItemCount) { + previousTotalItemCount = totalItemCount + if (totalItemCount == 0) { + loadingForwards = true + loadingBackwards = true + } + } + // If it’s still loading, we check to see if the dataset count has + // changed, if so we conclude it has finished loading + if (totalItemCount > previousTotalItemCount) { + loadingBackwards = false + loadingForwards = false + previousTotalItemCount = totalItemCount + } + // If it isn’t currently loading, we check to see if we have breached + // the visibleThreshold and need to reload more data. + if (!loadingBackwards && lastVisibleItemPosition + visibleThreshold > totalItemCount) { + loadingBackwards = true + onLoadMore(Timeline.Direction.BACKWARDS) + } + if (!loadingForwards && firstVisibleItemPosition < visibleThreshold) { + loadingForwards = true + onLoadMore(Timeline.Direction.FORWARDS) + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineAsyncHelper.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineAsyncHelper.kt index f721413b..a54d5f83 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineAsyncHelper.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineAsyncHelper.kt @@ -25,23 +25,16 @@ private const val THREAD_NAME = "Timeline_Building_Thread" object TimelineAsyncHelper { - private var backgroundHandlerThread: HandlerThread? = null private var backgroundHandler: Handler? = null fun getBackgroundHandler(): Handler { - if (backgroundHandler != null) { - backgroundHandler?.removeCallbacksAndMessages(null) - } - if (backgroundHandlerThread != null) { - backgroundHandlerThread?.quit() - } + return backgroundHandler ?: createBackgroundHandler().also { backgroundHandler = it } + } + + private fun createBackgroundHandler(): Handler { val handlerThread = HandlerThread(THREAD_NAME) - .also { - backgroundHandlerThread = it - it.start() - } - val looper = handlerThread.looper - return Handler(looper).also { backgroundHandler = it } + handlerThread.start() + return Handler(handlerThread.looper) } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt new file mode 100644 index 00000000..1d3a8bf7 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.features.home.room.detail.timeline.helper + +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent + +object TimelineDisplayableEvents { + + val DISPLAYABLE_TYPES = listOf( + EventType.MESSAGE, + EventType.STATE_ROOM_NAME, + EventType.STATE_ROOM_TOPIC, + EventType.STATE_ROOM_MEMBER, + EventType.STATE_HISTORY_VISIBILITY, + EventType.CALL_INVITE, + EventType.CALL_HANGUP, + EventType.CALL_ANSWER, + EventType.ENCRYPTED, + EventType.ENCRYPTION, + EventType.STATE_ROOM_THIRD_PARTY_INVITE, + EventType.STICKER, + EventType.STATE_ROOM_CREATE + ) +} + +fun TimelineEvent.isDisplayable(): Boolean { + return TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.type) +} + +fun List.filterDisplayableEvents(): List { + return this.filter { + it.isDisplayable() + } +} + +fun List.nextDisplayableEvent(index: Int): TimelineEvent? { + return if (index == size - 1) { + null + } else { + subList(index + 1, this.size).firstOrNull { it.isDisplayable() } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/item_empty.xml b/app/src/main/res/layout/item_empty.xml index f7afb775..c8dee60c 100644 --- a/app/src/main/res/layout/item_empty.xml +++ b/app/src/main/res/layout/item_empty.xml @@ -1,4 +1,4 @@ \ No newline at end of file + android:layout_height="0dp" /> \ No newline at end of file diff --git a/build.gradle b/build.gradle index 226ca00e..92421278 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:3.3.2' classpath "com.airbnb.okreplay:gradle-plugin:1.4.0" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineHolderTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineHolderTest.kt index 917b955b..2063a618 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineHolderTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineHolderTest.kt @@ -21,13 +21,10 @@ import androidx.test.annotation.UiThreadTest import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.LiveDataTestObserver -import im.vector.matrix.android.MainThreadExecutor import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService -import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.util.PagingRequestHelper import im.vector.matrix.android.testCoroutineDispatchers import io.realm.Realm import io.realm.RealmConfiguration @@ -56,10 +53,8 @@ internal class TimelineHolderTest : InstrumentedTest { val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy) val paginationTask = FakePaginationTask(tokenChunkEventPersistor) val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor) - val boundaryCallback = TimelineBoundaryCallback(roomId, taskExecutor, paginationTask, monarchy, PagingRequestHelper(MainThreadExecutor())) - RoomDataHelper.fakeInitialSync(monarchy, roomId) - val timelineHolder = DefaultTimelineService(roomId, monarchy, taskExecutor, boundaryCallback, getContextOfEventTask, RoomMemberExtractor(monarchy, roomId)) + val timelineHolder = DefaultTimelineService(roomId, monarchy, taskExecutor, getContextOfEventTask, RoomMemberExtractor(roomId)) val timelineObserver = LiveDataTestObserver.test(timelineHolder.timeline()) timelineObserver.awaitNextValue().assertHasValue() var timelineData = timelineObserver.value() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index 1a09c591..2c2530bb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -18,35 +18,67 @@ package im.vector.matrix.android.api.session.room.timeline +/** + * A Timeline instance represents a contiguous sequence of events in a room. + *

+ * There are two kinds of timeline: + *

+ * - live timelines: they process live events from the sync. You can paginate + * backwards but not forwards. + *

+ * - past timelines: they start in the past from an `initialEventId`. You can paginate + * backwards and forwards. + * + */ interface Timeline { var listener: Timeline.Listener? - fun hasMoreToLoad(direction: Direction): Boolean - fun hasReachedEnd(direction: Direction): Boolean - fun size(): Int - fun snapshot(): List - fun paginate(direction: Direction, count: Int) + /** + * This should be called before any other method after creating the timeline. It ensures the underlying database is open + */ fun start() + + /** + * This should be called when you don't need the timeline. It ensures the underlying database get closed. + */ fun dispose() + /** + * Check if the timeline can be enriched by paginating. + * @param the direction to check in + * @return true if timeline can be enriched + */ + fun hasMoreToLoad(direction: Direction): Boolean + + /** + * This is the main method to enrich the timeline with new data. + * It will call the onUpdated method from [Listener] when the data will be processed. + * It also ensures only one pagination by direction is launched at a time, so you can safely call this multiple time in a row. + */ + fun paginate(direction: Direction, count: Int) + + interface Listener { + /** + * Call when the timeline has been updated through pagination or sync. + * @param snapshot the most uptodate snapshot + */ fun onUpdated(snapshot: List) } - enum class Direction(val value: String) { + /** + * This is used to paginate in one or another direction. + */ + enum class Direction { /** - * Forwards when the event is added to the end of the timeline. - * These events come from the /sync stream or from forwards pagination. + * It represents future events. */ - FORWARDS("f"), - + FORWARDS, /** - * Backwards when the event is added to the start of the timeline. - * These events come from a back pagination. + * It represents past events. */ - BACKWARDS("b"); + BACKWARDS } - } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt index 28af1fe2..0980cfc5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt @@ -21,6 +21,13 @@ package im.vector.matrix.android.api.session.room.timeline */ interface TimelineService { - fun createTimeline(eventId: String?): Timeline + /** + * Instantiate a [Timeline] with an optional initial eventId, to be used with permalink. + * You can filter the type you want to grab with the allowedTypes param. + * @param eventId the optional initial eventId. + * @param allowedTypes the optional filter types + * @return the instantiated timeline + */ + fun createTimeline(eventId: String?, allowedTypes: List? = null): Timeline } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveData.kt new file mode 100644 index 00000000..6002d4d4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveData.kt @@ -0,0 +1,49 @@ +/* + * + * * Copyright 2019 New Vector Ltd + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package im.vector.matrix.android.internal.database + +import androidx.lifecycle.LiveData +import io.realm.* + +class RealmLiveData(private val realmConfiguration: RealmConfiguration, + private val query: (Realm) -> RealmQuery) : LiveData>() { + + private val listener = RealmChangeListener> { results -> + value = results + } + + private var realm: Realm? = null + private var results: RealmResults? = null + + override fun onActive() { + val realm = Realm.getInstance(realmConfiguration) + val results = query.invoke(realm).findAll() + value = results + results.addChangeListener(listener) + this.realm = realm + this.results = results + } + + override fun onInactive() { + results?.removeChangeListener(listener) + results = null + realm?.close() + realm = null + } +} \ 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 ed80750d..e352e847 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 @@ -54,11 +54,11 @@ internal fun ChunkEntity.merge(roomId: String, if (direction == PaginationDirection.FORWARDS) { this.nextToken = chunkToMerge.nextToken this.isLastForward = chunkToMerge.isLastForward - eventsToMerge = chunkToMerge.events.reversed() + eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) } else { this.prevToken = chunkToMerge.prevToken this.isLastBackward = chunkToMerge.isLastBackward - eventsToMerge = chunkToMerge.events + eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) } eventsToMerge.forEach { add(roomId, it.asDomain(), direction, isUnlinked = isUnlinked) @@ -107,7 +107,8 @@ internal fun ChunkEntity.add(roomId: String, this.displayIndex = currentDisplayIndex } // We are not using the order of the list, but will be sorting with displayIndex field - events.add(eventEntity) + val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size + events.add(position, eventEntity) } private fun ChunkEntity.assertIsManaged() { @@ -118,14 +119,14 @@ private fun ChunkEntity.assertIsManaged() { internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findFirst()?.displayIndex - PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING).findFirst()?.displayIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findFirst()?.displayIndex + PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING).findFirst()?.displayIndex + } ?: defaultValue } 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/ChunkEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt index 1c7c755a..dd3520e9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt @@ -19,13 +19,14 @@ package im.vector.matrix.android.internal.database.model import io.realm.RealmList import io.realm.RealmObject import io.realm.RealmResults +import io.realm.annotations.Index import io.realm.annotations.LinkingObjects -internal open class ChunkEntity(var prevToken: String? = null, - var nextToken: String? = null, +internal open class ChunkEntity(@Index var prevToken: String? = null, + @Index var nextToken: String? = null, var events: RealmList = RealmList(), - var isLastForward: Boolean = false, - var isLastBackward: Boolean = false + @Index var isLastForward: Boolean = false, + @Index var isLastBackward: Boolean = false ) : RealmObject() { @LinkingObjects("chunks") 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 5a7b04d8..c688a11c 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 @@ -26,17 +26,17 @@ import java.util.* internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUID().toString(), @Index var eventId: String = "", var roomId: String = "", - var type: String = "", + @Index var type: String = "", var content: String? = null, var prevContent: String? = null, - var stateKey: String? = null, + @Index var stateKey: String? = null, var originServerTs: Long? = null, - var sender: String? = null, + @Index var sender: String? = null, var age: Long? = 0, var redacts: String? = null, - var stateIndex: Int = 0, - var displayIndex: Int = 0, - var isUnlinked: Boolean = false + @Index var stateIndex: Int = 0, + @Index var displayIndex: Int = 0, + @Index var isUnlinked: Boolean = false ) : RealmObject() { enum class LinkFilterMode { 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 a86ec2c0..c500a880 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 @@ -77,7 +77,7 @@ internal fun RealmQuery.next(from: Int? = null, strict: Boolean = t .findFirst() } -internal fun RealmQuery.last(since: Int? = null, strict: Boolean = false): EventEntity? { +internal fun RealmQuery.prev(since: Int? = null, strict: Boolean = false): EventEntity? { if (since != null) { if (strict) { this.lessThan(EventEntityFields.STATE_INDEX, since) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt index de3de801..ae6255a1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt @@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields @@ -45,18 +46,16 @@ internal class DefaultRoom( ) : Room, - TimelineService by timelineService, - SendService by sendService, - ReadService by readService { + TimelineService by timelineService, + SendService by sendService, + ReadService by readService { override val roomSummary: LiveData by lazy { - val liveData = monarchy - .findAllMappedWithChanges( - { realm -> RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) }, - { from -> from.asDomain() }) - - Transformations.map(liveData) { - it.first() + val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> + RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) + } + Transformations.map(liveRealmData) { results -> + results.map { it.asDomain() }.first() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt index 235d18e7..c6786508 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt @@ -25,7 +25,7 @@ import im.vector.matrix.android.api.session.room.model.RoomAvatarContent import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.database.query.last +import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.room.members.RoomMembers @@ -41,7 +41,7 @@ internal class RoomAvatarResolver(private val monarchy: Monarchy, var res: String? = null monarchy.doWithRealm { realm -> val roomEntity = RoomEntity.where(realm, roomId).findFirst() - val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_AVATAR).last()?.asDomain() + val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_AVATAR).prev()?.asDomain() res = roomName?.content.toModel()?.avatarUrl if (!res.isNullOrEmpty()) { return@doWithRealm diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 6183be5c..b30ba3ee 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -30,8 +30,6 @@ import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEvent import im.vector.matrix.android.internal.session.room.timeline.PaginationTask import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.util.PagingRequestHelper -import java.util.concurrent.Executors internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask, private val monarchy: Monarchy, @@ -43,10 +41,9 @@ internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask, private val taskExecutor: TaskExecutor) { fun instantiate(roomId: String): Room { - val helper = PagingRequestHelper(Executors.newSingleThreadExecutor()) - val roomMemberExtractor = RoomMemberExtractor(monarchy, roomId) + val roomMemberExtractor = RoomMemberExtractor(roomId) val timelineEventFactory = TimelineEventFactory(roomMemberExtractor) - val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask, helper) + val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask) val sendService = DefaultSendService(roomId, eventFactory, monarchy) val readService = DefaultReadService(roomId, monarchy, setReadMarkersTask, taskExecutor) return DefaultRoom( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index 590d6ce8..f847237a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -27,8 +27,8 @@ import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity -import im.vector.matrix.android.internal.database.query.last import im.vector.matrix.android.internal.database.query.latestEvent +import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver import im.vector.matrix.android.internal.session.room.members.RoomMembers @@ -59,7 +59,7 @@ internal class RoomSummaryUpdater(monarchy: Monarchy, ?: realm.createObject(roomId) val lastEvent = EventEntity.latestEvent(realm, roomId, includedTypes = listOf(EventType.MESSAGE)) - val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).last()?.asDomain() + val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain() val otherRoomMembers = RoomMembers(realm, roomId).getLoaded().filterKeys { it != credentials.userId } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomDisplayNameResolver.kt index 7eb0f85f..f39dd300 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomDisplayNameResolver.kt @@ -30,7 +30,7 @@ import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity -import im.vector.matrix.android.internal.database.query.last +import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where /** @@ -58,19 +58,19 @@ internal class RoomDisplayNameResolver(private val monarchy: Monarchy, var name: CharSequence? = null monarchy.doWithRealm { realm -> val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() - val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_NAME).last()?.asDomain() + val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_NAME).prev()?.asDomain() name = roomName?.content.toModel()?.name if (!name.isNullOrEmpty()) { return@doWithRealm } - val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).last()?.asDomain() + val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).prev()?.asDomain() name = canonicalAlias?.content.toModel()?.canonicalAlias if (!name.isNullOrEmpty()) { return@doWithRealm } - val aliases = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).last()?.asDomain() + val aliases = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev()?.asDomain() name = aliases?.content.toModel()?.aliases?.firstOrNull() if (!name.isNullOrEmpty()) { return@doWithRealm @@ -111,16 +111,16 @@ internal class RoomDisplayNameResolver(private val monarchy: Monarchy, val member1 = memberIds[0] val member2 = memberIds[1] name = context.getString(R.string.room_displayname_two_members, - roomMemberDisplayNameResolver.resolve(member1, otherRoomMembers), - roomMemberDisplayNameResolver.resolve(member2, otherRoomMembers) + roomMemberDisplayNameResolver.resolve(member1, otherRoomMembers), + roomMemberDisplayNameResolver.resolve(member2, otherRoomMembers) ) } else -> { val member = memberIds[0] name = context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members, - roomMembers.getNumberOfJoinedMembers() - 1, - roomMemberDisplayNameResolver.resolve(member, otherRoomMembers), - roomMembers.getNumberOfJoinedMembers() - 1) + roomMembers.getNumberOfJoinedMembers() - 1, + roomMemberDisplayNameResolver.resolve(member, otherRoomMembers), + roomMembers.getNumberOfJoinedMembers() - 1) } } } 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 79d4a176..f2ec6a05 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,20 +16,19 @@ package im.vector.matrix.android.internal.session.room.members -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.internal.database.mapper.ContentMapper 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.last import im.vector.matrix.android.internal.database.query.next +import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where +import io.realm.Realm import io.realm.RealmQuery -internal class RoomMemberExtractor(private val monarchy: Monarchy, - private val roomId: String) { +internal class RoomMemberExtractor(private val roomId: String) { private val cached = HashMap() @@ -44,30 +43,25 @@ internal class RoomMemberExtractor(private val monarchy: Monarchy, // 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 content = if (event.stateIndex <= 0) { - baseQuery(monarchy, roomId, sender, unlinked).next(from = event.stateIndex)?.prevContent - ?: baseQuery(monarchy, roomId, sender, unlinked).last(since = event.stateIndex)?.content + baseQuery(event.realm, roomId, sender, unlinked).next(from = event.stateIndex)?.prevContent + ?: baseQuery(event.realm, roomId, sender, unlinked).prev(since = event.stateIndex)?.content } else { - baseQuery(monarchy, roomId, sender, unlinked).last(since = event.stateIndex)?.content + baseQuery(event.realm, roomId, sender, unlinked).prev(since = event.stateIndex)?.content } val roomMember: RoomMember? = ContentMapper.map(content).toModel() cached[cacheKey] = roomMember return roomMember } - private fun baseQuery(monarchy: Monarchy, + private fun baseQuery(realm: Realm, roomId: String, sender: String, isUnlinked: Boolean): RealmQuery { - lateinit var query: RealmQuery val filterMode = if (isUnlinked) EventEntity.LinkFilterMode.UNLINKED_ONLY else EventEntity.LinkFilterMode.LINKED_ONLY - monarchy.doWithRealm { realm -> - query = EventEntity - .where(realm, roomId = roomId, type = EventType.STATE_ROOM_MEMBER, linkFilterMode = filterMode) - .equalTo(EventEntityFields.STATE_KEY, sender) - } - return query + return EventEntity + .where(realm, roomId = roomId, type = EventType.STATE_ROOM_MEMBER, linkFilterMode = filterMode) + .equalTo(EventEntityFields.STATE_KEY, sender) } - } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index b85d8280..ef037ac5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -20,6 +20,7 @@ package im.vector.matrix.android.internal.session.room.timeline import android.os.Handler import android.os.HandlerThread +import android.os.Looper import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.timeline.Timeline @@ -30,19 +31,21 @@ import im.vector.matrix.android.internal.database.model.ChunkEntity 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.EventEntityFields +import im.vector.matrix.android.internal.database.query.findIncludingEvent +import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith -import im.vector.matrix.android.internal.util.PagingRequestHelper import io.realm.* +import timber.log.Timber import java.util.* -import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference -import kotlin.collections.ArrayList private const val INITIAL_LOAD_SIZE = 20 +private const val MIN_FETCHING_COUNT = 30 +private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE private const val THREAD_NAME = "TIMELINE_DB_THREAD" internal class DefaultTimeline( @@ -53,153 +56,237 @@ internal class DefaultTimeline( private val contextOfEventTask: GetContextOfEventTask, private val timelineEventFactory: TimelineEventFactory, private val paginationTask: PaginationTask, - private val helper: PagingRequestHelper + private val allowedTypes: List? ) : Timeline { override var listener: Timeline.Listener? = null set(value) { field = value - listener?.onUpdated(snapshot()) + backgroundHandler.get()?.post { + val snapshot = snapshot() + mainHandler.post { listener?.onUpdated(snapshot) } + } } private val isStarted = AtomicBoolean(false) - private val handlerThread = AtomicReference() - private val handler = AtomicReference() - private val realm = AtomicReference() - + private val isReady = AtomicBoolean(false) + private val backgroundHandlerThread = AtomicReference() + private val backgroundHandler = AtomicReference() + private val mainHandler = Handler(Looper.getMainLooper()) + private val backgroundRealm = AtomicReference() private val cancelableBag = CancelableBag() + private lateinit var liveEvents: RealmResults - private var prevDisplayIndex: Int = 0 - private var nextDisplayIndex: Int = 0 + + private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN + private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN private val isLive = initialEventId == null private val builtEvents = Collections.synchronizedList(ArrayList()) + private val backwardsPaginationState = AtomicReference(PaginationState()) + private val forwardsPaginationState = AtomicReference(PaginationState()) + private val eventsChangeListener = OrderedRealmCollectionChangeListener> { _, changeSet -> - if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) { - handleInitialLoad() - } else { - changeSet.insertionRanges.forEach { - val (startDisplayIndex, direction) = if (it.startIndex == 0) { - Pair(liveEvents[it.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS) - } else { - Pair(liveEvents[it.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS) - } - insertFromLiveResults(startDisplayIndex, direction, it.length.toLong()) + // TODO HANDLE CHANGES + changeSet.insertionRanges.forEach { range -> + val (startDisplayIndex, direction) = if (range.startIndex == 0) { + Pair(liveEvents[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS) + } else { + Pair(liveEvents[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS) + } + val state = getPaginationState(direction) + if (state.isPaginating) { + // We are getting new items from pagination + paginateInternal(startDisplayIndex, direction, state.requestedCount) + } else { + // We are getting new items from sync + buildTimelineEvents(startDisplayIndex, direction, range.length.toLong()) } } } + // Public methods ****************************************************************************** + override fun paginate(direction: Timeline.Direction, count: Int) { - handler.get()?.post { - if (!hasMoreToLoadLive(direction) && hasReachedEndLive(direction)) { + backgroundHandler.get()?.post { + if (!canPaginate(direction)) { return@post } + Timber.v("Paginate $direction of $count items") val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex - val builtCountItems = insertFromLiveResults(startDisplayIndex, direction, count.toLong()) - if (builtCountItems < count) { - val limit = count - builtCountItems - val token = getTokenLive(direction) ?: return@post - helper.runIfNotRunning(direction.toRequestType()) { executePaginationTask(it, token, direction, limit) } - } + paginateInternal(startDisplayIndex, direction, count) } } override fun start() { if (isStarted.compareAndSet(false, true)) { - val handlerThread = HandlerThread(THREAD_NAME) + Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId") + val handlerThread = HandlerThread(THREAD_NAME + hashCode()) handlerThread.start() val handler = Handler(handlerThread.looper) - this.handlerThread.set(handlerThread) - this.handler.set(handler) + this.backgroundHandlerThread.set(handlerThread) + this.backgroundHandler.set(handler) handler.post { val realm = Realm.getInstance(realmConfiguration) - this.realm.set(realm) - liveEvents = buildEventQuery(realm).findAllAsync() - liveEvents.addChangeListener(eventsChangeListener) + backgroundRealm.set(realm) + clearUnlinkedEvents(realm) + isReady.set(true) + liveEvents = buildEventQuery(realm) + .sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) + .findAll() + .also { it.addChangeListener(eventsChangeListener) } + handleInitialLoad() } } - } override fun dispose() { if (isStarted.compareAndSet(true, false)) { - handler.get()?.post { + Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId") + backgroundHandler.get()?.post { cancelableBag.cancel() liveEvents.removeAllChangeListeners() - realm.getAndSet(null)?.close() - handler.set(null) - handlerThread.getAndSet(null)?.quit() + backgroundRealm.getAndSet(null).also { + it.close() + } + backgroundHandler.set(null) + backgroundHandlerThread.getAndSet(null)?.quit() } } } - override fun snapshot(): List = synchronized(builtEvents) { + override fun hasMoreToLoad(direction: Timeline.Direction): Boolean { + return hasMoreInCache(direction) || !hasReachedEnd(direction) + } + + // Private methods ***************************************************************************** + + private fun hasMoreInCache(direction: Timeline.Direction): Boolean { + val localRealm = Realm.getInstance(realmConfiguration) + val eventEntity = buildEventQuery(localRealm).findFirst(direction) ?: return false + val hasMoreInCache = if (direction == Timeline.Direction.FORWARDS) { + val firstEvent = builtEvents.firstOrNull() ?: return true + firstEvent.displayIndex < eventEntity.displayIndex + } else { + val lastEvent = builtEvents.lastOrNull() ?: return true + lastEvent.displayIndex > eventEntity.displayIndex + } + localRealm.close() + return hasMoreInCache + } + + private fun hasReachedEnd(direction: Timeline.Direction): Boolean { + val localRealm = Realm.getInstance(realmConfiguration) + val currentChunk = findCurrentChunk(localRealm) ?: return false + val hasReachedEnd = if (direction == Timeline.Direction.FORWARDS) { + currentChunk.isLastForward + } else { + val eventEntity = buildEventQuery(localRealm).findFirst(direction) + currentChunk.isLastBackward || eventEntity?.type == EventType.STATE_ROOM_CREATE + } + localRealm.close() + return hasReachedEnd + } + + + /** + * This has to be called on TimelineThread as it access realm live results + */ + private fun paginateInternal(startDisplayIndex: Int, + direction: Timeline.Direction, + count: Int) { + updatePaginationState(direction) { it.copy(requestedCount = count, isPaginating = true) } + val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong()) + if (builtCount < count && !hasReachedEnd(direction)) { + val newRequestedCount = count - builtCount + updatePaginationState(direction) { it.copy(requestedCount = newRequestedCount) } + val fetchingCount = Math.max(MIN_FETCHING_COUNT, newRequestedCount) + executePaginationTask(direction, fetchingCount) + } else { + updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) } + } + } + + private fun snapshot(): List { return builtEvents.toList() } - override fun size(): Int = synchronized(builtEvents) { - return builtEvents.size + private fun canPaginate(direction: Timeline.Direction): Boolean { + return isReady.get() && !getPaginationState(direction).isPaginating && hasMoreToLoad(direction) } - override fun hasReachedEnd(direction: Timeline.Direction): Boolean { - return handler.get()?.postAndWait { - hasReachedEndLive(direction) - } ?: false + private fun getPaginationState(direction: Timeline.Direction): PaginationState { + return when (direction) { + Timeline.Direction.FORWARDS -> forwardsPaginationState.get() + Timeline.Direction.BACKWARDS -> backwardsPaginationState.get() + } } - override fun hasMoreToLoad(direction: Timeline.Direction): Boolean { - return handler.get()?.postAndWait { - hasMoreToLoadLive(direction) - } ?: false + private fun updatePaginationState(direction: Timeline.Direction, update: (PaginationState) -> PaginationState) { + val stateReference = when (direction) { + Timeline.Direction.FORWARDS -> forwardsPaginationState + Timeline.Direction.BACKWARDS -> backwardsPaginationState + } + val currentValue = stateReference.get() + val newValue = update(currentValue) + stateReference.set(newValue) } /** * This has to be called on TimelineThread as it access realm live results */ - private fun handleInitialLoad() = synchronized(builtEvents) { + private fun handleInitialLoad() { + var shouldFetchInitialEvent = false val initialDisplayIndex = if (isLive) { liveEvents.firstOrNull()?.displayIndex } else { - liveEvents.where().equalTo(EventEntityFields.EVENT_ID, initialEventId).findFirst()?.displayIndex - } ?: 0 + val initialEvent = liveEvents.where().equalTo(EventEntityFields.EVENT_ID, initialEventId).findFirst() + shouldFetchInitialEvent = initialEvent == null + initialEvent?.displayIndex + } ?: DISPLAY_INDEX_UNKNOWN + prevDisplayIndex = initialDisplayIndex nextDisplayIndex = initialDisplayIndex - val count = Math.min(INITIAL_LOAD_SIZE, liveEvents.size).toLong() - if (count == 0L) { - return@synchronized - } - if (isLive) { - insertFromLiveResults(initialDisplayIndex, Timeline.Direction.BACKWARDS, count) + if (initialEventId != null && shouldFetchInitialEvent) { + fetchEvent(initialEventId) } else { - val forwardCount = count / 2L - val backwardCount = count - forwardCount - insertFromLiveResults(initialDisplayIndex, Timeline.Direction.BACKWARDS, backwardCount) - insertFromLiveResults(initialDisplayIndex, Timeline.Direction.BACKWARDS, forwardCount) + val count = Math.min(INITIAL_LOAD_SIZE, liveEvents.size) + if (isLive) { + paginate(Timeline.Direction.BACKWARDS, count) + } else { + paginate(Timeline.Direction.FORWARDS, count / 2) + paginate(Timeline.Direction.BACKWARDS, count / 2) + } } } - private fun executePaginationTask(requestCallback: PagingRequestHelper.Request.Callback, - from: String, - direction: Timeline.Direction, - limit: Int) { - + /** + * This has to be called on TimelineThread as it access realm live results + */ + private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { + val token = getTokenLive(direction) ?: return val params = PaginationTask.Params(roomId = roomId, - from = from, + from = token, direction = direction.toPaginationDirection(), limit = limit) + Timber.v("Should fetch $limit items $direction") paginationTask.configureWith(params) .enableRetry() .dispatchTo(object : MatrixCallback { override fun onSuccess(data: TokenChunkEventPersistor.Result) { - requestCallback.recordSuccess() - if (data == TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE) { - paginate(direction, limit) + if (data == TokenChunkEventPersistor.Result.SUCCESS) { + Timber.v("Success fetching $limit items $direction from pagination request") + } else { + // Database won't be updated, so we force pagination request + backgroundHandler.get()?.post { + executePaginationTask(direction, limit) + } } } override fun onFailure(failure: Throwable) { - requestCallback.recordFailure(failure) + Timber.v("Failure fetching $limit items $direction from pagination request") } }) .executeBy(taskExecutor) @@ -209,36 +296,12 @@ internal class DefaultTimeline( /** * This has to be called on TimelineThread as it access realm live results */ + private fun getTokenLive(direction: Timeline.Direction): String? { val chunkEntity = getLiveChunk() ?: return null return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken } - /** - * This has to be called on TimelineThread as it access realm live results - */ - private fun hasReachedEndLive(direction: Timeline.Direction): Boolean { - val liveChunk = getLiveChunk() ?: return false - return if (direction == Timeline.Direction.FORWARDS) { - liveChunk.isLastForward - } else { - liveChunk.isLastBackward || liveEvents.lastOrNull()?.type == EventType.STATE_ROOM_CREATE - } - } - - /** - * This has to be called on TimelineThread as it access realm live results - */ - private fun hasMoreToLoadLive(direction: Timeline.Direction): Boolean { - if (liveEvents.isEmpty()) { - return true - } - return if (direction == Timeline.Direction.FORWARDS) { - builtEvents.firstOrNull()?.displayIndex != liveEvents.firstOrNull()?.displayIndex - } else { - builtEvents.lastOrNull()?.displayIndex != liveEvents.lastOrNull()?.displayIndex - } - } /** * This has to be called on TimelineThread as it access realm live results @@ -251,11 +314,11 @@ internal class DefaultTimeline( * This has to be called on TimelineThread as it access realm live results * @return number of items who have been added */ - private fun insertFromLiveResults(startDisplayIndex: Int, - direction: Timeline.Direction, - count: Long): Int = synchronized(builtEvents) { + private fun buildTimelineEvents(startDisplayIndex: Int, + direction: Timeline.Direction, + count: Long): Int { if (count < 1) { - throw java.lang.IllegalStateException("You should provide a count superior to 0") + return 0 } val offsetResults = getOffsetResults(startDisplayIndex, direction, count) if (offsetResults.isEmpty()) { @@ -272,7 +335,9 @@ internal class DefaultTimeline( val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size builtEvents.add(position, timelineEvent) } - listener?.onUpdated(snapshot()) + Timber.v("Built ${offsetResults.size} items from db") + val snapshot = snapshot() + mainHandler.post { listener?.onUpdated(snapshot) } return offsetResults.size } @@ -292,11 +357,15 @@ internal class DefaultTimeline( .sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) .greaterThanOrEqualTo(EventEntityFields.DISPLAY_INDEX, startDisplayIndex) } - return offsetQuery.limit(count).findAll() + return offsetQuery + .filterAllowedTypes() + .limit(count) + .findAll() } + private fun buildEventQuery(realm: Realm): RealmQuery { - val query = if (initialEventId == null) { + return if (initialEventId == null) { EventEntity .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY) .equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST_FORWARD}", true) @@ -305,30 +374,56 @@ internal class DefaultTimeline( .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH) .`in`("${EventEntityFields.CHUNK}.${ChunkEntityFields.EVENTS.EVENT_ID}", arrayOf(initialEventId)) } - query.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) - return query - } - private fun Handler.postAndWait(runnable: () -> T): T { - val lock = CountDownLatch(1) - val atomicReference = AtomicReference() - post { - val result = runnable() - atomicReference.set(result) - lock.countDown() + private fun findCurrentChunk(realm: Realm): ChunkEntity? { + return if (initialEventId == null) { + ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) + } else { + ChunkEntity.findIncludingEvent(realm, initialEventId) } - lock.await() - return atomicReference.get() } - private fun Timeline.Direction.toRequestType(): PagingRequestHelper.RequestType { - return if (this == Timeline.Direction.BACKWARDS) PagingRequestHelper.RequestType.BEFORE else PagingRequestHelper.RequestType.AFTER + private fun clearUnlinkedEvents(realm: Realm) { + realm.executeTransaction { + val unlinkedChunks = ChunkEntity + .where(it, roomId = roomId) + .equalTo(ChunkEntityFields.EVENTS.IS_UNLINKED, true) + .findAll() + unlinkedChunks.deleteAllFromRealm() + } } - //Todo : remove that + private fun fetchEvent(eventId: String) { + val params = GetContextOfEventTask.Params(roomId, eventId) + contextOfEventTask.configureWith(params).executeBy(taskExecutor) + } + +// Extension methods *************************************************************************** + private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS } + private fun RealmQuery.findFirst(direction: Timeline.Direction): EventEntity? { + return if (direction == Timeline.Direction.FORWARDS) { + sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) + } else { + sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) + } + .filterAllowedTypes() + .findFirst() + } + + private fun RealmQuery.filterAllowedTypes(): RealmQuery { + if (allowedTypes != null) { + `in`(EventEntityFields.TYPE, allowedTypes.toTypedArray()) + } + return this + } } + +private data class PaginationState( + val isPaginating: Boolean = false, + val requestedCount: Int = 0 +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index f112f078..9700d712 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -19,68 +19,18 @@ package im.vector.matrix.android.internal.session.room.timeline import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineService -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.task.TaskExecutor -import im.vector.matrix.android.internal.task.configureWith -import im.vector.matrix.android.internal.util.PagingRequestHelper -import im.vector.matrix.android.internal.util.tryTransactionAsync - -private const val EVENT_NOT_FOUND_INDEX = -1 internal class DefaultTimelineService(private val roomId: String, private val monarchy: Monarchy, private val taskExecutor: TaskExecutor, private val contextOfEventTask: GetContextOfEventTask, private val timelineEventFactory: TimelineEventFactory, - private val paginationTask: PaginationTask, - private val helper: PagingRequestHelper + private val paginationTask: PaginationTask ) : TimelineService { - override fun createTimeline(eventId: String?): Timeline { - return DefaultTimeline(roomId, eventId, monarchy.realmConfiguration, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask, helper) + override fun createTimeline(eventId: String?, allowedTypes: List?): Timeline { + return DefaultTimeline(roomId, eventId, monarchy.realmConfiguration, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask, allowedTypes) } - // PRIVATE FUNCTIONS *************************************************************************** - - private fun getInitialLoadKey(eventId: String?): Int { - var initialLoadKey = 0 - if (eventId != null) { - val indexOfEvent = indexOfEvent(eventId) - if (indexOfEvent == EVENT_NOT_FOUND_INDEX) { - fetchEvent(eventId) - } else { - initialLoadKey = indexOfEvent - } - } - return initialLoadKey - } - - - private fun fetchEvent(eventId: String) { - val params = GetContextOfEventTask.Params(roomId, eventId) - contextOfEventTask.configureWith(params).executeBy(taskExecutor) - } - - private fun clearUnlinkedEvents() { - monarchy.tryTransactionAsync { realm -> - val unlinkedEvents = EventEntity - .where(realm, roomId = roomId) - .equalTo(EventEntityFields.IS_UNLINKED, true) - .findAll() - unlinkedEvents.deleteAllFromRealm() - } - } - - private fun indexOfEvent(eventId: String): Int { - var displayIndex = EVENT_NOT_FOUND_INDEX - monarchy.doWithRealm { - displayIndex = EventEntity.where(it, eventId = eventId).findFirst()?.displayIndex - ?: EVENT_NOT_FOUND_INDEX - } - return displayIndex - } - - } \ No newline at end of file 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 deleted file mode 100644 index aeec3a3f..00000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineBoundaryCallback.kt +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.matrix.android.internal.session.room.timeline - -import androidx.lifecycle.LiveData -import androidx.paging.PagedList -import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.session.room.timeline.TimelineEvent -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.query.findIncludingEvent -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.task.configureWith -import im.vector.matrix.android.internal.util.PagingRequestHelper - -internal class TimelineBoundaryCallback(private val roomId: String, - private val taskExecutor: TaskExecutor, - private val paginationTask: PaginationTask, - private val monarchy: Monarchy, - private val helper: PagingRequestHelper -) : PagedList.BoundaryCallback() { - - var limit = 30 - - val status = object : LiveData() { - - init { - value = PagingRequestHelper.StatusReport.createDefault() - } - - val listener = PagingRequestHelper.Listener { postValue(it) } - - override fun onActive() { - helper.addListener(listener) - } - - override fun onInactive() { - helper.removeListener(listener) - } - } - - override fun onZeroItemsLoaded() { - // actually, it's not possible - } - - override fun onItemAtEndLoaded(itemAtEnd: TimelineEvent) { - val token = itemAtEnd.root.eventId?.let { getToken(it, PaginationDirection.BACKWARDS) } - ?: return - - helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) { - executePaginationTask(it, token, PaginationDirection.BACKWARDS) - } - } - - override fun onItemAtFrontLoaded(itemAtFront: TimelineEvent) { - val token = itemAtFront.root.eventId?.let { getToken(it, PaginationDirection.FORWARDS) } - ?: return - - helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE) { - executePaginationTask(it, token, PaginationDirection.FORWARDS) - } - } - - private fun getToken(eventId: String, direction: PaginationDirection): String? { - var token: String? = null - monarchy.doWithRealm { realm -> - val chunkEntity = ChunkEntity.findIncludingEvent(realm, eventId) - token = if (direction == PaginationDirection.FORWARDS) chunkEntity?.nextToken else chunkEntity?.prevToken - } - return token - } - - private fun executePaginationTask(requestCallback: PagingRequestHelper.Request.Callback, - from: String, - direction: PaginationDirection) { - - val params = PaginationTask.Params(roomId = roomId, - from = from, - direction = direction, - limit = limit) - - paginationTask.configureWith(params) - .enableRetry() - .dispatchTo(object : MatrixCallback { - override fun onSuccess(data: TokenChunkEventPersistor.Result) { - requestCallback.recordSuccess() - } - - override fun onFailure(failure: Throwable) { - requestCallback.recordFailure(failure) - } - }) - .executeBy(taskExecutor) - } - -} 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 187833ef..3ac88211 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 @@ -18,12 +18,7 @@ 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.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.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 @@ -32,6 +27,7 @@ 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 +import timber.log.Timber internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { @@ -47,8 +43,10 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { return monarchy .tryTransactionSync { realm -> + Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction") + val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) val nextToken: String? val prevToken: String? @@ -68,16 +66,16 @@ 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) } if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) { currentChunk.isLastBackward = true } else { + Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken}-${currentChunk.prevToken}") currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked()) - // Then we merge chunks if needed if (currentChunk != prevChunk && prevChunk != null) { currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk) @@ -111,6 +109,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { otherChunk: ChunkEntity): ChunkEntity { // We always merge the bottom chunk into top chunk, so we are always merging backwards + Timber.v("Merge ${currentChunk.prevToken}-${currentChunk.nextToken} with ${otherChunk.prevToken}-${otherChunk.nextToken}") return if (direction == PaginationDirection.BACKWARDS) { currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS) roomEntity.deleteOnCascade(otherChunk) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/PagingRequestHelper.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/PagingRequestHelper.java deleted file mode 100644 index 45066740..00000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/PagingRequestHelper.java +++ /dev/null @@ -1,530 +0,0 @@ - -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package im.vector.matrix.android.internal.util; - -import androidx.annotation.AnyThread; -import androidx.annotation.GuardedBy; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import java.util.Arrays; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * A helper class for {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}s and - * {@link androidx.paging.DataSource}s to help with tracking network requests. - *

- * It is designed to support 3 types of requests, {@link RequestType#INITIAL INITIAL}, - * {@link RequestType#BEFORE BEFORE} and {@link RequestType#AFTER AFTER} and runs only 1 request - * for each of them via {@link #runIfNotRunning(RequestType, Request)}. - *

- * It tracks a {@link Status} and an {@code error} for each {@link RequestType}. - *

- * A sample usage of this class to limit requests looks like this: - *

- * class PagingBoundaryCallback extends PagedList.BoundaryCallback<MyItem> {
- *     // TODO replace with an executor from your application
- *     Executor executor = Executors.newSingleThreadExecutor();
- *     PagingRequestHelper helper = new PagingRequestHelper(executor);
- *     // imaginary API service, using Retrofit
- *     MyApi api;
- *
- *     {@literal @}Override
- *     public void onItemAtFrontLoaded({@literal @}NonNull MyItem itemAtFront) {
- *         helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE,
- *                 helperCallback -> api.getTopBefore(itemAtFront.getName(), 10).enqueue(
- *                         new Callback<ApiResponse>() {
- *                             {@literal @}Override
- *                             public void onResponse(Call<ApiResponse> call,
- *                                     Response<ApiResponse> response) {
- *                                 // TODO insert new records into database
- *                                 helperCallback.recordSuccess();
- *                             }
- *
- *                             {@literal @}Override
- *                             public void onFailure(Call<ApiResponse> call, Throwable t) {
- *                                 helperCallback.recordFailure(t);
- *                             }
- *                         }));
- *     }
- *
- *     {@literal @}Override
- *     public void onItemAtEndLoaded({@literal @}NonNull MyItem itemAtEnd) {
- *         helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER,
- *                 helperCallback -> api.getTopBefore(itemAtEnd.getName(), 10).enqueue(
- *                         new Callback<ApiResponse>() {
- *                             {@literal @}Override
- *                             public void onResponse(Call<ApiResponse> call,
- *                                     Response<ApiResponse> response) {
- *                                 // TODO insert new records into database
- *                                 helperCallback.recordSuccess();
- *                             }
- *
- *                             {@literal @}Override
- *                             public void onFailure(Call<ApiResponse> call, Throwable t) {
- *                                 helperCallback.recordFailure(t);
- *                             }
- *                         }));
- *     }
- * }
- * 
- *

- * The helper provides an API to observe combined request status, which can be reported back to the - * application based on your business rules. - *

- * MutableLiveData<PagingRequestHelper.Status> combined = new MutableLiveData<>();
- * helper.addListener(status -> {
- *     // merge multiple states per request type into one, or dispatch separately depending on
- *     // your application logic.
- *     if (status.hasRunning()) {
- *         combined.postValue(PagingRequestHelper.Status.RUNNING);
- *     } else if (status.hasError()) {
- *         // can also obtain the error via {@link StatusReport#getErrorFor(RequestType)}
- *         combined.postValue(PagingRequestHelper.Status.FAILED);
- *     } else {
- *         combined.postValue(PagingRequestHelper.Status.SUCCESS);
- *     }
- * });
- * 
- */ -// THIS class is likely to be moved into the library in a future release. Feel free to copy it -// from this sample. -public class PagingRequestHelper { - private final Object mLock = new Object(); - private final Executor mRetryService; - @GuardedBy("mLock") - private final RequestQueue[] mRequestQueues = new RequestQueue[] - {new RequestQueue(RequestType.INITIAL), - new RequestQueue(RequestType.BEFORE), - new RequestQueue(RequestType.AFTER)}; - @NonNull - final CopyOnWriteArrayList mListeners = new CopyOnWriteArrayList<>(); - - /** - * Creates a new PagingRequestHelper with the given {@link Executor} which is used to run - * retry actions. - * - * @param retryService The {@link Executor} that can run the retry actions. - */ - public PagingRequestHelper(@NonNull Executor retryService) { - mRetryService = retryService; - } - - /** - * Adds a new listener that will be notified when any request changes {@link Status state}. - * - * @param listener The listener that will be notified each time a request's status changes. - * @return True if it is added, false otherwise (e.g. it already exists in the list). - */ - @AnyThread - public boolean addListener(@NonNull Listener listener) { - return mListeners.add(listener); - } - - /** - * Removes the given listener from the listeners list. - * - * @param listener The listener that will be removed. - * @return True if the listener is removed, false otherwise (e.g. it never existed) - */ - public boolean removeListener(@NonNull Listener listener) { - return mListeners.remove(listener); - } - - /** - * Runs the given {@link Request} if no other requests in the given request type is already - * running. - *

- * If run, the request will be run in the current thread. - * - * @param type The type of the request. - * @param request The request to run. - * @return True if the request is run, false otherwise. - */ - @SuppressWarnings("WeakerAccess") - @AnyThread - public boolean runIfNotRunning(@NonNull RequestType type, @NonNull Request request) { - boolean hasListeners = !mListeners.isEmpty(); - StatusReport report = null; - synchronized (mLock) { - RequestQueue queue = mRequestQueues[type.ordinal()]; - if (queue.mRunning != null) { - return false; - } - queue.mRunning = request; - queue.mStatus = Status.RUNNING; - queue.mFailed = null; - queue.mLastError = null; - if (hasListeners) { - report = prepareStatusReportLocked(); - } - } - if (report != null) { - dispatchReport(report); - } - final RequestWrapper wrapper = new RequestWrapper(request, this, type); - wrapper.run(); - return true; - } - - @GuardedBy("mLock") - private StatusReport prepareStatusReportLocked() { - Throwable[] errors = new Throwable[]{ - mRequestQueues[0].mLastError, - mRequestQueues[1].mLastError, - mRequestQueues[2].mLastError - }; - return new StatusReport( - getStatusForLocked(RequestType.INITIAL), - getStatusForLocked(RequestType.BEFORE), - getStatusForLocked(RequestType.AFTER), - errors - ); - } - - @GuardedBy("mLock") - private Status getStatusForLocked(RequestType type) { - return mRequestQueues[type.ordinal()].mStatus; - } - - @AnyThread - @VisibleForTesting - void recordResult(@NonNull RequestWrapper wrapper, @Nullable Throwable throwable) { - StatusReport report = null; - final boolean success = throwable == null; - boolean hasListeners = !mListeners.isEmpty(); - synchronized (mLock) { - RequestQueue queue = mRequestQueues[wrapper.mType.ordinal()]; - queue.mRunning = null; - queue.mLastError = throwable; - if (success) { - queue.mFailed = null; - queue.mStatus = Status.SUCCESS; - } else { - queue.mFailed = wrapper; - queue.mStatus = Status.FAILED; - } - if (hasListeners) { - report = prepareStatusReportLocked(); - } - } - if (report != null) { - dispatchReport(report); - } - } - - private void dispatchReport(StatusReport report) { - for (Listener listener : mListeners) { - listener.onStatusChange(report); - } - } - - /** - * Retries all failed requests. - * - * @return True if any request is retried, false otherwise. - */ - public boolean retryAllFailed() { - final RequestWrapper[] toBeRetried = new RequestWrapper[RequestType.values().length]; - boolean retried = false; - synchronized (mLock) { - for (int i = 0; i < RequestType.values().length; i++) { - toBeRetried[i] = mRequestQueues[i].mFailed; - mRequestQueues[i].mFailed = null; - } - } - for (RequestWrapper failed : toBeRetried) { - if (failed != null) { - failed.retry(mRetryService); - retried = true; - } - } - return retried; - } - - static class RequestWrapper implements Runnable { - @NonNull - final Request mRequest; - @NonNull - final PagingRequestHelper mHelper; - @NonNull - final RequestType mType; - - RequestWrapper(@NonNull Request request, @NonNull PagingRequestHelper helper, - @NonNull RequestType type) { - mRequest = request; - mHelper = helper; - mType = type; - } - - @Override - public void run() { - mRequest.run(new Request.Callback(this, mHelper)); - } - - void retry(Executor service) { - service.execute(new Runnable() { - @Override - public void run() { - mHelper.runIfNotRunning(mType, mRequest); - } - }); - } - } - - /** - * Runner class that runs a request tracked by the {@link PagingRequestHelper}. - *

- * When a request is invoked, it must call one of {@link Callback#recordFailure(Throwable)} - * or {@link Callback#recordSuccess()} once and only once. This call - * can be made any time. Until that method call is made, {@link PagingRequestHelper} will - * consider the request is running. - */ - @FunctionalInterface - public interface Request { - /** - * Should run the request and call the given {@link Callback} with the result of the - * request. - * - * @param callback The callback that should be invoked with the result. - */ - void run(Callback callback); - - /** - * Callback class provided to the {@link #run(Callback)} method to report the result. - */ - class Callback { - private final AtomicBoolean mCalled = new AtomicBoolean(); - private final RequestWrapper mWrapper; - private final PagingRequestHelper mHelper; - - Callback(RequestWrapper wrapper, PagingRequestHelper helper) { - mWrapper = wrapper; - mHelper = helper; - } - - /** - * Call this method when the request succeeds and new data is fetched. - */ - @SuppressWarnings("unused") - public final void recordSuccess() { - if (mCalled.compareAndSet(false, true)) { - mHelper.recordResult(mWrapper, null); - } else { - throw new IllegalStateException( - "already called recordSuccess or recordFailure"); - } - } - - /** - * Call this method with the failure message and the request can be retried via - * {@link #retryAllFailed()}. - * - * @param throwable The error that occured while carrying out the request. - */ - @SuppressWarnings("unused") - public final void recordFailure(@NonNull Throwable throwable) { - //noinspection ConstantConditions - if (throwable == null) { - throw new IllegalArgumentException("You must provide a throwable describing" - + " the error to record the failure"); - } - if (mCalled.compareAndSet(false, true)) { - mHelper.recordResult(mWrapper, throwable); - } else { - throw new IllegalStateException( - "already called recordSuccess or recordFailure"); - } - } - } - } - - /** - * Data class that holds the information about the current status of the ongoing requests - * using this helper. - */ - public static final class StatusReport { - /** - * Status of the latest request that were submitted with {@link RequestType#INITIAL}. - */ - @NonNull - public final Status initial; - /** - * Status of the latest request that were submitted with {@link RequestType#BEFORE}. - */ - @NonNull - public final Status before; - /** - * Status of the latest request that were submitted with {@link RequestType#AFTER}. - */ - @NonNull - public final Status after; - @NonNull - private final Throwable[] mErrors; - - public static StatusReport createDefault() { - final Throwable[] errors = {}; - return new StatusReport(Status.SUCCESS, Status.SUCCESS, Status.SUCCESS, errors); - } - - StatusReport(@NonNull Status initial, @NonNull Status before, @NonNull Status after, - @NonNull Throwable[] errors) { - this.initial = initial; - this.before = before; - this.after = after; - this.mErrors = errors; - } - - /** - * Convenience method to check if there are any running requests. - * - * @return True if there are any running requests, false otherwise. - */ - public boolean hasRunning() { - return initial == Status.RUNNING - || before == Status.RUNNING - || after == Status.RUNNING; - } - - /** - * Convenience method to check if there are any requests that resulted in an error. - * - * @return True if there are any requests that finished with error, false otherwise. - */ - public boolean hasError() { - return initial == Status.FAILED - || before == Status.FAILED - || after == Status.FAILED; - } - - /** - * Returns the error for the given request type. - * - * @param type The request type for which the error should be returned. - * @return The {@link Throwable} returned by the failing request with the given type or - * {@code null} if the request for the given type did not fail. - */ - @Nullable - public Throwable getErrorFor(@NonNull RequestType type) { - return mErrors[type.ordinal()]; - } - - @Override - public String toString() { - return "StatusReport{" - + "initial=" + initial - + ", before=" + before - + ", after=" + after - + ", mErrors=" + Arrays.toString(mErrors) - + '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - StatusReport that = (StatusReport) o; - if (initial != that.initial) return false; - if (before != that.before) return false; - if (after != that.after) return false; - // Probably incorrect - comparing Object[] arrays with Arrays.equals - return Arrays.equals(mErrors, that.mErrors); - } - - @Override - public int hashCode() { - int result = initial.hashCode(); - result = 31 * result + before.hashCode(); - result = 31 * result + after.hashCode(); - result = 31 * result + Arrays.hashCode(mErrors); - return result; - } - } - - /** - * Listener interface to get notified by request status changes. - */ - public interface Listener { - /** - * Called when the status for any of the requests has changed. - * - * @param report The current status report that has all the information about the requests. - */ - void onStatusChange(@NonNull StatusReport report); - } - - /** - * Represents the status of a Request for each {@link RequestType}. - */ - public enum Status { - /** - * There is current a running request. - */ - RUNNING, - /** - * The last request has succeeded or no such requests have ever been run. - */ - SUCCESS, - /** - * The last request has failed. - */ - FAILED - } - - /** - * Available request types. - */ - public enum RequestType { - /** - * Corresponds to an initial request made to a {@link androidx.paging.DataSource} or the empty state for - * a {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}. - */ - INITIAL, - /** - * Corresponds to the {@code loadBefore} calls in {@link androidx.paging.DataSource} or - * {@code onItemAtFrontLoaded} in - * {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}. - */ - BEFORE, - /** - * Corresponds to the {@code loadAfter} calls in {@link androidx.paging.DataSource} or - * {@code onItemAtEndLoaded} in - * {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}. - */ - AFTER - } - - class RequestQueue { - @NonNull - final RequestType mRequestType; - @Nullable - RequestWrapper mFailed; - @Nullable - Request mRunning; - @Nullable - Throwable mLastError; - @NonNull - Status mStatus = Status.SUCCESS; - - RequestQueue(@NonNull RequestType requestType) { - mRequestType = requestType; - } - } -} \ No newline at end of file From a6366e47fedb06a93144b2911616d6f89752035c Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 28 Mar 2019 12:00:45 +0100 Subject: [PATCH 07/12] Timeline : change some database details to make it faster --- .../database/helper/ChunkEntityHelper.kt | 16 ++-- .../internal/database/model/ChunkEntity.kt | 6 +- .../android/internal/session/SessionModule.kt | 7 +- .../session/room/RoomSummaryUpdater.kt | 74 ------------------- .../room/members/RoomDisplayNameResolver.kt | 16 ++-- .../internal/session/sync/RoomSyncHandler.kt | 50 +++++++++---- .../internal/session/sync/SyncModule.kt | 2 +- .../session/sync/SyncResponseHandler.kt | 22 +++--- 8 files changed, 76 insertions(+), 117 deletions(-) delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt 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 e352e847..9a9e4b7c 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 @@ -89,16 +89,20 @@ internal fun ChunkEntity.add(roomId: String, var currentDisplayIndex = lastDisplayIndex(direction, 0) if (direction == PaginationDirection.FORWARDS) { currentDisplayIndex += 1 + forwardsDisplayIndex = currentDisplayIndex } else { currentDisplayIndex -= 1 + backwardsDisplayIndex = currentDisplayIndex } var currentStateIndex = lastStateIndex(direction, defaultValue = stateIndexOffset) if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(event.type)) { currentStateIndex += 1 + forwardsStateIndex = currentStateIndex } else if (direction == PaginationDirection.BACKWARDS && events.isNotEmpty()) { val lastEventType = events.last()?.type ?: "" if (EventType.isStateEvent(lastEventType)) { currentStateIndex -= 1 + backwardsStateIndex = currentStateIndex } } val eventEntity = event.toEntity(roomId).apply { @@ -119,14 +123,14 @@ private fun ChunkEntity.assertIsManaged() { internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findFirst()?.displayIndex - PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING).findFirst()?.displayIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> forwardsDisplayIndex + PaginationDirection.BACKWARDS -> backwardsDisplayIndex + } ?: defaultValue } 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 -> forwardsStateIndex + PaginationDirection.BACKWARDS -> backwardsStateIndex + } ?: defaultValue } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt index dd3520e9..6f34fb03 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt @@ -26,7 +26,11 @@ internal open class ChunkEntity(@Index var prevToken: String? = null, @Index var nextToken: String? = null, var events: RealmList = RealmList(), @Index var isLastForward: Boolean = false, - @Index var isLastBackward: Boolean = false + @Index var isLastBackward: Boolean = false, + var backwardsDisplayIndex: Int? = null, + var forwardsDisplayIndex: Int? = null, + var backwardsStateIndex: Int? = null, + var forwardsStateIndex: Int? = null ) : RealmObject() { @LinkingObjects("chunks") 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 5bbbc6cb..510325fa 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 @@ -29,13 +29,11 @@ import im.vector.matrix.android.internal.session.group.DefaultGroupService import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater import im.vector.matrix.android.internal.session.room.DefaultRoomService 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.session.room.prune.EventsPruner import im.vector.matrix.android.internal.session.user.DefaultUserService import im.vector.matrix.android.internal.session.user.UserEntityUpdater -import im.vector.matrix.android.internal.session.user.UserModule import im.vector.matrix.android.internal.util.md5 import io.realm.RealmConfiguration import org.koin.dsl.module.module @@ -84,7 +82,7 @@ internal class SessionModule(private val sessionParams: SessionParams) { } scope(DefaultSession.SCOPE) { - RoomDisplayNameResolver(get(), get(), sessionParams.credentials) + RoomDisplayNameResolver(get(), get(), get(), sessionParams.credentials) } scope(DefaultSession.SCOPE) { @@ -112,11 +110,10 @@ internal class SessionModule(private val sessionParams: SessionParams) { } scope(DefaultSession.SCOPE) { - val roomSummaryUpdater = RoomSummaryUpdater(get(), get(), get(), get(), sessionParams.credentials) val groupSummaryUpdater = GroupSummaryUpdater(get()) val eventsPruner = EventsPruner(get()) val userEntityUpdater = UserEntityUpdater(get(), get(), get()) - listOf(roomSummaryUpdater, groupSummaryUpdater, eventsPruner, userEntityUpdater) + listOf(groupSummaryUpdater, eventsPruner, userEntityUpdater) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt deleted file mode 100644 index f847237a..00000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.matrix.android.internal.session.room - -import android.content.Context -import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.auth.data.Credentials -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.RoomTopicContent -import im.vector.matrix.android.internal.database.RealmLiveEntityObserver -import im.vector.matrix.android.internal.database.mapper.asDomain -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.database.model.RoomSummaryEntity -import im.vector.matrix.android.internal.database.query.latestEvent -import im.vector.matrix.android.internal.database.query.prev -import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver -import im.vector.matrix.android.internal.session.room.members.RoomMembers -import io.realm.Realm -import io.realm.kotlin.createObject - -internal class RoomSummaryUpdater(monarchy: Monarchy, - private val roomDisplayNameResolver: RoomDisplayNameResolver, - private val roomAvatarResolver: RoomAvatarResolver, - private val context: Context, - private val credentials: Credentials -) : RealmLiveEntityObserver(monarchy) { - - override val query = Monarchy.Query { RoomEntity.where(it) } - - override fun processChanges(inserted: List, updated: List, deleted: List) { - val rooms = (inserted + updated).map { it.roomId } - monarchy.writeAsync { realm -> - rooms.forEach { updateRoom(realm, it) } - } - } - - private fun updateRoom(realm: Realm, roomId: String?) { - if (roomId == null) { - return - } - val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) - - val lastEvent = EventEntity.latestEvent(realm, roomId, includedTypes = listOf(EventType.MESSAGE)) - val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain() - - val otherRoomMembers = RoomMembers(realm, roomId).getLoaded().filterKeys { it != credentials.userId } - - roomSummary.displayName = roomDisplayNameResolver.resolve(context, roomId).toString() - roomSummary.avatarUrl = roomAvatarResolver.resolve(roomId) - roomSummary.topic = lastTopicEvent?.content.toModel()?.topic - roomSummary.lastMessage = lastEvent - roomSummary.otherMemberIds.clear() - roomSummary.otherMemberIds.addAll(otherRoomMembers.keys) - } - -} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomDisplayNameResolver.kt index f39dd300..7f19762c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomDisplayNameResolver.kt @@ -36,7 +36,8 @@ import im.vector.matrix.android.internal.database.query.where /** * This class computes room display name */ -internal class RoomDisplayNameResolver(private val monarchy: Monarchy, +internal class RoomDisplayNameResolver(private val context: Context, + private val monarchy: Monarchy, private val roomMemberDisplayNameResolver: RoomMemberDisplayNameResolver, private val credentials: Credentials ) { @@ -44,11 +45,10 @@ internal class RoomDisplayNameResolver(private val monarchy: Monarchy, /** * Compute the room display name * - * @param context * @param roomId: the roomId to resolve the name of. * @return the room display name */ - fun resolve(context: Context, roomId: String): CharSequence { + fun resolve(roomId: String): CharSequence { // this algorithm is the one defined in // https://github.com/matrix-org/matrix-js-sdk/blob/develop/lib/models/room.js#L617 // calculateRoomName(room, userId) @@ -111,16 +111,16 @@ internal class RoomDisplayNameResolver(private val monarchy: Monarchy, val member1 = memberIds[0] val member2 = memberIds[1] name = context.getString(R.string.room_displayname_two_members, - roomMemberDisplayNameResolver.resolve(member1, otherRoomMembers), - roomMemberDisplayNameResolver.resolve(member2, otherRoomMembers) + roomMemberDisplayNameResolver.resolve(member1, otherRoomMembers), + roomMemberDisplayNameResolver.resolve(member2, otherRoomMembers) ) } else -> { val member = memberIds[0] name = context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members, - roomMembers.getNumberOfJoinedMembers() - 1, - roomMemberDisplayNameResolver.resolve(member, otherRoomMembers), - roomMembers.getNumberOfJoinedMembers() - 1) + roomMembers.getNumberOfJoinedMembers() - 1, + roomMemberDisplayNameResolver.resolve(member, otherRoomMembers), + roomMembers.getNumberOfJoinedMembers() - 1) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index 51044646..cc29f99e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -17,20 +17,29 @@ package im.vector.matrix.android.internal.session.sync import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.auth.data.Credentials 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.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.MyMembership +import im.vector.matrix.android.api.session.room.model.RoomTopicContent import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent 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.lastStateIndex +import im.vector.matrix.android.internal.database.mapper.asDomain 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.RoomEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom +import im.vector.matrix.android.internal.database.query.latestEvent +import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.session.room.RoomAvatarResolver +import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver +import im.vector.matrix.android.internal.session.room.members.RoomMembers import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync import im.vector.matrix.android.internal.session.sync.model.RoomSync @@ -44,6 +53,9 @@ import io.realm.kotlin.createObject internal class RoomSyncHandler(private val monarchy: Monarchy, private val readReceiptHandler: ReadReceiptHandler, + private val roomDisplayNameResolver: RoomDisplayNameResolver, + private val roomAvatarResolver: RoomAvatarResolver, + private val credentials: Credentials, private val roomTagHandler: RoomTagHandler) { sealed class HandlingStrategy { @@ -51,6 +63,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, data class INVITED(val data: Map) : HandlingStrategy() data class LEFT(val data: Map) : HandlingStrategy() } + fun handle(roomsSyncResponse: RoomsSyncResponse) { monarchy.runTransactionSync { realm -> handleRoomSync(realm, RoomSyncHandler.HandlingStrategy.JOINED(roomsSyncResponse.join)) @@ -107,9 +120,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, roomEntity.addOrUpdate(chunkEntity) } - if (roomSync.summary != null) { - handleRoomSummary(realm, roomId, roomSync.summary) - } + handleRoomSummary(realm, roomId, roomSync.summary) if (roomSync.unreadNotifications != null) { handleUnreadNotifications(realm, roomId, roomSync.unreadNotifications) @@ -171,20 +182,33 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, private fun handleRoomSummary(realm: Realm, roomId: String, - roomSummary: RoomSyncSummary) { + roomSummary: RoomSyncSummary?) { val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() ?: RoomSummaryEntity(roomId) - if (roomSummary.heroes.isNotEmpty()) { - roomSummaryEntity.heroes.clear() - roomSummaryEntity.heroes.addAll(roomSummary.heroes) - } - if (roomSummary.invitedMembersCount != null) { - roomSummaryEntity.invitedMembersCount = roomSummary.invitedMembersCount - } - if (roomSummary.joinedMembersCount != null) { - roomSummaryEntity.joinedMembersCount = roomSummary.joinedMembersCount + val lastEvent = EventEntity.latestEvent(realm, roomId, includedTypes = listOf(EventType.MESSAGE)) + val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain() + + val otherRoomMembers = RoomMembers(realm, roomId).getLoaded().filterKeys { it != credentials.userId } + roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString() + roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId) + roomSummaryEntity.topic = lastTopicEvent?.content.toModel()?.topic + roomSummaryEntity.lastMessage = lastEvent + roomSummaryEntity.otherMemberIds.clear() + roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers.keys) + + if (roomSummary != null) { + if (roomSummary.heroes.isNotEmpty()) { + roomSummaryEntity.heroes.clear() + roomSummaryEntity.heroes.addAll(roomSummary.heroes) + } + if (roomSummary.invitedMembersCount != null) { + roomSummaryEntity.invitedMembersCount = roomSummary.invitedMembersCount + } + if (roomSummary.joinedMembersCount != null) { + roomSummaryEntity.joinedMembersCount = roomSummary.joinedMembersCount + } } realm.insertOrUpdate(roomSummaryEntity) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt index c62bc2e1..b1926486 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt @@ -40,7 +40,7 @@ internal class SyncModule { } scope(DefaultSession.SCOPE) { - RoomSyncHandler(get(), get(), get()) + RoomSyncHandler(get(), get(), get(),get(),get(),get()) } scope(DefaultSession.SCOPE) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt index ef06471c..d9859526 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.sync import arrow.core.Try import im.vector.matrix.android.internal.session.sync.model.SyncResponse import timber.log.Timber +import kotlin.system.measureTimeMillis internal class SyncResponseHandler(private val roomSyncHandler: RoomSyncHandler, private val userAccountDataSyncHandler: UserAccountDataSyncHandler, @@ -26,16 +27,19 @@ internal class SyncResponseHandler(private val roomSyncHandler: RoomSyncHandler, fun handleResponse(syncResponse: SyncResponse, fromToken: String?, isCatchingUp: Boolean): Try { return Try { - Timber.v("Handle sync response") - if (syncResponse.rooms != null) { - roomSyncHandler.handle(syncResponse.rooms) - } - if (syncResponse.groups != null) { - groupSyncHandler.handle(syncResponse.groups) - } - if (syncResponse.accountData != null) { - userAccountDataSyncHandler.handle(syncResponse.accountData) + Timber.v("Start handling sync") + val measure = measureTimeMillis { + if (syncResponse.rooms != null) { + roomSyncHandler.handle(syncResponse.rooms) + } + if (syncResponse.groups != null) { + groupSyncHandler.handle(syncResponse.groups) + } + if (syncResponse.accountData != null) { + userAccountDataSyncHandler.handle(syncResponse.accountData) + } } + Timber.v("Finish handling sync in $measure ms") syncResponse } } From 86a60f7ebd4aa2ac03b96297140e5d4a0adf17bb Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 28 Mar 2019 16:06:43 +0100 Subject: [PATCH 08/12] Timeline : fix avatar issues / remove glide logs --- .../core/glide/MyAppGlideModule.java | 13 ++++- .../features/home/AvatarRenderer.kt | 54 +++++++------------ .../features/html/PillImageSpan.kt | 6 +-- 3 files changed, 34 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/im/vector/riotredesign/core/glide/MyAppGlideModule.java b/app/src/main/java/im/vector/riotredesign/core/glide/MyAppGlideModule.java index f88d4f80..08fed579 100644 --- a/app/src/main/java/im/vector/riotredesign/core/glide/MyAppGlideModule.java +++ b/app/src/main/java/im/vector/riotredesign/core/glide/MyAppGlideModule.java @@ -16,8 +16,19 @@ package im.vector.riotredesign.core.glide; +import android.content.Context; +import android.util.Log; + +import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule -public final class MyAppGlideModule extends AppGlideModule {} \ No newline at end of file +public final class MyAppGlideModule extends AppGlideModule { + + @Override + public void applyOptions(Context context, GlideBuilder builder) { + builder.setLogLevel(Log.ERROR); + } + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt b/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt index 275d7613..373c6fc6 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt @@ -19,11 +19,10 @@ package im.vector.riotredesign.features.home import android.content.Context import android.graphics.drawable.Drawable import android.widget.ImageView +import androidx.annotation.AnyThread import androidx.annotation.UiThread -import androidx.annotation.WorkerThread import androidx.core.content.ContextCompat import com.amulyakhare.textdrawable.TextDrawable -import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.target.DrawableImageViewTarget import com.bumptech.glide.request.target.Target @@ -40,8 +39,11 @@ import im.vector.riotredesign.core.glide.GlideRequests /** * This helper centralise ways to retrieve avatar into ImageView or even generic Target */ + object AvatarRenderer { + private const val THUMBNAIL_SIZE = 250 + @UiThread fun render(roomMember: RoomMember, imageView: ImageView) { render(roomMember.avatarUrl, roomMember.displayName, imageView) @@ -54,7 +56,7 @@ object AvatarRenderer { @UiThread fun render(avatarUrl: String?, name: String?, imageView: ImageView) { - render(imageView.context, GlideApp.with(imageView), avatarUrl, name, imageView.height, DrawableImageViewTarget(imageView)) + render(imageView.context, GlideApp.with(imageView), avatarUrl, name, DrawableImageViewTarget(imageView)) } @UiThread @@ -62,45 +64,18 @@ object AvatarRenderer { glideRequest: GlideRequests, avatarUrl: String?, name: String?, - size: Int, target: Target) { if (name.isNullOrEmpty()) { return } - val placeholder = buildPlaceholderDrawable(context, name) - buildGlideRequest(glideRequest, avatarUrl, size) + val placeholder = getPlaceholderDrawable(context, name) + buildGlideRequest(glideRequest, avatarUrl) .placeholder(placeholder) .into(target) } - @WorkerThread - fun getCachedOrPlaceholder(context: Context, - glideRequest: GlideRequests, - avatarUrl: String?, - text: String, - size: Int): Drawable { - val future = buildGlideRequest(glideRequest, avatarUrl, size).onlyRetrieveFromCache(true).submit() - return try { - future.get() - } catch (exception: Exception) { - buildPlaceholderDrawable(context, text) - } - } - - // PRIVATE API ********************************************************************************* - - private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?, size: Int): GlideRequest { - val resolvedUrl = Matrix.getInstance().currentSession - .contentUrlResolver() - .resolveThumbnail(avatarUrl, size, size, ContentUrlResolver.ThumbnailMethod.SCALE) - - return glideRequest - .load(resolvedUrl) - .apply(RequestOptions.circleCropTransform()) - .diskCacheStrategy(DiskCacheStrategy.DATA) - } - - private fun buildPlaceholderDrawable(context: Context, text: String): Drawable { + @AnyThread + fun getPlaceholderDrawable(context: Context, text: String): Drawable { val avatarColor = ContextCompat.getColor(context, R.color.pale_teal) return if (text.isEmpty()) { TextDrawable.builder().buildRound("", avatarColor) @@ -113,4 +88,15 @@ object AvatarRenderer { } + // PRIVATE API ********************************************************************************* + + private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest { + val resolvedUrl = Matrix.getInstance().currentSession.contentUrlResolver() + .resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE) + + return glideRequest + .load(resolvedUrl) + .apply(RequestOptions.circleCropTransform()) + } + } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt b/app/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt index 3f48043d..a5189494 100644 --- a/app/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt +++ b/app/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt @@ -37,8 +37,6 @@ import java.lang.ref.WeakReference * It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached. */ -private const val PILL_AVATAR_SIZE = 80 - class PillImageSpan(private val glideRequests: GlideRequests, private val context: Context, private val userId: String, @@ -55,7 +53,7 @@ class PillImageSpan(private val glideRequests: GlideRequests, @UiThread fun bind(textView: TextView) { tv = WeakReference(textView) - AvatarRenderer.render(context, glideRequests, user?.avatarUrl, displayName, PILL_AVATAR_SIZE, target) + AvatarRenderer.render(context, glideRequests, user?.avatarUrl, displayName, target) } // ReplacementSpan ***************************************************************************** @@ -108,7 +106,7 @@ class PillImageSpan(private val glideRequests: GlideRequests, textStartPadding = textPadding setChipMinHeightResource(R.dimen.pill_min_height) setChipIconSizeResource(R.dimen.pill_avatar_size) - chipIcon = AvatarRenderer.getCachedOrPlaceholder(context, glideRequests, user?.avatarUrl, displayName, PILL_AVATAR_SIZE) + chipIcon = AvatarRenderer.getPlaceholderDrawable(context, displayName) setBounds(0, 0, intrinsicWidth, intrinsicHeight) } } From a7b81a467183d5a92a9081f580a61519216c70f2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 28 Mar 2019 16:07:05 +0100 Subject: [PATCH 09/12] Timeline : remove item animation for now. --- .../features/home/room/detail/RoomDetailFragment.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 8f9d494e..30a262c4 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -34,7 +34,6 @@ import im.vector.riotredesign.features.home.AvatarRenderer import im.vector.riotredesign.features.home.HomeModule import im.vector.riotredesign.features.home.HomePermalinkHandler import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController -import im.vector.riotredesign.features.home.room.detail.timeline.animation.TimelineItemAnimator import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener import im.vector.riotredesign.features.media.MediaContentRenderer import im.vector.riotredesign.features.media.MediaViewerActivity @@ -104,7 +103,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager) recyclerView.layoutManager = layoutManager - recyclerView.itemAnimator = TimelineItemAnimator() + recyclerView.itemAnimator = null recyclerView.setHasFixedSize(true) timelineEventController.addModelBuildListener { it.dispatchTo(stateRestorer) From 2e2d5b9f86561cf503a9d39f758a5bae798efba1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 28 Mar 2019 16:24:40 +0100 Subject: [PATCH 10/12] Timeline : add non empty content to displayable conditions => should probably be removed later if we want to handle this case with special epoxy item. --- .../room/detail/timeline/helper/TimelineDisplayableEvents.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 1d3a8bf7..565f4254 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -39,7 +39,7 @@ object TimelineDisplayableEvents { } fun TimelineEvent.isDisplayable(): Boolean { - return TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.type) + return TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.type) && !root.content.isNullOrEmpty() } fun List.filterDisplayableEvents(): List { From 94db36d6c4185021b9c913b76f058a87b82428cf Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 29 Mar 2019 11:15:27 +0100 Subject: [PATCH 11/12] Fix room summary not being updated when room members are loaded --- .../android/internal/session/SessionModule.kt | 5 ++ .../internal/session/room/RoomModule.kt | 10 +-- .../session/room/RoomSummaryUpdater.kt | 79 +++++++++++++++++++ .../room/members/LoadRoomMembersTask.kt | 16 ++-- .../internal/session/sync/RoomSyncHandler.kt | 79 ++----------------- .../internal/session/sync/SyncModule.kt | 2 +- 6 files changed, 103 insertions(+), 88 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt 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 510325fa..85dcdb03 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 @@ -29,6 +29,7 @@ import im.vector.matrix.android.internal.session.group.DefaultGroupService import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater import im.vector.matrix.android.internal.session.room.DefaultRoomService 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.session.room.prune.EventsPruner @@ -89,6 +90,10 @@ internal class SessionModule(private val sessionParams: SessionParams) { RoomAvatarResolver(get(), sessionParams.credentials) } + scope(DefaultSession.SCOPE) { + RoomSummaryUpdater(get(), get(), get()) + } + scope(DefaultSession.SCOPE) { DefaultRoomService(get(), get()) as RoomService } 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 716fd155..c506cb0d 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 @@ -22,11 +22,7 @@ import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTas import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask import im.vector.matrix.android.internal.session.room.send.EventFactory -import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask -import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask -import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask -import im.vector.matrix.android.internal.session.room.timeline.PaginationTask -import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor +import im.vector.matrix.android.internal.session.room.timeline.* import org.koin.dsl.module.module import retrofit2.Retrofit @@ -41,7 +37,7 @@ class RoomModule { } scope(DefaultSession.SCOPE) { - DefaultLoadRoomMembersTask(get(), get(), get()) as LoadRoomMembersTask + DefaultLoadRoomMembersTask(get(), get(), get(), get()) as LoadRoomMembersTask } scope(DefaultSession.SCOPE) { @@ -57,7 +53,7 @@ class RoomModule { } scope(DefaultSession.SCOPE) { - DefaultSetReadMarkersTask(get(), get(),get()) as SetReadMarkersTask + DefaultSetReadMarkersTask(get(), get(), get()) as SetReadMarkersTask } scope(DefaultSession.SCOPE) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt new file mode 100644 index 00000000..c8ed0ee9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -0,0 +1,79 @@ +/* + * + * * Copyright 2019 New Vector Ltd + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package im.vector.matrix.android.internal.session.room + +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.RoomTopicContent +import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity +import im.vector.matrix.android.internal.database.query.latestEvent +import im.vector.matrix.android.internal.database.query.prev +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver +import im.vector.matrix.android.internal.session.room.members.RoomMembers +import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary +import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications +import io.realm.Realm +import io.realm.kotlin.createObject + +internal class RoomSummaryUpdater(private val credentials: Credentials, + private val roomDisplayNameResolver: RoomDisplayNameResolver, + private val roomAvatarResolver: RoomAvatarResolver) { + + fun update(realm: Realm, + roomId: String, + roomSummary: RoomSyncSummary? = null, + unreadNotifications: RoomSyncUnreadNotifications? = null) { + + val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() + ?: realm.createObject(roomId) + + if (roomSummary != null) { + if (roomSummary.heroes.isNotEmpty()) { + roomSummaryEntity.heroes.clear() + roomSummaryEntity.heroes.addAll(roomSummary.heroes) + } + if (roomSummary.invitedMembersCount != null) { + roomSummaryEntity.invitedMembersCount = roomSummary.invitedMembersCount + } + if (roomSummary.joinedMembersCount != null) { + roomSummaryEntity.joinedMembersCount = roomSummary.joinedMembersCount + } + } + if (unreadNotifications?.highlightCount != null) { + roomSummaryEntity.highlightCount = unreadNotifications.highlightCount + } + if (unreadNotifications?.notificationCount != null) { + roomSummaryEntity.notificationCount = unreadNotifications.notificationCount + } + + val lastEvent = EventEntity.latestEvent(realm, roomId, includedTypes = listOf(EventType.MESSAGE)) + val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain() + val otherRoomMembers = RoomMembers(realm, roomId).getLoaded().filterKeys { it != credentials.userId } + roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString() + roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId) + roomSummaryEntity.topic = lastTopicEvent?.content.toModel()?.topic + roomSummaryEntity.lastMessage = lastEvent + roomSummaryEntity.otherMemberIds.clear() + roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers.keys) + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt index 788bb1c1..7cd8bbaf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.tryTransactionSync @@ -39,7 +40,8 @@ internal interface LoadRoomMembersTask : Task { @@ -49,7 +51,7 @@ internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI, //TODO use this token val lastToken = syncTokenStore.getLastToken() executeRequest { - apiCall = roomAPI.getMembers(params.roomId, null, null, params.excludeMembership?.value) + apiCall = roomAPI.getMembers(params.roomId, lastToken, null, params.excludeMembership?.value) }.flatMap { response -> insertInDb(response, params.roomId) }.map { true } @@ -61,22 +63,24 @@ internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI, .tryTransactionSync { realm -> // We ignore all the already known members val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) val roomMembers = RoomMembers(realm, roomId).getLoaded() val eventsToInsert = response.roomMemberEvents.filter { !roomMembers.containsKey(it.stateKey) } roomEntity.addStateEvents(eventsToInsert) roomEntity.areAllMembersLoaded = true + + roomSummaryUpdater.update(realm, roomId) } .map { response } } private fun areAllMembersAlreadyLoaded(roomId: String): Boolean { return monarchy - .fetchAllCopiedSync { RoomEntity.where(it, roomId) } - .firstOrNull() - ?.areAllMembersLoaded ?: false + .fetchAllCopiedSync { RoomEntity.where(it, roomId) } + .firstOrNull() + ?.areAllMembersLoaded ?: false } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index cc29f99e..70848eea 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -17,45 +17,28 @@ package im.vector.matrix.android.internal.session.sync import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.auth.data.Credentials 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.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.MyMembership -import im.vector.matrix.android.api.session.room.model.RoomTopicContent import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent 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.lastStateIndex -import im.vector.matrix.android.internal.database.mapper.asDomain 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.RoomEntity -import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom -import im.vector.matrix.android.internal.database.query.latestEvent -import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.room.RoomAvatarResolver -import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver -import im.vector.matrix.android.internal.session.room.members.RoomMembers +import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection -import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync -import im.vector.matrix.android.internal.session.sync.model.RoomSync -import im.vector.matrix.android.internal.session.sync.model.RoomSyncAccountData -import im.vector.matrix.android.internal.session.sync.model.RoomSyncEphemeral -import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary -import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications -import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse +import im.vector.matrix.android.internal.session.sync.model.* import io.realm.Realm import io.realm.kotlin.createObject internal class RoomSyncHandler(private val monarchy: Monarchy, private val readReceiptHandler: ReadReceiptHandler, - private val roomDisplayNameResolver: RoomDisplayNameResolver, - private val roomAvatarResolver: RoomAvatarResolver, - private val credentials: Credentials, + private val roomSummaryUpdater: RoomSummaryUpdater, private val roomTagHandler: RoomTagHandler) { sealed class HandlingStrategy { @@ -88,7 +71,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, roomSync: RoomSync): RoomEntity { val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) if (roomEntity.membership == MyMembership.INVITED) { roomEntity.chunks.deleteAllFromRealm() @@ -119,12 +102,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, ) roomEntity.addOrUpdate(chunkEntity) } - - handleRoomSummary(realm, roomId, roomSync.summary) - - if (roomSync.unreadNotifications != null) { - handleUnreadNotifications(realm, roomId, roomSync.unreadNotifications) - } + roomSummaryUpdater.update(realm, roomId, roomSync.summary, roomSync.unreadNotifications) if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) { handleEphemeral(realm, roomId, roomSync.ephemeral) @@ -180,39 +158,6 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, return chunkEntity } - private fun handleRoomSummary(realm: Realm, - roomId: String, - roomSummary: RoomSyncSummary?) { - - val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: RoomSummaryEntity(roomId) - - val lastEvent = EventEntity.latestEvent(realm, roomId, includedTypes = listOf(EventType.MESSAGE)) - val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain() - - val otherRoomMembers = RoomMembers(realm, roomId).getLoaded().filterKeys { it != credentials.userId } - roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString() - roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId) - roomSummaryEntity.topic = lastTopicEvent?.content.toModel()?.topic - roomSummaryEntity.lastMessage = lastEvent - roomSummaryEntity.otherMemberIds.clear() - roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers.keys) - - if (roomSummary != null) { - if (roomSummary.heroes.isNotEmpty()) { - roomSummaryEntity.heroes.clear() - roomSummaryEntity.heroes.addAll(roomSummary.heroes) - } - if (roomSummary.invitedMembersCount != null) { - roomSummaryEntity.invitedMembersCount = roomSummary.invitedMembersCount - } - if (roomSummary.joinedMembersCount != null) { - roomSummaryEntity.joinedMembersCount = roomSummary.joinedMembersCount - } - } - realm.insertOrUpdate(roomSummaryEntity) - } - private fun handleEphemeral(realm: Realm, roomId: String, ephemeral: RoomSyncEphemeral) { @@ -222,20 +167,6 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, .flatMap { readReceiptHandler.handle(realm, roomId, it) } } - private fun handleUnreadNotifications(realm: Realm, roomId: String, unreadNotifications: RoomSyncUnreadNotifications) { - val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: RoomSummaryEntity(roomId) - - if (unreadNotifications.highlightCount != null) { - roomSummaryEntity.highlightCount = unreadNotifications.highlightCount - } - if (unreadNotifications.notificationCount != null) { - roomSummaryEntity.notificationCount = unreadNotifications.notificationCount - } - realm.insertOrUpdate(roomSummaryEntity) - - } - private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) { accountData.events .filter { it.type == EventType.TAG } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt index b1926486..7995fff1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt @@ -40,7 +40,7 @@ internal class SyncModule { } scope(DefaultSession.SCOPE) { - RoomSyncHandler(get(), get(), get(),get(),get(),get()) + RoomSyncHandler(get(), get(), get(), get()) } scope(DefaultSession.SCOPE) { From be6a4efacbbe7df4dc671627a7ea090bf1af9935 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 1 Apr 2019 15:18:52 +0200 Subject: [PATCH 12/12] Timeline : make tests compile and pass --- matrix-sdk-android/build.gradle | 5 +- .../session/room/timeline/ChunkEntityTest.kt | 28 +++----- .../session/room/timeline/RoomDataHelper.kt | 49 ++++++++++++-- ...{TimelineHolderTest.kt => TimelineTest.kt} | 64 ++++++++++++------- .../android/api/session/events/model/Event.kt | 12 ++++ .../database/helper/ChunkEntityHelper.kt | 7 +- .../room/members/LoadRoomMembersTask.kt | 9 ++- .../session/room/timeline/DefaultTimeline.kt | 45 +++++++++---- 8 files changed, 150 insertions(+), 69 deletions(-) rename matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/{TimelineHolderTest.kt => TimelineTest.kt} (50%) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 4653d4b5..30ff7df4 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -28,6 +28,7 @@ android { targetSdkVersion 28 versionCode 1 versionName "1.0" + multiDexEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -104,17 +105,17 @@ dependencies { testImplementation 'org.robolectric:shadows-support-v4:3.0' testImplementation "io.mockk:mockk:1.8.13.kotlin13" testImplementation 'org.amshove.kluent:kluent-android:1.44' - testImplementation "androidx.arch.core:core-testing:$lifecycle_version" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" androidTestImplementation "org.koin:koin-test:$koin_version" + androidTestImplementation 'androidx.test:core:1.1.0' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test:rules:1.1.1' + androidTestImplementation 'androidx.test.ext:junit:1.1.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' androidTestImplementation 'org.amshove.kluent:kluent-android:1.44' androidTestImplementation "io.mockk:mockk-android:1.8.13.kotlin13" androidTestImplementation "androidx.arch.core:core-testing:$lifecycle_version" androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" - } 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 397dcfc8..b76014d1 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 @@ -16,10 +16,9 @@ package im.vector.matrix.android.session.room.timeline +import androidx.test.ext.junit.runners.AndroidJUnit4 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 @@ -27,6 +26,9 @@ 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 im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents +import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeMessageEvent +import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeRoomMemberEvent import io.realm.Realm import io.realm.RealmConfiguration import io.realm.kotlin.createObject @@ -35,9 +37,10 @@ import org.amshove.kluent.shouldBeTrue import org.amshove.kluent.shouldEqual import org.junit.Before import org.junit.Test -import kotlin.random.Random +import org.junit.runner.RunWith +@RunWith(AndroidJUnit4::class) internal class ChunkEntityTest : InstrumentedTest { private lateinit var monarchy: Monarchy @@ -54,7 +57,7 @@ internal class ChunkEntityTest : InstrumentedTest { fun add_shouldAdd_whenNotAlreadyIncluded() { monarchy.runTransactionSync { realm -> val chunk: ChunkEntity = realm.createObject() - val fakeEvent = createFakeEvent(false) + val fakeEvent = createFakeMessageEvent() chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) chunk.events.size shouldEqual 1 } @@ -64,7 +67,7 @@ internal class ChunkEntityTest : InstrumentedTest { fun add_shouldNotAdd_whenAlreadyIncluded() { monarchy.runTransactionSync { realm -> val chunk: ChunkEntity = realm.createObject() - val fakeEvent = createFakeEvent(false) + val fakeEvent = createFakeMessageEvent() chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) chunk.events.size shouldEqual 1 @@ -75,7 +78,7 @@ internal class ChunkEntityTest : InstrumentedTest { fun add_shouldStateIndexIncremented_whenStateEventIsAddedForward() { monarchy.runTransactionSync { realm -> val chunk: ChunkEntity = realm.createObject() - val fakeEvent = createFakeEvent(true) + val fakeEvent = createFakeRoomMemberEvent() chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 1 } @@ -85,7 +88,7 @@ internal class ChunkEntityTest : InstrumentedTest { fun add_shouldStateIndexNotIncremented_whenNoStateEventIsAdded() { monarchy.runTransactionSync { realm -> val chunk: ChunkEntity = realm.createObject() - val fakeEvent = createFakeEvent(false) + val fakeEvent = createFakeMessageEvent() chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 0 } @@ -196,15 +199,4 @@ internal class ChunkEntityTest : InstrumentedTest { } } - - 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/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt index 35e9cb12..9d2edd74 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt @@ -17,9 +17,15 @@ package im.vector.matrix.android.session.room.timeline import com.zhuinden.monarchy.Monarchy +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.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toContent +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.MyMembership +import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.message.MessageTextContent +import im.vector.matrix.android.api.session.room.model.message.MessageType 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.model.ChunkEntity @@ -30,27 +36,56 @@ import kotlin.random.Random object RoomDataHelper { + private const val FAKE_TEST_SENDER = "@sender:test.org" + private val EVENT_FACTORIES = hashMapOf( + 0 to { createFakeMessageEvent() }, + 1 to { createFakeRoomMemberEvent() } + ) + fun createFakeListOfEvents(size: Int = 10): List { - return (0 until size).map { createFakeEvent(Random.nextBoolean()) } + return (0 until size).mapNotNull { + val nextInt = Random.nextInt(EVENT_FACTORIES.size) + EVENT_FACTORIES[nextInt]?.invoke() + } } - 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) + fun createFakeEvent(type: String, + content: Content? = null, + prevContent: Content? = null, + sender: String = FAKE_TEST_SENDER, + stateKey: String = FAKE_TEST_SENDER + ): Event { + return Event( + type = type, + eventId = Random.nextLong().toString(), + content = content, + prevContent = prevContent, + sender = sender, + stateKey = stateKey + ) + } + + fun createFakeMessageEvent(): Event { + val message = MessageTextContent(MessageType.MSGTYPE_TEXT, "Fake message #${Random.nextLong()}").toContent() + return createFakeEvent(EventType.MESSAGE, message) + } + + fun createFakeRoomMemberEvent(): Event { + val roomMember = RoomMember(Membership.JOIN, "Fake name #${Random.nextLong()}").toContent() + return createFakeEvent(EventType.STATE_ROOM_MEMBER, roomMember) } fun fakeInitialSync(monarchy: Monarchy, roomId: String) { monarchy.runTransactionSync { realm -> val roomEntity = realm.createObject(roomId) roomEntity.membership = MyMembership.JOINED - val eventList = createFakeListOfEvents(30) + val eventList = createFakeListOfEvents(10) val chunkEntity = realm.createObject().apply { nextToken = null prevToken = Random.nextLong(System.currentTimeMillis()).toString() isLastForward = true } - chunkEntity.addAll("roomId", eventList, PaginationDirection.FORWARDS) + chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS) roomEntity.addOrUpdate(chunkEntity) } } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineHolderTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt similarity index 50% rename from matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineHolderTest.kt rename to matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt index 2063a618..a80e92a0 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineHolderTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt @@ -16,55 +16,75 @@ package im.vector.matrix.android.session.room.timeline -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.test.annotation.UiThreadTest import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest -import im.vector.matrix.android.LiveDataTestObserver +import im.vector.matrix.android.api.session.room.timeline.Timeline +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor -import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService +import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline +import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.testCoroutineDispatchers import io.realm.Realm import io.realm.RealmConfiguration +import org.amshove.kluent.shouldEqual import org.junit.Before -import org.junit.Rule import org.junit.Test +import timber.log.Timber +import java.util.concurrent.CountDownLatch -internal class TimelineHolderTest : InstrumentedTest { +internal class TimelineTest : InstrumentedTest { + + companion object { + private const val ROOM_ID = "roomId" + } - @get:Rule val testRule = InstantTaskExecutorRule() private lateinit var monarchy: Monarchy @Before fun setup() { + Timber.plant(Timber.DebugTree()) Realm.init(context()) val testConfiguration = RealmConfiguration.Builder().name("test-realm").build() Realm.deleteRealm(testConfiguration) monarchy = Monarchy.Builder().setRealmConfiguration(testConfiguration).build() + RoomDataHelper.fakeInitialSync(monarchy, ROOM_ID) } - @Test - @UiThreadTest - fun backPaginate_shouldLoadMoreEvents_whenLoadAroundIsCalled() { - val roomId = "roomId" + private fun createTimeline(initialEventId: String? = null): Timeline { val taskExecutor = TaskExecutor(testCoroutineDispatchers) val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy) val paginationTask = FakePaginationTask(tokenChunkEventPersistor) val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor) - RoomDataHelper.fakeInitialSync(monarchy, roomId) - val timelineHolder = DefaultTimelineService(roomId, monarchy, taskExecutor, getContextOfEventTask, RoomMemberExtractor(roomId)) - val timelineObserver = LiveDataTestObserver.test(timelineHolder.timeline()) - timelineObserver.awaitNextValue().assertHasValue() - var timelineData = timelineObserver.value() - timelineData.events.size shouldEqual 30 - (0 until timelineData.events.size).map { - timelineData.events.loadAround(it) + val roomMemberExtractor = RoomMemberExtractor(ROOM_ID) + val timelineEventFactory = TimelineEventFactory(roomMemberExtractor) + return DefaultTimeline(ROOM_ID, initialEventId, monarchy.realmConfiguration, taskExecutor, getContextOfEventTask, timelineEventFactory, paginationTask, null) + } + + @Test + fun backPaginate_shouldLoadMoreEvents_whenPaginateIsCalled() { + val timeline = createTimeline() + timeline.start() + val paginationCount = 30 + var initialLoad = 0 + val latch = CountDownLatch(2) + var timelineEvents: List = emptyList() + timeline.listener = object : Timeline.Listener { + override fun onUpdated(snapshot: List) { + if (snapshot.isNotEmpty()) { + if (initialLoad == 0) { + initialLoad = snapshot.size + } + timelineEvents = snapshot + latch.countDown() + timeline.paginate(Timeline.Direction.BACKWARDS, paginationCount) + } + } } - timelineObserver.awaitNextValue().assertHasValue() - timelineData = timelineObserver.value() - timelineData.events.size shouldEqual 60 + latch.await() + timelineEvents.size shouldEqual initialLoad + paginationCount + timeline.dispose() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt index 6d99c9eb..9a66ffed 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt @@ -35,6 +35,18 @@ inline fun Content?.toModel(): T? { } } +/** + * This methods is a facility method to map a model to a json Content + */ +@Suppress("UNCHECKED_CAST") +inline fun T?.toContent(): Content? { + return this?.let { + val moshi = MoshiProvider.providesMoshi() + val moshiAdapter = moshi.adapter(T::class.java) + return moshiAdapter.toJsonValue(it) as Content + } +} + /** * Generic event class with all possible fields for events. * The content and prevContent json fields can easily be mapped to a model with [toModel] method. 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 9a9e4b7c..b92598ab 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 @@ -54,10 +54,14 @@ internal fun ChunkEntity.merge(roomId: String, if (direction == PaginationDirection.FORWARDS) { this.nextToken = chunkToMerge.nextToken this.isLastForward = chunkToMerge.isLastForward + this.forwardsStateIndex = chunkToMerge.forwardsStateIndex + this.forwardsDisplayIndex = chunkToMerge.forwardsDisplayIndex eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) } else { this.prevToken = chunkToMerge.prevToken this.isLastBackward = chunkToMerge.isLastBackward + this.backwardsStateIndex = chunkToMerge.backwardsStateIndex + this.backwardsDisplayIndex = chunkToMerge.backwardsDisplayIndex eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) } eventsToMerge.forEach { @@ -111,8 +115,7 @@ internal fun ChunkEntity.add(roomId: String, this.displayIndex = currentDisplayIndex } // We are not using the order of the list, but will be sorting with displayIndex field - val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size - events.add(position, eventEntity) + events.add(eventEntity) } private fun ChunkEntity.assertIsManaged() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt index 7cd8bbaf..c421b3bd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt @@ -48,7 +48,6 @@ internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI, return if (areAllMembersAlreadyLoaded(params.roomId)) { Try.just(true) } else { - //TODO use this token val lastToken = syncTokenStore.getLastToken() executeRequest { apiCall = roomAPI.getMembers(params.roomId, lastToken, null, params.excludeMembership?.value) @@ -63,7 +62,7 @@ internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI, .tryTransactionSync { realm -> // We ignore all the already known members val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) val roomMembers = RoomMembers(realm, roomId).getLoaded() val eventsToInsert = response.roomMemberEvents.filter { !roomMembers.containsKey(it.stateKey) } @@ -78,9 +77,9 @@ internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI, private fun areAllMembersAlreadyLoaded(roomId: String): Boolean { return monarchy - .fetchAllCopiedSync { RoomEntity.where(it, roomId) } - .firstOrNull() - ?.areAllMembersLoaded ?: false + .fetchAllCopiedSync { RoomEntity.where(it, roomId) } + .firstOrNull() + ?.areAllMembersLoaded ?: false } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index ef037ac5..38555d20 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -36,7 +36,12 @@ import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoo import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith -import io.realm.* +import io.realm.OrderedRealmCollectionChangeListener +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.RealmQuery +import io.realm.RealmResults +import io.realm.Sort import timber.log.Timber import java.util.* import java.util.concurrent.atomic.AtomicBoolean @@ -97,10 +102,14 @@ internal class DefaultTimeline( val state = getPaginationState(direction) if (state.isPaginating) { // We are getting new items from pagination - paginateInternal(startDisplayIndex, direction, state.requestedCount) + val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, state.requestedCount) + if (shouldPostSnapshot) { + postSnapshot() + } } else { // We are getting new items from sync buildTimelineEvents(startDisplayIndex, direction, range.length.toLong()) + postSnapshot() } } } @@ -114,7 +123,10 @@ internal class DefaultTimeline( } Timber.v("Paginate $direction of $count items") val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex - paginateInternal(startDisplayIndex, direction, count) + val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, count) + if (shouldPostSnapshot) { + postSnapshot() + } } } @@ -191,13 +203,15 @@ internal class DefaultTimeline( /** * This has to be called on TimelineThread as it access realm live results + * @return true if snapshot should be posted */ private fun paginateInternal(startDisplayIndex: Int, direction: Timeline.Direction, - count: Int) { + count: Int): Boolean { updatePaginationState(direction) { it.copy(requestedCount = count, isPaginating = true) } val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong()) - if (builtCount < count && !hasReachedEnd(direction)) { + val shouldFetchMore = builtCount < count && !hasReachedEnd(direction) + if (shouldFetchMore) { val newRequestedCount = count - builtCount updatePaginationState(direction) { it.copy(requestedCount = newRequestedCount) } val fetchingCount = Math.max(MIN_FETCHING_COUNT, newRequestedCount) @@ -205,6 +219,7 @@ internal class DefaultTimeline( } else { updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) } } + return !shouldFetchMore } private fun snapshot(): List { @@ -252,12 +267,13 @@ internal class DefaultTimeline( } else { val count = Math.min(INITIAL_LOAD_SIZE, liveEvents.size) if (isLive) { - paginate(Timeline.Direction.BACKWARDS, count) + paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count) } else { - paginate(Timeline.Direction.FORWARDS, count / 2) - paginate(Timeline.Direction.BACKWARDS, count / 2) + paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2) + paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count / 2) } } + postSnapshot() } /** @@ -266,9 +282,9 @@ internal class DefaultTimeline( private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { val token = getTokenLive(direction) ?: return val params = PaginationTask.Params(roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit) + from = token, + direction = direction.toPaginationDirection(), + limit = limit) Timber.v("Should fetch $limit items $direction") paginationTask.configureWith(params) @@ -336,8 +352,6 @@ internal class DefaultTimeline( builtEvents.add(position, timelineEvent) } Timber.v("Built ${offsetResults.size} items from db") - val snapshot = snapshot() - mainHandler.post { listener?.onUpdated(snapshot) } return offsetResults.size } @@ -399,6 +413,11 @@ internal class DefaultTimeline( contextOfEventTask.configureWith(params).executeBy(taskExecutor) } + private fun postSnapshot() { + val snapshot = snapshot() + mainHandler.post { listener?.onUpdated(snapshot) } + } + // Extension methods *************************************************************************** private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {