From 2898eae566a5425ea602e0ee7a6de5c453955a81 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 19 Mar 2019 19:45:32 +0100 Subject: [PATCH] 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 }