Timeline : make tests compile and pass

This commit is contained in:
ganfra 2019-04-01 15:18:52 +02:00
parent 94db36d6c4
commit be6a4efacb
9 changed files with 199 additions and 118 deletions

View File

@ -28,6 +28,7 @@ android {
targetSdkVersion 28 targetSdkVersion 28
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
@ -104,17 +105,17 @@ dependencies {
testImplementation 'org.robolectric:shadows-support-v4:3.0' testImplementation 'org.robolectric:shadows-support-v4:3.0'
testImplementation "io.mockk:mockk:1.8.13.kotlin13" testImplementation "io.mockk:mockk:1.8.13.kotlin13"
testImplementation 'org.amshove.kluent:kluent-android:1.44' testImplementation 'org.amshove.kluent:kluent-android:1.44'
testImplementation "androidx.arch.core:core-testing:$lifecycle_version"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
androidTestImplementation "org.koin:koin-test:$koin_version" androidTestImplementation "org.koin:koin-test:$koin_version"
androidTestImplementation 'androidx.test:core:1.1.0'
androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test:rules:1.1.1' androidTestImplementation 'androidx.test:rules:1.1.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44' androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
androidTestImplementation "io.mockk:mockk-android:1.8.13.kotlin13" androidTestImplementation "io.mockk:mockk-android:1.8.13.kotlin13"
androidTestImplementation "androidx.arch.core:core-testing:$lifecycle_version" androidTestImplementation "androidx.arch.core:core-testing:$lifecycle_version"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
} }

View File

@ -16,10 +16,9 @@
package im.vector.matrix.android.session.room.timeline package im.vector.matrix.android.session.room.timeline
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.database.helper.add import im.vector.matrix.android.internal.database.helper.add
import im.vector.matrix.android.internal.database.helper.addAll import im.vector.matrix.android.internal.database.helper.addAll
import im.vector.matrix.android.internal.database.helper.isUnlinked import im.vector.matrix.android.internal.database.helper.isUnlinked
@ -27,6 +26,9 @@ import im.vector.matrix.android.internal.database.helper.lastStateIndex
import im.vector.matrix.android.internal.database.helper.merge import im.vector.matrix.android.internal.database.helper.merge
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeMessageEvent
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeRoomMemberEvent
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
@ -35,9 +37,10 @@ import org.amshove.kluent.shouldBeTrue
import org.amshove.kluent.shouldEqual import org.amshove.kluent.shouldEqual
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import kotlin.random.Random import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
internal class ChunkEntityTest : InstrumentedTest { internal class ChunkEntityTest : InstrumentedTest {
private lateinit var monarchy: Monarchy private lateinit var monarchy: Monarchy
@ -54,7 +57,7 @@ internal class ChunkEntityTest : InstrumentedTest {
fun add_shouldAdd_whenNotAlreadyIncluded() { fun add_shouldAdd_whenNotAlreadyIncluded() {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject() val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeEvent(false) val fakeEvent = createFakeMessageEvent()
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.events.size shouldEqual 1 chunk.events.size shouldEqual 1
} }
@ -64,7 +67,7 @@ internal class ChunkEntityTest : InstrumentedTest {
fun add_shouldNotAdd_whenAlreadyIncluded() { fun add_shouldNotAdd_whenAlreadyIncluded() {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject() val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeEvent(false) val fakeEvent = createFakeMessageEvent()
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.events.size shouldEqual 1 chunk.events.size shouldEqual 1
@ -75,7 +78,7 @@ internal class ChunkEntityTest : InstrumentedTest {
fun add_shouldStateIndexIncremented_whenStateEventIsAddedForward() { fun add_shouldStateIndexIncremented_whenStateEventIsAddedForward() {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject() val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeEvent(true) val fakeEvent = createFakeRoomMemberEvent()
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 1 chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 1
} }
@ -85,7 +88,7 @@ internal class ChunkEntityTest : InstrumentedTest {
fun add_shouldStateIndexNotIncremented_whenNoStateEventIsAdded() { fun add_shouldStateIndexNotIncremented_whenNoStateEventIsAdded() {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject() val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeEvent(false) val fakeEvent = createFakeMessageEvent()
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 0 chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 0
} }
@ -196,15 +199,4 @@ internal class ChunkEntityTest : InstrumentedTest {
} }
} }
private fun createFakeListOfEvents(size: Int = 10): List<Event> {
return (0 until size).map { createFakeEvent(Random.nextBoolean()) }
}
private fun createFakeEvent(asStateEvent: Boolean = false): Event {
val eventId = Random.nextLong(System.currentTimeMillis()).toString()
val type = if (asStateEvent) EventType.STATE_ROOM_NAME else EventType.MESSAGE
return Event(type, eventId)
}
} }

View File

@ -17,9 +17,15 @@
package im.vector.matrix.android.session.room.timeline package im.vector.matrix.android.session.room.timeline
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.MyMembership import im.vector.matrix.android.api.session.room.model.MyMembership
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.internal.database.helper.addAll import im.vector.matrix.android.internal.database.helper.addAll
import im.vector.matrix.android.internal.database.helper.addOrUpdate import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
@ -30,27 +36,56 @@ import kotlin.random.Random
object RoomDataHelper { object RoomDataHelper {
private const val FAKE_TEST_SENDER = "@sender:test.org"
private val EVENT_FACTORIES = hashMapOf(
0 to { createFakeMessageEvent() },
1 to { createFakeRoomMemberEvent() }
)
fun createFakeListOfEvents(size: Int = 10): List<Event> { fun createFakeListOfEvents(size: Int = 10): List<Event> {
return (0 until size).map { createFakeEvent(Random.nextBoolean()) } return (0 until size).mapNotNull {
val nextInt = Random.nextInt(EVENT_FACTORIES.size)
EVENT_FACTORIES[nextInt]?.invoke()
}
} }
fun createFakeEvent(asStateEvent: Boolean = false): Event { fun createFakeEvent(type: String,
val eventId = Random.nextLong(System.currentTimeMillis()).toString() content: Content? = null,
val type = if (asStateEvent) EventType.STATE_ROOM_NAME else EventType.MESSAGE prevContent: Content? = null,
return Event(type, eventId) sender: String = FAKE_TEST_SENDER,
stateKey: String = FAKE_TEST_SENDER
): Event {
return Event(
type = type,
eventId = Random.nextLong().toString(),
content = content,
prevContent = prevContent,
sender = sender,
stateKey = stateKey
)
}
fun createFakeMessageEvent(): Event {
val message = MessageTextContent(MessageType.MSGTYPE_TEXT, "Fake message #${Random.nextLong()}").toContent()
return createFakeEvent(EventType.MESSAGE, message)
}
fun createFakeRoomMemberEvent(): Event {
val roomMember = RoomMember(Membership.JOIN, "Fake name #${Random.nextLong()}").toContent()
return createFakeEvent(EventType.STATE_ROOM_MEMBER, roomMember)
} }
fun fakeInitialSync(monarchy: Monarchy, roomId: String) { fun fakeInitialSync(monarchy: Monarchy, roomId: String) {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
val roomEntity = realm.createObject<RoomEntity>(roomId) val roomEntity = realm.createObject<RoomEntity>(roomId)
roomEntity.membership = MyMembership.JOINED roomEntity.membership = MyMembership.JOINED
val eventList = createFakeListOfEvents(30) val eventList = createFakeListOfEvents(10)
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()
isLastForward = true isLastForward = true
} }
chunkEntity.addAll("roomId", eventList, PaginationDirection.FORWARDS) chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS)
roomEntity.addOrUpdate(chunkEntity) roomEntity.addOrUpdate(chunkEntity)
} }
} }

View File

@ -1,71 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.session.room.timeline
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.annotation.UiThreadTest
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.LiveDataTestObserver
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.testCoroutineDispatchers
import io.realm.Realm
import io.realm.RealmConfiguration
import org.junit.Before
import org.junit.Rule
import org.junit.Test
internal class TimelineHolderTest : InstrumentedTest {
@get:Rule val testRule = InstantTaskExecutorRule()
private lateinit var monarchy: Monarchy
@Before
fun setup() {
Realm.init(context())
val testConfiguration = RealmConfiguration.Builder().name("test-realm").build()
Realm.deleteRealm(testConfiguration)
monarchy = Monarchy.Builder().setRealmConfiguration(testConfiguration).build()
}
@Test
@UiThreadTest
fun backPaginate_shouldLoadMoreEvents_whenLoadAroundIsCalled() {
val roomId = "roomId"
val taskExecutor = TaskExecutor(testCoroutineDispatchers)
val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy)
val paginationTask = FakePaginationTask(tokenChunkEventPersistor)
val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor)
RoomDataHelper.fakeInitialSync(monarchy, roomId)
val timelineHolder = DefaultTimelineService(roomId, monarchy, taskExecutor, getContextOfEventTask, RoomMemberExtractor(roomId))
val timelineObserver = LiveDataTestObserver.test(timelineHolder.timeline())
timelineObserver.awaitNextValue().assertHasValue()
var timelineData = timelineObserver.value()
timelineData.events.size shouldEqual 30
(0 until timelineData.events.size).map {
timelineData.events.loadAround(it)
}
timelineObserver.awaitNextValue().assertHasValue()
timelineData = timelineObserver.value()
timelineData.events.size shouldEqual 60
}
}

View File

@ -0,0 +1,91 @@
/*
* 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.session.room.timeline
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.testCoroutineDispatchers
import io.realm.Realm
import io.realm.RealmConfiguration
import org.amshove.kluent.shouldEqual
import org.junit.Before
import org.junit.Test
import timber.log.Timber
import java.util.concurrent.CountDownLatch
internal class TimelineTest : InstrumentedTest {
companion object {
private const val ROOM_ID = "roomId"
}
private lateinit var monarchy: Monarchy
@Before
fun setup() {
Timber.plant(Timber.DebugTree())
Realm.init(context())
val testConfiguration = RealmConfiguration.Builder().name("test-realm").build()
Realm.deleteRealm(testConfiguration)
monarchy = Monarchy.Builder().setRealmConfiguration(testConfiguration).build()
RoomDataHelper.fakeInitialSync(monarchy, ROOM_ID)
}
private fun createTimeline(initialEventId: String? = null): Timeline {
val taskExecutor = TaskExecutor(testCoroutineDispatchers)
val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy)
val paginationTask = FakePaginationTask(tokenChunkEventPersistor)
val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor)
val roomMemberExtractor = RoomMemberExtractor(ROOM_ID)
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor)
return DefaultTimeline(ROOM_ID, initialEventId, monarchy.realmConfiguration, taskExecutor, getContextOfEventTask, timelineEventFactory, paginationTask, null)
}
@Test
fun backPaginate_shouldLoadMoreEvents_whenPaginateIsCalled() {
val timeline = createTimeline()
timeline.start()
val paginationCount = 30
var initialLoad = 0
val latch = CountDownLatch(2)
var timelineEvents: List<TimelineEvent> = emptyList()
timeline.listener = object : Timeline.Listener {
override fun onUpdated(snapshot: List<TimelineEvent>) {
if (snapshot.isNotEmpty()) {
if (initialLoad == 0) {
initialLoad = snapshot.size
}
timelineEvents = snapshot
latch.countDown()
timeline.paginate(Timeline.Direction.BACKWARDS, paginationCount)
}
}
}
latch.await()
timelineEvents.size shouldEqual initialLoad + paginationCount
timeline.dispose()
}
}

View File

@ -35,6 +35,18 @@ inline fun <reified T> Content?.toModel(): T? {
} }
} }
/**
* This methods is a facility method to map a model to a json Content
*/
@Suppress("UNCHECKED_CAST")
inline fun <reified T> T?.toContent(): Content? {
return this?.let {
val moshi = MoshiProvider.providesMoshi()
val moshiAdapter = moshi.adapter(T::class.java)
return moshiAdapter.toJsonValue(it) as Content
}
}
/** /**
* Generic event class with all possible fields for events. * Generic event class with all possible fields for events.
* The content and prevContent json fields can easily be mapped to a model with [toModel] method. * The content and prevContent json fields can easily be mapped to a model with [toModel] method.

View File

@ -54,10 +54,14 @@ internal fun ChunkEntity.merge(roomId: String,
if (direction == PaginationDirection.FORWARDS) { if (direction == PaginationDirection.FORWARDS) {
this.nextToken = chunkToMerge.nextToken this.nextToken = chunkToMerge.nextToken
this.isLastForward = chunkToMerge.isLastForward this.isLastForward = chunkToMerge.isLastForward
this.forwardsStateIndex = chunkToMerge.forwardsStateIndex
this.forwardsDisplayIndex = chunkToMerge.forwardsDisplayIndex
eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
} else { } else {
this.prevToken = chunkToMerge.prevToken this.prevToken = chunkToMerge.prevToken
this.isLastBackward = chunkToMerge.isLastBackward this.isLastBackward = chunkToMerge.isLastBackward
this.backwardsStateIndex = chunkToMerge.backwardsStateIndex
this.backwardsDisplayIndex = chunkToMerge.backwardsDisplayIndex
eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
} }
eventsToMerge.forEach { eventsToMerge.forEach {
@ -111,8 +115,7 @@ internal fun ChunkEntity.add(roomId: String,
this.displayIndex = currentDisplayIndex this.displayIndex = currentDisplayIndex
} }
// We are not using the order of the list, but will be sorting with displayIndex field // We are not using the order of the list, but will be sorting with displayIndex field
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size events.add(eventEntity)
events.add(position, eventEntity)
} }
private fun ChunkEntity.assertIsManaged() { private fun ChunkEntity.assertIsManaged() {

View File

@ -48,7 +48,6 @@ internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI,
return if (areAllMembersAlreadyLoaded(params.roomId)) { return if (areAllMembersAlreadyLoaded(params.roomId)) {
Try.just(true) Try.just(true)
} else { } else {
//TODO use this token
val lastToken = syncTokenStore.getLastToken() val lastToken = syncTokenStore.getLastToken()
executeRequest<RoomMembersResponse> { executeRequest<RoomMembersResponse> {
apiCall = roomAPI.getMembers(params.roomId, lastToken, null, params.excludeMembership?.value) apiCall = roomAPI.getMembers(params.roomId, lastToken, null, params.excludeMembership?.value)

View File

@ -36,7 +36,12 @@ import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoo
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.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 io.realm.* import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.Sort
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -97,10 +102,14 @@ internal class DefaultTimeline(
val state = getPaginationState(direction) val state = getPaginationState(direction)
if (state.isPaginating) { if (state.isPaginating) {
// We are getting new items from pagination // We are getting new items from pagination
paginateInternal(startDisplayIndex, direction, state.requestedCount) val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, state.requestedCount)
if (shouldPostSnapshot) {
postSnapshot()
}
} else { } else {
// We are getting new items from sync // We are getting new items from sync
buildTimelineEvents(startDisplayIndex, direction, range.length.toLong()) buildTimelineEvents(startDisplayIndex, direction, range.length.toLong())
postSnapshot()
} }
} }
} }
@ -114,7 +123,10 @@ internal class DefaultTimeline(
} }
Timber.v("Paginate $direction of $count items") Timber.v("Paginate $direction of $count items")
val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex
paginateInternal(startDisplayIndex, direction, count) val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, count)
if (shouldPostSnapshot) {
postSnapshot()
}
} }
} }
@ -191,13 +203,15 @@ internal class DefaultTimeline(
/** /**
* This has to be called on TimelineThread as it access realm live results * This has to be called on TimelineThread as it access realm live results
* @return true if snapshot should be posted
*/ */
private fun paginateInternal(startDisplayIndex: Int, private fun paginateInternal(startDisplayIndex: Int,
direction: Timeline.Direction, direction: Timeline.Direction,
count: Int) { count: Int): Boolean {
updatePaginationState(direction) { it.copy(requestedCount = count, isPaginating = true) } updatePaginationState(direction) { it.copy(requestedCount = count, isPaginating = true) }
val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong()) val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong())
if (builtCount < count && !hasReachedEnd(direction)) { val shouldFetchMore = builtCount < count && !hasReachedEnd(direction)
if (shouldFetchMore) {
val newRequestedCount = count - builtCount val newRequestedCount = count - builtCount
updatePaginationState(direction) { it.copy(requestedCount = newRequestedCount) } updatePaginationState(direction) { it.copy(requestedCount = newRequestedCount) }
val fetchingCount = Math.max(MIN_FETCHING_COUNT, newRequestedCount) val fetchingCount = Math.max(MIN_FETCHING_COUNT, newRequestedCount)
@ -205,6 +219,7 @@ internal class DefaultTimeline(
} else { } else {
updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) } updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) }
} }
return !shouldFetchMore
} }
private fun snapshot(): List<TimelineEvent> { private fun snapshot(): List<TimelineEvent> {
@ -252,12 +267,13 @@ internal class DefaultTimeline(
} else { } else {
val count = Math.min(INITIAL_LOAD_SIZE, liveEvents.size) val count = Math.min(INITIAL_LOAD_SIZE, liveEvents.size)
if (isLive) { if (isLive) {
paginate(Timeline.Direction.BACKWARDS, count) paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count)
} else { } else {
paginate(Timeline.Direction.FORWARDS, count / 2) paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2)
paginate(Timeline.Direction.BACKWARDS, count / 2) paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count / 2)
} }
} }
postSnapshot()
} }
/** /**
@ -336,8 +352,6 @@ internal class DefaultTimeline(
builtEvents.add(position, timelineEvent) builtEvents.add(position, timelineEvent)
} }
Timber.v("Built ${offsetResults.size} items from db") Timber.v("Built ${offsetResults.size} items from db")
val snapshot = snapshot()
mainHandler.post { listener?.onUpdated(snapshot) }
return offsetResults.size return offsetResults.size
} }
@ -399,6 +413,11 @@ internal class DefaultTimeline(
contextOfEventTask.configureWith(params).executeBy(taskExecutor) contextOfEventTask.configureWith(params).executeBy(taskExecutor)
} }
private fun postSnapshot() {
val snapshot = snapshot()
mainHandler.post { listener?.onUpdated(snapshot) }
}
// Extension methods *************************************************************************** // Extension methods ***************************************************************************
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {