Timeline : reactivate loaders and get off the main thread

This commit is contained in:
ganfra 2019-03-19 19:45:32 +01:00 committed by ganfra
parent 0c76178bee
commit 2898eae566
26 changed files with 366 additions and 228 deletions

View File

@ -58,7 +58,7 @@ android {


dependencies { dependencies {


def epoxy_version = "3.0.0" def epoxy_version = "3.3.0"
def arrow_version = "0.8.2" def arrow_version = "0.8.2"
def coroutines_version = "1.0.1" def coroutines_version = "1.0.1"
def markwon_version = '3.0.0-SNAPSHOT' def markwon_version = '3.0.0-SNAPSHOT'
@ -77,9 +77,6 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.core:core-ktx:1.0.1' 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.threetenabp:threetenabp:1.1.1'
implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'com.facebook.stetho:stetho:1.5.0' implementation 'com.facebook.stetho:stetho:1.5.0'

View File

@ -19,6 +19,8 @@ package im.vector.riotredesign
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import androidx.multidex.MultiDex import androidx.multidex.MultiDex
import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyController
import com.facebook.stetho.Stetho import com.facebook.stetho.Stetho
import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.glide.GlideImageLoader import com.github.piasy.biv.loader.glide.GlideImageLoader
@ -41,6 +43,8 @@ class Riot : Application() {
} }
AndroidThreeTen.init(this) AndroidThreeTen.init(this)
BigImageViewer.initialize(GlideImageLoader.with(applicationContext)) BigImageViewer.initialize(GlideImageLoader.with(applicationContext))
EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
val appModule = AppModule(applicationContext).definition val appModule = AppModule(applicationContext).definition
val homeModule = HomeModule().definition val homeModule = HomeModule().definition
startKoin(listOf(appModule, homeModule), logger = EmptyLogger()) startKoin(listOf(appModule, homeModule), logger = EmptyLogger())

View File

@ -16,6 +16,7 @@


package im.vector.riotredesign.features.home.room.detail 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 import im.vector.matrix.android.api.session.room.timeline.TimelineEvent


sealed class RoomDetailActions { sealed class RoomDetailActions {
@ -23,6 +24,6 @@ sealed class RoomDetailActions {
data class SendMessage(val text: String) : RoomDetailActions() data class SendMessage(val text: String) : RoomDetailActions()
object IsDisplayed : RoomDetailActions() object IsDisplayed : RoomDetailActions()
data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions() data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions()
object LoadMore: RoomDetailActions() data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions()


} }

View File

@ -25,6 +25,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.fragmentViewModel 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.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
@ -110,9 +111,14 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
it.dispatchTo(stateRestorer) it.dispatchTo(stateRestorer)
it.dispatchTo(scrollOnNewMessageCallback) it.dispatchTo(scrollOnNewMessageCallback)
} }
recyclerView.addOnScrollListener(object : EndlessRecyclerViewScrollListener(layoutManager, EndlessRecyclerViewScrollListener.LoadOnScrollDirection.BOTTOM) { recyclerView.addOnScrollListener(object : EndlessRecyclerViewScrollListener(layoutManager, Timeline.Direction.BACKWARDS) {
override fun onLoadMore(page: Int, totalItemsCount: Int) { override fun onLoadMore() {
roomDetailViewModel.process(RoomDetailActions.LoadMore) 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) recyclerView.setController(timelineEventController)

View File

@ -22,12 +22,12 @@ import com.jakewharton.rxrelay2.BehaviorRelay
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session 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.events.model.Event
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.RiotViewModel import im.vector.riotredesign.core.platform.RiotViewModel
import im.vector.riotredesign.features.home.room.VisibleRoomStore import im.vector.riotredesign.features.home.room.VisibleRoomStore
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit


class RoomDetailViewModel(initialState: RoomDetailViewState, class RoomDetailViewModel(initialState: RoomDetailViewState,
@ -64,7 +64,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
is RoomDetailActions.SendMessage -> handleSendMessage(action) is RoomDetailActions.SendMessage -> handleSendMessage(action)
is RoomDetailActions.IsDisplayed -> handleIsDisplayed() is RoomDetailActions.IsDisplayed -> handleIsDisplayed()
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action) 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) visibleRoomHolder.post(roomId)
} }


private fun handleLoadMore(action: RoomDetailActions.LoadMore) {
timeline.paginate(action.direction, 50)
}

private fun observeDisplayedEvents() { private fun observeDisplayedEvents() {
// We are buffering scroll events for one second // We are buffering scroll events for one second
// and keep the most recent one to set the read receipt on. // and keep the most recent one to set the read receipt on.
@ -100,13 +104,14 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
private fun observeRoomSummary() { private fun observeRoomSummary() {
room.rx().liveRoomSummary() room.rx().liveRoomSummary()
.execute { async -> .execute { async ->
Timber.v("Room summary updated: $async")
copy(asyncRoomSummary = async) copy(asyncRoomSummary = async)
} }
} }


override fun onCleared() { override fun onCleared() {
super.onCleared()
timeline.dispose() timeline.dispose()
super.onCleared()
} }


} }

View File

@ -16,19 +16,21 @@


package im.vector.riotredesign.features.home.room.detail.timeline package im.vector.riotredesign.features.home.room.detail.timeline


import android.os.Handler
import android.view.View import android.view.View
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.VisibilityState import com.airbnb.epoxy.VisibilityState
import im.vector.matrix.android.api.session.room.timeline.Timeline 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.session.room.timeline.TimelineEvent
import im.vector.riotredesign.core.epoxy.LoadingItemModel_
import im.vector.riotredesign.core.epoxy.RiotEpoxyModel import im.vector.riotredesign.core.epoxy.RiotEpoxyModel
import im.vector.riotredesign.core.extensions.localDateTime 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.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.TimelineDateFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback 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.TimelineMediaSizeProvider
@ -37,20 +39,16 @@ import im.vector.riotredesign.features.media.MediaContentRenderer


class TimelineEventController(private val dateFormatter: TimelineDateFormatter, class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
private val timelineItemFactory: TimelineItemFactory, private val timelineItemFactory: TimelineItemFactory,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
) : EpoxyController( private val backgroundHandler: Handler = TimelineAsyncHelper.getBackgroundHandler()
EpoxyAsyncUtil.getAsyncBackgroundHandler(), ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {
EpoxyAsyncUtil.getAsyncBackgroundHandler()
), Timeline.Listener {


private val modelCache = arrayListOf<List<EpoxyModel<*>>>() private val modelCache = arrayListOf<List<EpoxyModel<*>>>()
private var currentSnapshot: List<TimelineEvent> = emptyList() private var currentSnapshot: List<TimelineEvent> = emptyList()


override fun onUpdated(snapshot: List<TimelineEvent>) {
submitSnapshot(snapshot)
}

private val listUpdateCallback = object : ListUpdateCallback { private val listUpdateCallback = object : ListUpdateCallback {

@Synchronized
override fun onChanged(position: Int, count: Int, payload: Any?) { override fun onChanged(position: Int, count: Int, payload: Any?) {
(position until (position + count)).forEach { (position until (position + count)).forEach {
modelCache[it] = emptyList() modelCache[it] = emptyList()
@ -62,7 +60,8 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
//no-op //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) { if (modelCache.isNotEmpty() && position == modelCache.size) {
modelCache[position - 1] = emptyList() 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 private var timeline: Timeline? = null
var callback: Callback? = null var callback: Callback? = null


@ -89,7 +84,6 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
if (this.timeline != timeline) { if (this.timeline != timeline) {
this.timeline = timeline this.timeline = timeline
this.timeline?.listener = this this.timeline?.listener = this
submitSnapshot(timeline?.snapshot() ?: emptyList())
} }
} }


@ -99,9 +93,25 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
} }


override fun buildModels() { override fun buildModels() {
LoadingItemModel_()
.id("forward_loading_item")
.addWhen(Timeline.Direction.FORWARDS)

add(getModels()) 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<EpoxyModel<*>> { private fun getModels(): List<EpoxyModel<*>> {
(0 until modelCache.size).forEach { position -> (0 until modelCache.size).forEach { position ->
if (modelCache[position].isEmpty()) { if (modelCache[position].isEmpty()) {
@ -133,8 +143,14 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
return epoxyModels return epoxyModels
} }


// Timeline.LISTENER ***************************************************************************

override fun onUpdated(snapshot: List<TimelineEvent>) {
submitSnapshot(snapshot)
}

private fun submitSnapshot(newSnapshot: List<TimelineEvent>) { private fun submitSnapshot(newSnapshot: List<TimelineEvent>) {
EpoxyAsyncUtil.getAsyncBackgroundHandler().post { backgroundHandler.post {
val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot) val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot)
currentSnapshot = newSnapshot currentSnapshot = newSnapshot
val diffResult = DiffUtil.calculateDiff(diffCallback) val diffResult = DiffUtil.calculateDiff(diffCallback)
@ -142,6 +158,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
} }
} }



interface Callback { interface Callback {
fun onEventVisible(event: TimelineEvent) fun onEventVisible(event: TimelineEvent)
fun onUrlClicked(url: String) fun onUrlClicked(url: String)

View File

@ -19,26 +19,24 @@ package im.vector.riotredesign.features.home.room.detail.timeline.helper;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; 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 { public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnScrollListener {
// Sets the starting page index // Sets the starting page index
private static final int startingPageIndex = 0; private static final int startingPageIndex = 0;
// The minimum amount of items to have below your current scroll position // The minimum amount of items to have below your current scroll position
// before loading more. // before loading more.
private int visibleThreshold = 30; private int visibleThreshold = 50;
// 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 // The total number of items in the dataset after the last load
private int previousTotalItemCount = 0; private int previousTotalItemCount = 0;
// True if we are still waiting for the last set of data to load. // True if we are still waiting for the last set of data to load.
private boolean loading = true; private boolean loading = true;
private LinearLayoutManager mLayoutManager; 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; this.mLayoutManager = layoutManager;
mDirection = direction; this.mDirection = direction;
} }




@ -55,11 +53,10 @@ public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnS
firstVisibleItemPosition = mLayoutManager.findFirstVisibleItemPosition(); firstVisibleItemPosition = mLayoutManager.findFirstVisibleItemPosition();


switch (mDirection) { switch (mDirection) {
case BOTTOM: case BACKWARDS:
// If the total item count is zero and the previous isn't, assume the // 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 // list is invalidated and should be reset back to initial state
if (totalItemCount < previousTotalItemCount) { if (totalItemCount < previousTotalItemCount) {
this.currentPage = startingPageIndex;
this.previousTotalItemCount = totalItemCount; this.previousTotalItemCount = totalItemCount;
if (totalItemCount == 0) { if (totalItemCount == 0) {
this.loading = true; 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. // 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 // threshold should reflect how many total columns there are too
if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) { if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) {
currentPage++; onLoadMore();
onLoadMore(currentPage, totalItemCount);
loading = true; loading = true;
} }
break; break;
case TOP: case FORWARDS:
// If the total item count is zero and the previous isn't, assume the // 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 // list is invalidated and should be reset back to initial state
if (totalItemCount < previousTotalItemCount) { if (totalItemCount < previousTotalItemCount) {
this.currentPage = startingPageIndex;
this.previousTotalItemCount = totalItemCount; this.previousTotalItemCount = totalItemCount;
if (totalItemCount == 0) { if (totalItemCount == 0) {
this.loading = true; 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. // 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 // threshold should reflect how many total columns there are too
if (!loading && firstVisibleItemPosition < visibleThreshold) { if (!loading && firstVisibleItemPosition < visibleThreshold) {
currentPage++; onLoadMore();
onLoadMore(currentPage, totalItemCount);
loading = true; loading = true;
} }
break; 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 // 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
}
} }

View File

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

}

View File

@ -76,9 +76,6 @@ dependencies {
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1' implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'
kapt 'dk.ilios:realmfieldnameshelper:1.1.1' kapt 'dk.ilios:realmfieldnameshelper:1.1.1'


// Paging
implementation 'androidx.paging:paging-runtime:2.0.0'

// Work // Work
implementation "android.arch.work:work-runtime-ktx:1.0.0-beta02" implementation "android.arch.work:work-runtime-ktx:1.0.0-beta02"



View File

@ -134,13 +134,13 @@ internal class ChunkEntityTest : InstrumentedTest {
val chunk2: ChunkEntity = realm.createObject() val chunk2: ChunkEntity = realm.createObject()
val eventsForChunk1 = createFakeListOfEvents(30) val eventsForChunk1 = createFakeListOfEvents(30)
val eventsForChunk2 = eventsForChunk1 + createFakeListOfEvents(10) val eventsForChunk2 = eventsForChunk1 + createFakeListOfEvents(10)
chunk1.isLast = true chunk1.isLastForward = true
chunk2.isLast = false chunk2.isLastForward = false
chunk1.addAll("roomId", eventsForChunk1, PaginationDirection.FORWARDS) chunk1.addAll("roomId", eventsForChunk1, PaginationDirection.FORWARDS)
chunk2.addAll("roomId", eventsForChunk2, PaginationDirection.BACKWARDS) chunk2.addAll("roomId", eventsForChunk2, PaginationDirection.BACKWARDS)
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
chunk1.events.size shouldEqual 40 chunk1.events.size shouldEqual 40
chunk1.isLast.shouldBeTrue() chunk1.isLastForward.shouldBeTrue()
} }
} }



View File

@ -25,7 +25,7 @@ import kotlin.random.Random


internal class FakeGetContextOfEventTask(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : GetContextOfEventTask { internal class FakeGetContextOfEventTask(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : GetContextOfEventTask {


override fun execute(params: GetContextOfEventTask.Params): Try<TokenChunkEvent> { override fun execute(params: GetContextOfEventTask.Params): Try<TokenChunkEventPersistor.Result> {
val fakeEvents = RoomDataHelper.createFakeListOfEvents(30) val fakeEvents = RoomDataHelper.createFakeListOfEvents(30)
val tokenChunkEvent = FakeTokenChunkEvent( val tokenChunkEvent = FakeTokenChunkEvent(
Random.nextLong(System.currentTimeMillis()).toString(), Random.nextLong(System.currentTimeMillis()).toString(),
@ -33,7 +33,6 @@ internal class FakeGetContextOfEventTask(private val tokenChunkEventPersistor: T
fakeEvents fakeEvents
) )
return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, PaginationDirection.BACKWARDS) return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, PaginationDirection.BACKWARDS)
.map { tokenChunkEvent }
} }





View File

@ -18,17 +18,15 @@ package im.vector.matrix.android.session.room.timeline


import arrow.core.Try import arrow.core.Try
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask 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 im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
import kotlin.random.Random import kotlin.random.Random


internal class FakePaginationTask(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : PaginationTask { internal class FakePaginationTask(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : PaginationTask {


override fun execute(params: PaginationTask.Params): Try<TokenChunkEvent> { override fun execute(params: PaginationTask.Params): Try<TokenChunkEventPersistor.Result> {
val fakeEvents = RoomDataHelper.createFakeListOfEvents(30) val fakeEvents = RoomDataHelper.createFakeListOfEvents(30)
val tokenChunkEvent = FakeTokenChunkEvent(params.from, Random.nextLong(System.currentTimeMillis()).toString(), fakeEvents) val tokenChunkEvent = FakeTokenChunkEvent(params.from, Random.nextLong(System.currentTimeMillis()).toString(), fakeEvents)
return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, params.direction) return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, params.direction)
.map { tokenChunkEvent }
} }


} }

View File

@ -48,7 +48,7 @@ object RoomDataHelper {
val chunkEntity = realm.createObject<ChunkEntity>().apply { val chunkEntity = realm.createObject<ChunkEntity>().apply {
nextToken = null nextToken = null
prevToken = Random.nextLong(System.currentTimeMillis()).toString() prevToken = Random.nextLong(System.currentTimeMillis()).toString()
isLast = true isLastForward = true
} }
chunkEntity.addAll("roomId", eventList, PaginationDirection.FORWARDS) chunkEntity.addAll("roomId", eventList, PaginationDirection.FORWARDS)
roomEntity.addOrUpdate(chunkEntity) roomEntity.addOrUpdate(chunkEntity)

View File

@ -22,6 +22,8 @@ interface Timeline {


var listener: Timeline.Listener? var listener: Timeline.Listener?


fun hasMoreToLoad(direction: Direction): Boolean
fun hasReachedEnd(direction: Direction): Boolean
fun size(): Int fun size(): Int
fun snapshot(): List<TimelineEvent> fun snapshot(): List<TimelineEvent>
fun paginate(direction: Direction, count: Int) fun paginate(direction: Direction, count: Int)
@ -44,14 +46,6 @@ interface Timeline {
* These events come from a back pagination. * These events come from a back pagination.
*/ */
BACKWARDS("b"); BACKWARDS("b");

fun reversed(): Direction {
return when (this) {
FORWARDS -> BACKWARDS
BACKWARDS -> FORWARDS
}
}

} }





View File

@ -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<Cancelable>()

fun add(cancelable: Cancelable) {
cancelableList.add(cancelable)
}

override fun cancel() {
cancelableList.forEach { it.cancel() }
}

}

fun Cancelable.addTo(cancelables: CancelableBag) {
cancelables.add(this)
}

View File

@ -43,7 +43,6 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val m
queryResults.addChangeListener { t, changeSet -> queryResults.addChangeListener { t, changeSet ->
onChanged(t, changeSet) onChanged(t, changeSet)
} }
processInitialResults(queryResults)
results = AtomicReference(queryResults) results = AtomicReference(queryResults)
} }
} }
@ -61,7 +60,7 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val m
return isStarted.get() return isStarted.get()
} }


protected open fun onChanged(realmResults: RealmResults<T>, changeSet: OrderedCollectionChangeSet) { private fun onChanged(realmResults: RealmResults<T>, changeSet: OrderedCollectionChangeSet) {
val insertionIndexes = changeSet.insertions val insertionIndexes = changeSet.insertions
val updateIndexes = changeSet.changes val updateIndexes = changeSet.changes
val deletionIndexes = changeSet.deletions val deletionIndexes = changeSet.deletions
@ -71,12 +70,6 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val m
processChanges(inserted, updated, deleted) processChanges(inserted, updated, deleted)
} }


protected open fun processInitialResults(results: RealmResults<T>) { protected abstract fun processChanges(inserted: List<T>, updated: List<T>, deleted: List<T>)
// no-op
}

protected open fun processChanges(inserted: List<T>, updated: List<T>, deleted: List<T>) {
//no-op
}


} }

View File

@ -27,18 +27,18 @@ import im.vector.matrix.android.internal.database.query.fastContains
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import io.realm.Sort 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 // By default if a chunk is empty we consider it unlinked
internal fun ChunkEntity.isUnlinked(): Boolean { internal fun ChunkEntity.isUnlinked(): Boolean {
assertIsManaged() assertIsManaged()
return events.where().equalTo(EventEntityFields.IS_UNLINKED, false).findAll().isEmpty() 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, internal fun ChunkEntity.merge(roomId: String,
chunkToMerge: ChunkEntity, chunkToMerge: ChunkEntity,
direction: PaginationDirection) { direction: PaginationDirection) {
@ -53,10 +53,11 @@ internal fun ChunkEntity.merge(roomId: String,
val eventsToMerge: List<EventEntity> val eventsToMerge: List<EventEntity>
if (direction == PaginationDirection.FORWARDS) { if (direction == PaginationDirection.FORWARDS) {
this.nextToken = chunkToMerge.nextToken this.nextToken = chunkToMerge.nextToken
this.isLast = chunkToMerge.isLast this.isLastForward = chunkToMerge.isLastForward
eventsToMerge = chunkToMerge.events.reversed() eventsToMerge = chunkToMerge.events.reversed()
} else { } else {
this.prevToken = chunkToMerge.prevToken this.prevToken = chunkToMerge.prevToken
this.isLastBackward = chunkToMerge.isLastBackward
eventsToMerge = chunkToMerge.events eventsToMerge = chunkToMerge.events
} }
eventsToMerge.forEach { eventsToMerge.forEach {
@ -117,14 +118,14 @@ private fun ChunkEntity.assertIsManaged() {


internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
return when (direction) { return when (direction) {
PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findFirst()?.displayIndex PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findFirst()?.displayIndex
PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING).findFirst()?.displayIndex PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING).findFirst()?.displayIndex
} ?: defaultValue } ?: defaultValue
} }


internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
return when (direction) { return when (direction) {
PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING).findFirst()?.stateIndex PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING).findFirst()?.stateIndex
PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.ASCENDING).findFirst()?.stateIndex PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.ASCENDING).findFirst()?.stateIndex
} ?: defaultValue } ?: defaultValue
} }

View File

@ -23,8 +23,9 @@ import io.realm.annotations.LinkingObjects


internal open class ChunkEntity(var prevToken: String? = null, internal open class ChunkEntity(var prevToken: String? = null,
var nextToken: String? = null, var nextToken: String? = null,
var isLast: Boolean = false, var events: RealmList<EventEntity> = RealmList(),
var events: RealmList<EventEntity> = RealmList() var isLastForward: Boolean = false,
var isLastBackward: Boolean = false
) : RealmObject() { ) : RealmObject() {


@LinkingObjects("chunks") @LinkingObjects("chunks")

View File

@ -43,7 +43,7 @@ internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken:


internal fun ChunkEntity.Companion.findLastLiveChunkFromRoom(realm: Realm, roomId: String): ChunkEntity? { internal fun ChunkEntity.Companion.findLastLiveChunkFromRoom(realm: Realm, roomId: String): ChunkEntity? {
return where(realm, roomId) return where(realm, roomId)
.equalTo(ChunkEntityFields.IS_LAST, true) .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
.findFirst() .findFirst()
} }



View File

@ -17,12 +17,12 @@
package im.vector.matrix.android.internal.session.room.timeline package im.vector.matrix.android.internal.session.room.timeline


import arrow.core.Try 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.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI 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 import im.vector.matrix.android.internal.util.FilterUtil


internal interface GetContextOfEventTask : Task<GetContextOfEventTask.Params, TokenChunkEvent> { internal interface GetContextOfEventTask : Task<GetContextOfEventTask.Params, TokenChunkEventPersistor.Result> {


data class Params( data class Params(
val roomId: String, val roomId: String,
@ -35,12 +35,12 @@ internal class DefaultGetContextOfEventTask(private val roomAPI: RoomAPI,
private val tokenChunkEventPersistor: TokenChunkEventPersistor private val tokenChunkEventPersistor: TokenChunkEventPersistor
) : GetContextOfEventTask { ) : GetContextOfEventTask {


override fun execute(params: GetContextOfEventTask.Params): Try<EventContextResponse> { override fun execute(params: GetContextOfEventTask.Params): Try<TokenChunkEventPersistor.Result> {
val filter = FilterUtil.createRoomEventFilter(true)?.toJSONString() val filter = FilterUtil.createRoomEventFilter(true)?.toJSONString()
return executeRequest<EventContextResponse> { return executeRequest<EventContextResponse> {
apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter) apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter)
}.flatMap { response -> }.flatMap { response ->
tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS).map { response } tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS)
} }
} }



View File

@ -23,7 +23,7 @@ import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.FilterUtil import im.vector.matrix.android.internal.util.FilterUtil




internal interface PaginationTask : Task<PaginationTask.Params, Boolean> { internal interface PaginationTask : Task<PaginationTask.Params, TokenChunkEventPersistor.Result> {


data class Params( data class Params(
val roomId: String, val roomId: String,
@ -38,7 +38,7 @@ internal class DefaultPaginationTask(private val roomAPI: RoomAPI,
private val tokenChunkEventPersistor: TokenChunkEventPersistor private val tokenChunkEventPersistor: TokenChunkEventPersistor
) : PaginationTask { ) : PaginationTask {


override fun execute(params: PaginationTask.Params): Try<Boolean> { override fun execute(params: PaginationTask.Params): Try<TokenChunkEventPersistor.Result> {
val filter = FilterUtil.createRoomEventFilter(true)?.toJSONString() val filter = FilterUtil.createRoomEventFilter(true)?.toJSONString()
return executeRequest<PaginationResponse> { return executeRequest<PaginationResponse> {
apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter) apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter)

View File

@ -18,10 +18,15 @@


package im.vector.matrix.android.internal.session.room.timeline 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.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.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent 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.ChunkEntityFields
import im.vector.matrix.android.internal.database.model.EventEntity 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.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.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.PagingRequestHelper import im.vector.matrix.android.internal.util.PagingRequestHelper
import io.realm.OrderedCollectionChangeSet 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 java.util.* import java.util.*
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import kotlin.collections.ArrayList 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( internal class DefaultTimeline(
private val roomId: String, private val roomId: String,
@ -54,16 +57,24 @@ internal class DefaultTimeline(
) : Timeline { ) : Timeline {


override var listener: Timeline.Listener? = null 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<HandlerThread>()
private val handler = AtomicReference<Handler>()
private val realm = AtomicReference<Realm>()

private val cancelableBag = CancelableBag()
private lateinit var liveEvents: RealmResults<EventEntity> private lateinit var liveEvents: RealmResults<EventEntity>
private var prevDisplayIndex: Int = 0 private var prevDisplayIndex: Int = 0
private var nextDisplayIndex: Int = 0 private var nextDisplayIndex: Int = 0
private val isLive = initialEventId == null private val isLive = initialEventId == null
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList()) private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())



private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<EventEntity>> { _, changeSet ->
private val changeListener = OrderedRealmCollectionChangeListener<RealmResults<EventEntity>> { _, changeSet ->
if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) { if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) {
handleInitialLoad() handleInitialLoad()
} else { } else {
@ -78,32 +89,48 @@ internal class DefaultTimeline(
} }
} }


@UiThread
override fun paginate(direction: Timeline.Direction, count: Int) { override fun paginate(direction: Timeline.Direction, count: Int) {
if (direction == Timeline.Direction.FORWARDS && isLive) { handler.get()?.post {
return if (!hasMoreToLoadLive(direction) && hasReachedEndLive(direction)) {
} return@post
val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex }
val hasBuiltCountItems = insertFromLiveResults(startDisplayIndex, direction, count.toLong()) val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex
if (hasBuiltCountItems.not()) { val builtCountItems = insertFromLiveResults(startDisplayIndex, direction, count.toLong())
val token = getToken(direction) ?: return if (builtCountItems < count) {
helper.runIfNotRunning(direction.toRequestType()) { val limit = count - builtCountItems
executePaginationTask(it, token, direction.toPaginationDirection(), 30) val token = getTokenLive(direction) ?: return@post
helper.runIfNotRunning(direction.toRequestType()) { executePaginationTask(it, token, direction, limit) }
} }
} }
} }


@UiThread
override fun start() { override fun start() {
realm = Realm.getInstance(realmConfiguration) if (isStarted.compareAndSet(false, true)) {
liveEvents = buildQuery(initialEventId).findAllAsync() val handlerThread = HandlerThread(THREAD_NAME)
liveEvents.addChangeListener(changeListener) 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() { override fun dispose() {
liveEvents.removeAllChangeListeners() if (isStarted.compareAndSet(true, false)) {
realm.close() handler.get()?.post {
cancelableBag.cancel()
liveEvents.removeAllChangeListeners()
realm.getAndSet(null)?.close()
handler.set(null)
handlerThread.getAndSet(null)?.quit()
}
}
} }


override fun snapshot(): List<TimelineEvent> = synchronized(builtEvents) { override fun snapshot(): List<TimelineEvent> = synchronized(builtEvents) {
@ -114,6 +141,21 @@ internal class DefaultTimeline(
return builtEvents.size 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) { private fun handleInitialLoad() = synchronized(builtEvents) {
val initialDisplayIndex = if (isLive) { val initialDisplayIndex = if (isLive) {
liveEvents.firstOrNull()?.displayIndex liveEvents.firstOrNull()?.displayIndex
@ -138,19 +180,22 @@ internal class DefaultTimeline(


private fun executePaginationTask(requestCallback: PagingRequestHelper.Request.Callback, private fun executePaginationTask(requestCallback: PagingRequestHelper.Request.Callback,
from: String, from: String,
direction: PaginationDirection, direction: Timeline.Direction,
limit: Int) { limit: Int) {


val params = PaginationTask.Params(roomId = roomId, val params = PaginationTask.Params(roomId = roomId,
from = from, from = from,
direction = direction, direction = direction.toPaginationDirection(),
limit = limit) limit = limit)


paginationTask.configureWith(params) paginationTask.configureWith(params)
.enableRetry() .enableRetry()
.dispatchTo(object : MatrixCallback<Boolean> { .dispatchTo(object : MatrixCallback<TokenChunkEventPersistor.Result> {
override fun onSuccess(data: Boolean) { override fun onSuccess(data: TokenChunkEventPersistor.Result) {
requestCallback.recordSuccess() requestCallback.recordSuccess()
if (data == TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE) {
paginate(direction, limit)
}
} }


override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
@ -158,26 +203,63 @@ internal class DefaultTimeline(
} }
}) })
.executeBy(taskExecutor) .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 return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken
} }


/** /**
* This has to be called on MonarchyThread as it access realm live results * This has to be called on TimelineThread as it access realm live results
* @return true if count items has been added */
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, private fun insertFromLiveResults(startDisplayIndex: Int,
direction: Timeline.Direction, direction: Timeline.Direction,
count: Long): Boolean = synchronized(builtEvents) { count: Long): Int = synchronized(builtEvents) {
if (count < 1) { if (count < 1) {
throw java.lang.IllegalStateException("You should provide a count superior to 0") throw java.lang.IllegalStateException("You should provide a count superior to 0")
} }
val offsetResults = getOffsetResults(startDisplayIndex, direction, count) val offsetResults = getOffsetResults(startDisplayIndex, direction, count)
if (offsetResults.isEmpty()) { if (offsetResults.isEmpty()) {
return false return 0
} }
val offsetIndex = offsetResults.last()!!.displayIndex val offsetIndex = offsetResults.last()!!.displayIndex
if (direction == Timeline.Direction.BACKWARDS) { if (direction == Timeline.Direction.BACKWARDS) {
@ -191,9 +273,12 @@ internal class DefaultTimeline(
builtEvents.add(position, timelineEvent) builtEvents.add(position, timelineEvent)
} }
listener?.onUpdated(snapshot()) 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, private fun getOffsetResults(startDisplayIndex: Int,
direction: Timeline.Direction, direction: Timeline.Direction,
count: Long): RealmResults<EventEntity> { count: Long): RealmResults<EventEntity> {
@ -210,21 +295,33 @@ internal class DefaultTimeline(
return offsetQuery.limit(count).findAll() return offsetQuery.limit(count).findAll()
} }


private fun buildQuery(eventId: String?): RealmQuery<EventEntity> { private fun buildEventQuery(realm: Realm): RealmQuery<EventEntity> {
val query = if (eventId == null) { val query = if (initialEventId == null) {
EventEntity EventEntity
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY) .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 { } else {
EventEntity EventEntity
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH) .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) query.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
return query return query


} }


private fun <T> Handler.postAndWait(runnable: () -> T): T {
val lock = CountDownLatch(1)
val atomicReference = AtomicReference<T>()
post {
val result = runnable()
atomicReference.set(result)
lock.countDown()
}
lock.await()
return atomicReference.get()
}

private fun Timeline.Direction.toRequestType(): PagingRequestHelper.RequestType { private fun Timeline.Direction.toRequestType(): PagingRequestHelper.RequestType {
return if (this == Timeline.Direction.BACKWARDS) PagingRequestHelper.RequestType.BEFORE else PagingRequestHelper.RequestType.AFTER 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 { private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
} }
}
}

View File

@ -16,12 +16,9 @@


package im.vector.matrix.android.internal.session.room.timeline package im.vector.matrix.android.internal.session.room.timeline


import androidx.paging.PagedList
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.room.timeline.Timeline 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.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.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.where 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.task.configureWith
import im.vector.matrix.android.internal.util.PagingRequestHelper import im.vector.matrix.android.internal.util.PagingRequestHelper
import im.vector.matrix.android.internal.util.tryTransactionAsync 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 private const val EVENT_NOT_FOUND_INDEX = -1


internal class DefaultTimelineService(private val roomId: String, internal class DefaultTimelineService(private val roomId: String,
@ -46,8 +38,6 @@ internal class DefaultTimelineService(private val roomId: String,
private val helper: PagingRequestHelper private val helper: PagingRequestHelper
) : TimelineService { ) : TimelineService {


private val eventInterceptors = ArrayList<TimelineEventInterceptor>()

override fun createTimeline(eventId: String?): Timeline { override fun createTimeline(eventId: String?): Timeline {
return DefaultTimeline(roomId, eventId, monarchy.realmConfiguration, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask, helper) 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) 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() { private fun clearUnlinkedEvents() {
monarchy.tryTransactionAsync { realm -> monarchy.tryTransactionAsync { realm ->
val unlinkedEvents = EventEntity val unlinkedEvents = EventEntity
@ -101,18 +82,5 @@ internal class DefaultTimelineService(private val roomId: String,
return displayIndex return displayIndex
} }


private fun buildDataSourceFactoryQuery(realm: Realm, eventId: String?): RealmQuery<EventEntity> {
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)
}



} }

View File

@ -95,8 +95,8 @@ internal class TimelineBoundaryCallback(private val roomId: String,


paginationTask.configureWith(params) paginationTask.configureWith(params)
.enableRetry() .enableRetry()
.dispatchTo(object : MatrixCallback<Boolean> { .dispatchTo(object : MatrixCallback<TokenChunkEventPersistor.Result> {
override fun onSuccess(data: Boolean) { override fun onSuccess(data: TokenChunkEventPersistor.Result) {
requestCallback.recordSuccess() requestCallback.recordSuccess()
} }



View File

@ -36,13 +36,15 @@ import io.realm.kotlin.createObject


internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {


enum class Result {
SHOULD_FETCH_MORE,
SUCCESS
}

fun insertInDb(receivedChunk: TokenChunkEvent, fun insertInDb(receivedChunk: TokenChunkEvent,
roomId: String, roomId: String,
direction: PaginationDirection): Try<Boolean> { direction: PaginationDirection): Try<Result> {


if (receivedChunk.events.isEmpty() && receivedChunk.stateEvents.isEmpty()) {
return Try.just(false)
}
return monarchy return monarchy
.tryTransactionSync { realm -> .tryTransactionSync { realm ->
val roomEntity = RoomEntity.where(realm, roomId).findFirst() val roomEntity = RoomEntity.where(realm, roomId).findFirst()
@ -71,27 +73,36 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
nextChunk?.apply { this.prevToken = prevToken } nextChunk?.apply { this.prevToken = prevToken }
?: ChunkEntity.create(realm, prevToken, nextToken) ?: ChunkEntity.create(realm, prevToken, nextToken)
} }

if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked()) currentChunk.isLastBackward = true

// 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 { } else {
val newEventIds = receivedChunk.events.mapNotNull { it.eventId } currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
ChunkEntity
.findAllIncludingEvents(realm, newEventIds) // Then we merge chunks if needed
.filter { it != currentChunk } if (currentChunk != prevChunk && prevChunk != null) {
.forEach { overlapped -> currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
currentChunk = handleMerge(roomEntity, direction, currentChunk, overlapped) } 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, private fun handleMerge(roomEntity: RoomEntity,

View File

@ -51,7 +51,6 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
data class INVITED(val data: Map<String, InvitedRoomSync>) : HandlingStrategy() data class INVITED(val data: Map<String, InvitedRoomSync>) : HandlingStrategy()
data class LEFT(val data: Map<String, RoomSync>) : HandlingStrategy() data class LEFT(val data: Map<String, RoomSync>) : HandlingStrategy()
} }

fun handle(roomsSyncResponse: RoomsSyncResponse) { fun handle(roomsSyncResponse: RoomsSyncResponse) {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
handleRoomSync(realm, RoomSyncHandler.HandlingStrategy.JOINED(roomsSyncResponse.join)) handleRoomSync(realm, RoomSyncHandler.HandlingStrategy.JOINED(roomsSyncResponse.join))
@ -164,8 +163,8 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken } realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken }
} }


lastChunk?.isLast = false lastChunk?.isLastForward = false
chunkEntity.isLast = true chunkEntity.isLastForward = true
chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS, stateIndexOffset) chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS, stateIndexOffset)
return chunkEntity return chunkEntity
} }