Merge feature/replace_paged_list into develop

This commit is contained in:
ganfra
2019-04-01 17:33:53 +02:00
73 changed files with 1486 additions and 1542 deletions

View File

@ -28,6 +28,7 @@ android {
targetSdkVersion 28
versionCode 1
versionName "1.0"
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
@ -77,7 +78,7 @@ static def gitRevisionDate() {
dependencies {
def arrow_version = "0.8.0"
def support_version = '1.1.0-alpha01'
def support_version = '1.1.0-alpha03'
def moshi_version = '1.8.0'
def lifecycle_version = '2.0.0'
def coroutines_version = "1.0.1"
@ -96,7 +97,7 @@ dependencies {
// Network
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.4.0'
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
implementation 'com.squareup.okhttp3:okhttp:3.11.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
implementation 'com.novoda:merlin:1.1.6'
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
@ -106,11 +107,8 @@ 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"
implementation "android.arch.work:work-runtime-ktx:1.0.0"
// FP
implementation "io.arrow-kt:arrow-core:$arrow_version"
@ -137,17 +135,17 @@ dependencies {
//testImplementation 'org.robolectric:shadows-support-v4:3.0'
testImplementation "io.mockk:mockk:1.8.13.kotlin13"
testImplementation 'org.amshove.kluent:kluent-android:1.44'
testImplementation "androidx.arch.core:core-testing:$lifecycle_version"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
androidTestImplementation "org.koin:koin-test:$koin_version"
androidTestImplementation 'androidx.test:core:1.1.0'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test:rules:1.1.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
androidTestImplementation "io.mockk:mockk-android:1.8.13.kotlin13"
androidTestImplementation "androidx.arch.core:core-testing:$lifecycle_version"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
}

View File

@ -16,10 +16,9 @@
package im.vector.matrix.android.session.room.timeline
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.database.helper.add
import im.vector.matrix.android.internal.database.helper.addAll
import im.vector.matrix.android.internal.database.helper.isUnlinked
@ -27,6 +26,9 @@ import im.vector.matrix.android.internal.database.helper.lastStateIndex
import im.vector.matrix.android.internal.database.helper.merge
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeMessageEvent
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeRoomMemberEvent
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.kotlin.createObject
@ -35,9 +37,10 @@ import org.amshove.kluent.shouldBeTrue
import org.amshove.kluent.shouldEqual
import org.junit.Before
import org.junit.Test
import kotlin.random.Random
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
internal class ChunkEntityTest : InstrumentedTest {
private lateinit var monarchy: Monarchy
@ -54,7 +57,7 @@ internal class ChunkEntityTest : InstrumentedTest {
fun add_shouldAdd_whenNotAlreadyIncluded() {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeEvent(false)
val fakeEvent = createFakeMessageEvent()
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.events.size shouldEqual 1
}
@ -64,7 +67,7 @@ internal class ChunkEntityTest : InstrumentedTest {
fun add_shouldNotAdd_whenAlreadyIncluded() {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeEvent(false)
val fakeEvent = createFakeMessageEvent()
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.events.size shouldEqual 1
@ -75,7 +78,7 @@ internal class ChunkEntityTest : InstrumentedTest {
fun add_shouldStateIndexIncremented_whenStateEventIsAddedForward() {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeEvent(true)
val fakeEvent = createFakeRoomMemberEvent()
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 1
}
@ -85,7 +88,7 @@ internal class ChunkEntityTest : InstrumentedTest {
fun add_shouldStateIndexNotIncremented_whenNoStateEventIsAdded() {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeEvent(false)
val fakeEvent = createFakeMessageEvent()
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 0
}
@ -134,13 +137,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()
}
}
@ -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

@ -25,7 +25,7 @@ import kotlin.random.Random
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 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 }
}

View File

@ -23,7 +23,7 @@ import kotlin.random.Random
internal class FakePaginationTask(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : PaginationTask {
override fun execute(params: PaginationTask.Params): Try<Boolean> {
override fun execute(params: PaginationTask.Params): Try<TokenChunkEventPersistor.Result> {
val fakeEvents = RoomDataHelper.createFakeListOfEvents(30)
val tokenChunkEvent = FakeTokenChunkEvent(params.from, Random.nextLong(System.currentTimeMillis()).toString(), fakeEvents)
return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, params.direction)

View File

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

View File

@ -16,61 +16,75 @@
package im.vector.matrix.android.session.room.timeline
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.annotation.UiThreadTest
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.LiveDataTestObserver
import im.vector.matrix.android.MainThreadExecutor
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback
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.internal.util.PagingRequestHelper
import im.vector.matrix.android.testCoroutineDispatchers
import io.realm.Realm
import io.realm.RealmConfiguration
import org.amshove.kluent.shouldEqual
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import timber.log.Timber
import java.util.concurrent.CountDownLatch
internal class TimelineHolderTest : InstrumentedTest {
internal class TimelineTest : InstrumentedTest {
companion object {
private const val ROOM_ID = "roomId"
}
@get:Rule val testRule = InstantTaskExecutorRule()
private lateinit var monarchy: Monarchy
@Before
fun setup() {
Timber.plant(Timber.DebugTree())
Realm.init(context())
val testConfiguration = RealmConfiguration.Builder().name("test-realm").build()
Realm.deleteRealm(testConfiguration)
monarchy = Monarchy.Builder().setRealmConfiguration(testConfiguration).build()
RoomDataHelper.fakeInitialSync(monarchy, ROOM_ID)
}
@Test
@UiThreadTest
fun backPaginate_shouldLoadMoreEvents_whenLoadAroundIsCalled() {
val roomId = "roomId"
private fun createTimeline(initialEventId: String? = null): Timeline {
val taskExecutor = TaskExecutor(testCoroutineDispatchers)
val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy)
val paginationTask = FakePaginationTask(tokenChunkEventPersistor)
val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor)
val boundaryCallback = TimelineBoundaryCallback(roomId, taskExecutor, paginationTask, monarchy, PagingRequestHelper(MainThreadExecutor()))
val roomMemberExtractor = RoomMemberExtractor(ROOM_ID)
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor)
return DefaultTimeline(ROOM_ID, initialEventId, monarchy.realmConfiguration, taskExecutor, getContextOfEventTask, timelineEventFactory, paginationTask, null)
}
RoomDataHelper.fakeInitialSync(monarchy, roomId)
val timelineHolder = DefaultTimelineService(roomId, monarchy, taskExecutor, boundaryCallback, getContextOfEventTask, RoomMemberExtractor(monarchy, 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)
@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)
}
}
}
timelineObserver.awaitNextValue().assertHasValue()
timelineData = timelineObserver.value()
timelineData.events.size shouldEqual 60
latch.await()
timelineEvents.size shouldEqual initialLoad + paginationCount
timeline.dispose()
}

View File

@ -50,11 +50,9 @@ class Matrix private constructor(context: Context) : MatrixKoinComponent {
val authModule = AuthModule().definition
MatrixKoinHolder.instance.loadModules(listOf(matrixModule, networkModule, authModule))
ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver)
val lastActiveSession = authenticator.getLastActiveSession()
if (lastActiveSession != null) {
currentSession = lastActiveSession.apply {
open()
}
authenticator.getLastActiveSession()?.also {
currentSession = it
it.open()
}
}

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.
* The content and prevContent json fields can easily be mapped to a model with [toModel] method.

View File

@ -0,0 +1,84 @@
/*
*
* * Copyright 2019 New Vector Ltd
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
*
*/
package im.vector.matrix.android.api.session.room.timeline
/**
* A Timeline instance represents a contiguous sequence of events in a room.
* <p>
* There are two kinds of timeline:
* <p>
* - live timelines: they process live events from the sync. You can paginate
* backwards but not forwards.
* <p>
* - past timelines: they start in the past from an `initialEventId`. You can paginate
* backwards and forwards.
*
*/
interface Timeline {
var listener: Timeline.Listener?
/**
* This should be called before any other method after creating the timeline. It ensures the underlying database is open
*/
fun start()
/**
* This should be called when you don't need the timeline. It ensures the underlying database get closed.
*/
fun dispose()
/**
* Check if the timeline can be enriched by paginating.
* @param the direction to check in
* @return true if timeline can be enriched
*/
fun hasMoreToLoad(direction: Direction): Boolean
/**
* This is the main method to enrich the timeline with new data.
* It will call the onUpdated method from [Listener] when the data will be processed.
* It also ensures only one pagination by direction is launched at a time, so you can safely call this multiple time in a row.
*/
fun paginate(direction: Direction, count: Int)
interface Listener {
/**
* Call when the timeline has been updated through pagination or sync.
* @param snapshot the most uptodate snapshot
*/
fun onUpdated(snapshot: List<TimelineEvent>)
}
/**
* This is used to paginate in one or another direction.
*/
enum class Direction {
/**
* It represents future events.
*/
FORWARDS,
/**
* It represents past events.
*/
BACKWARDS
}
}

View File

@ -27,6 +27,7 @@ import im.vector.matrix.android.api.session.room.model.RoomMember
data class TimelineEvent(
val root: Event,
val localId: String,
val displayIndex: Int,
val roomMember: RoomMember?
) {

View File

@ -16,20 +16,18 @@
package im.vector.matrix.android.api.session.room.timeline
import androidx.lifecycle.LiveData
/**
* This interface defines methods to interact with the timeline. It's implemented at the room level.
*/
interface TimelineService {
/**
* This is the main method of the service. It allows to listen for live [TimelineData].
* It's automatically refreshed as soon as timeline data gets updated, through sync or pagination.
*
* @param eventId: an optional eventId to start loading timeline around.
* @return the [LiveData] of [TimelineData]
* Instantiate a [Timeline] with an optional initial eventId, to be used with permalink.
* You can filter the type you want to grab with the allowedTypes param.
* @param eventId the optional initial eventId.
* @param allowedTypes the optional filter types
* @return the instantiated timeline
*/
fun timeline(eventId: String? = null): LiveData<TimelineData>
fun createTimeline(eventId: String?, allowedTypes: List<String>? = null): Timeline
}

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

@ -0,0 +1,49 @@
/*
*
* * Copyright 2019 New Vector Ltd
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
*
*/
package im.vector.matrix.android.internal.database
import androidx.lifecycle.LiveData
import io.realm.*
class RealmLiveData<T : RealmModel>(private val realmConfiguration: RealmConfiguration,
private val query: (Realm) -> RealmQuery<T>) : LiveData<RealmResults<T>>() {
private val listener = RealmChangeListener<RealmResults<T>> { results ->
value = results
}
private var realm: Realm? = null
private var results: RealmResults<T>? = null
override fun onActive() {
val realm = Realm.getInstance(realmConfiguration)
val results = query.invoke(realm).findAll()
value = results
results.addChangeListener(listener)
this.realm = realm
this.results = results
}
override fun onInactive() {
results?.removeChangeListener(listener)
results = null
realm?.close()
realm = null
}
}

View File

@ -26,6 +26,7 @@ import java.util.concurrent.atomic.AtomicReference
internal interface LiveEntityObserver {
fun start()
fun dispose()
fun isStarted(): Boolean
}
internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val monarchy: Monarchy)
@ -55,7 +56,9 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val m
}
}
// PRIVATE
override fun isStarted(): Boolean {
return isStarted.get()
}
private fun onChanged(realmResults: RealmResults<T>, changeSet: OrderedCollectionChangeSet) {
val insertionIndexes = changeSet.insertions
@ -64,9 +67,9 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val m
val inserted = realmResults.filterIndexed { index, _ -> insertionIndexes.contains(index) }
val updated = realmResults.filterIndexed { index, _ -> updateIndexes.contains(index) }
val deleted = realmResults.filterIndexed { index, _ -> deletionIndexes.contains(index) }
process(inserted, updated, deleted)
processChanges(inserted, updated, deleted)
}
abstract fun process(inserted: List<T>, updated: List<T>, deleted: List<T>)
protected abstract fun processChanges(inserted: List<T>, updated: List<T>, deleted: List<T>)
}

View File

@ -20,7 +20,6 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.mapper.updateWith
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
@ -29,18 +28,18 @@ import im.vector.matrix.android.internal.extensions.assertIsManaged
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) {
@ -55,11 +54,16 @@ internal fun ChunkEntity.merge(roomId: String,
val eventsToMerge: List<EventEntity>
if (direction == PaginationDirection.FORWARDS) {
this.nextToken = chunkToMerge.nextToken
this.isLast = chunkToMerge.isLast
eventsToMerge = chunkToMerge.events.reversed()
this.isLastForward = chunkToMerge.isLastForward
this.forwardsStateIndex = chunkToMerge.forwardsStateIndex
this.forwardsDisplayIndex = chunkToMerge.forwardsDisplayIndex
eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
} else {
this.prevToken = chunkToMerge.prevToken
eventsToMerge = chunkToMerge.events
this.isLastBackward = chunkToMerge.isLastBackward
this.backwardsStateIndex = chunkToMerge.backwardsStateIndex
this.backwardsDisplayIndex = chunkToMerge.backwardsDisplayIndex
eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
}
eventsToMerge.forEach {
add(roomId, it.asDomain(), direction, isUnlinked = isUnlinked)
@ -70,7 +74,7 @@ internal fun ChunkEntity.addAll(roomId: String,
events: List<Event>,
direction: PaginationDirection,
stateIndexOffset: Int = 0,
// Set to true for Event retrieved from a Permalink (i.e. not linked to live Chunk)
// Set to true for Event retrieved from a Permalink (i.e. not linked to live Chunk)
isUnlinked: Boolean = false) {
assertIsManaged()
events.forEach { event ->
@ -78,10 +82,6 @@ internal fun ChunkEntity.addAll(roomId: String,
}
}
internal fun ChunkEntity.updateDisplayIndexes() {
events.forEachIndexed { index, eventEntity -> eventEntity.displayIndex = index }
}
internal fun ChunkEntity.add(roomId: String,
event: Event,
direction: PaginationDirection,
@ -92,24 +92,44 @@ internal fun ChunkEntity.add(roomId: String,
if (event.eventId.isNullOrEmpty() || events.fastContains(event.eventId)) {
return
}
var currentDisplayIndex = lastDisplayIndex(direction, 0)
if (direction == PaginationDirection.FORWARDS) {
currentDisplayIndex += 1
forwardsDisplayIndex = currentDisplayIndex
} else {
currentDisplayIndex -= 1
backwardsDisplayIndex = currentDisplayIndex
}
var currentStateIndex = lastStateIndex(direction, defaultValue = stateIndexOffset)
if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(event.type)) {
currentStateIndex += 1
forwardsStateIndex = currentStateIndex
} else if (direction == PaginationDirection.BACKWARDS && events.isNotEmpty()) {
val lastEventType = events.last()?.type ?: ""
if (EventType.isStateEvent(lastEventType)) {
currentStateIndex -= 1
backwardsStateIndex = currentStateIndex
}
}
val eventEntity = event.toEntity(roomId)
eventEntity.updateWith(currentStateIndex, isUnlinked)
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size
events.add(position, eventEntity)
val eventEntity = event.toEntity(roomId).apply {
this.stateIndex = currentStateIndex
this.isUnlinked = isUnlinked
this.displayIndex = currentDisplayIndex
}
// We are not using the order of the list, but will be sorting with displayIndex field
events.add(eventEntity)
}
internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
return when (direction) {
PaginationDirection.FORWARDS -> forwardsDisplayIndex
PaginationDirection.BACKWARDS -> backwardsDisplayIndex
} ?: defaultValue
}
internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
return when (direction) {
PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING).findFirst()?.stateIndex
PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.ASCENDING).findFirst()?.stateIndex
PaginationDirection.FORWARDS -> forwardsStateIndex
PaginationDirection.BACKWARDS -> backwardsStateIndex
} ?: defaultValue
}

View File

@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.database.helper
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.mapper.updateWith
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.fastContains
@ -31,7 +30,6 @@ internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) {
}
internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) {
chunkEntity.updateDisplayIndexes()
if (!chunks.contains(chunkEntity)) {
chunks.add(chunkEntity)
}
@ -47,8 +45,10 @@ internal fun RoomEntity.addStateEvents(stateEvents: List<Event>,
if (event.eventId == null || (filterDuplicates && fastContains(event.eventId))) {
return@forEach
}
val eventEntity = event.toEntity(roomId)
eventEntity.updateWith(stateIndex, isUnlinked)
val eventEntity = event.toEntity(roomId).apply {
this.stateIndex = stateIndex
this.isUnlinked = isUnlinked
}
untimelinedStateEvents.add(eventEntity)
}
}

View File

@ -57,11 +57,6 @@ internal object EventMapper {
}
internal fun EventEntity.updateWith(stateIndex: Int, isUnlinked: Boolean) {
this.stateIndex = stateIndex
this.isUnlinked = isUnlinked
}
internal fun EventEntity.asDomain(): Event {
return EventMapper.map(this)
}

View File

@ -19,12 +19,18 @@ package im.vector.matrix.android.internal.database.model
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.Index
import io.realm.annotations.LinkingObjects
internal open class ChunkEntity(var prevToken: String? = null,
var nextToken: String? = null,
var isLast: Boolean = false,
var events: RealmList<EventEntity> = RealmList()
internal open class ChunkEntity(@Index var prevToken: String? = null,
@Index var nextToken: String? = null,
var events: RealmList<EventEntity> = RealmList(),
@Index var isLastForward: Boolean = false,
@Index var isLastBackward: Boolean = false,
var backwardsDisplayIndex: Int? = null,
var forwardsDisplayIndex: Int? = null,
var backwardsStateIndex: Int? = null,
var forwardsStateIndex: Int? = null
) : RealmObject() {
@LinkingObjects("chunks")

View File

@ -18,24 +18,25 @@ package im.vector.matrix.android.internal.database.model
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.Index
import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey
import java.util.*
internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUID().toString(),
var eventId: String = "",
@Index var eventId: String = "",
var roomId: String = "",
var type: String = "",
@Index var type: String = "",
var content: String? = null,
var prevContent: String? = null,
var stateKey: String? = null,
@Index var stateKey: String? = null,
var originServerTs: Long? = null,
var sender: String? = null,
@Index var sender: String? = null,
var age: Long? = 0,
var redacts: String? = null,
var stateIndex: Int = 0,
var displayIndex: Int = 0,
var isUnlinked: Boolean = false
@Index var stateIndex: Int = 0,
@Index var displayIndex: Int = 0,
@Index var isUnlinked: Boolean = false
) : RealmObject() {
enum class LinkFilterMode {

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? {
return where(realm, roomId)
.equalTo(ChunkEntityFields.IS_LAST, true)
.equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
.findFirst()
}

View File

@ -59,7 +59,7 @@ internal fun EventEntity.Companion.latestEvent(realm: Realm,
query?.not()?.`in`(EventEntityFields.TYPE, excludedTypes.toTypedArray())
}
return query
?.sort(EventEntityFields.DISPLAY_INDEX)
?.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
?.findFirst()
}
@ -77,7 +77,7 @@ internal fun RealmQuery<EventEntity>.next(from: Int? = null, strict: Boolean = t
.findFirst()
}
internal fun RealmQuery<EventEntity>.last(since: Int? = null, strict: Boolean = false): EventEntity? {
internal fun RealmQuery<EventEntity>.prev(since: Int? = null, strict: Boolean = false): EventEntity? {
if (since != null) {
if (strict) {
this.lessThan(EventEntityFields.STATE_INDEX, since)

View File

@ -35,7 +35,6 @@ import im.vector.matrix.android.internal.session.room.members.RoomMemberDisplayN
import im.vector.matrix.android.internal.session.room.prune.EventsPruner
import im.vector.matrix.android.internal.session.user.DefaultUserService
import im.vector.matrix.android.internal.session.user.UserEntityUpdater
import im.vector.matrix.android.internal.session.user.UserModule
import im.vector.matrix.android.internal.util.md5
import io.realm.RealmConfiguration
import org.koin.dsl.module.module
@ -84,13 +83,17 @@ internal class SessionModule(private val sessionParams: SessionParams) {
}
scope(DefaultSession.SCOPE) {
RoomDisplayNameResolver(get(), get(), sessionParams.credentials)
RoomDisplayNameResolver(get(), get(), get(), sessionParams.credentials)
}
scope(DefaultSession.SCOPE) {
RoomAvatarResolver(get(), sessionParams.credentials)
}
scope(DefaultSession.SCOPE) {
RoomSummaryUpdater(get(), get(), get())
}
scope(DefaultSession.SCOPE) {
DefaultRoomService(get(), get()) as RoomService
}
@ -112,11 +115,10 @@ internal class SessionModule(private val sessionParams: SessionParams) {
}
scope(DefaultSession.SCOPE) {
val roomSummaryUpdater = RoomSummaryUpdater(get(), get(), get(), get(), sessionParams.credentials)
val groupSummaryUpdater = GroupSummaryUpdater(get())
val eventsPruner = EventsPruner(get())
val userEntityUpdater = UserEntityUpdater(get(), get(), get())
listOf<LiveEntityObserver>(roomSummaryUpdater, groupSummaryUpdater, eventsPruner, userEntityUpdater)
listOf<LiveEntityObserver>(groupSummaryUpdater, eventsPruner, userEntityUpdater)
}

View File

@ -23,6 +23,7 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.GroupEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.util.WorkerParamsFactory
@ -38,7 +39,7 @@ internal class GroupSummaryUpdater(monarchy: Monarchy
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
override fun process(inserted: List<GroupEntity>, updated: List<GroupEntity>, deleted: List<GroupEntity>) {
override fun processChanges(inserted: List<GroupEntity>, updated: List<GroupEntity>, deleted: List<GroupEntity>) {
val newGroupIds = inserted.map { it.groupId }
val getGroupDataWorkerParams = GetGroupDataWorker.Params(newGroupIds)
val workData = WorkerParamsFactory.toData(getGroupDataWorkerParams)

View File

@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.room.read.ReadService
import im.vector.matrix.android.api.session.room.send.SendService
import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.RealmLiveData
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
@ -45,18 +46,16 @@ internal class DefaultRoom(
) : Room,
TimelineService by timelineService,
SendService by sendService,
ReadService by readService {
TimelineService by timelineService,
SendService by sendService,
ReadService by readService {
override val roomSummary: LiveData<RoomSummary> by lazy {
val liveData = monarchy
.findAllMappedWithChanges(
{ realm -> RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) },
{ from -> from.asDomain() })
Transformations.map(liveData) {
it.first()
val liveRealmData = RealmLiveData<RoomSummaryEntity>(monarchy.realmConfiguration) { realm ->
RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
}
Transformations.map(liveRealmData) { results ->
results.map { it.asDomain() }.first()
}
}

View File

@ -25,7 +25,7 @@ import im.vector.matrix.android.api.session.room.model.RoomAvatarContent
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.last
import im.vector.matrix.android.internal.database.query.prev
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.members.RoomMembers
@ -41,7 +41,7 @@ internal class RoomAvatarResolver(private val monarchy: Monarchy,
var res: String? = null
monarchy.doWithRealm { realm ->
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_AVATAR).last()?.asDomain()
val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_AVATAR).prev()?.asDomain()
res = roomName?.content.toModel<RoomAvatarContent>()?.avatarUrl
if (!res.isNullOrEmpty()) {
return@doWithRealm

View File

@ -28,10 +28,8 @@ import im.vector.matrix.android.internal.session.room.send.EventFactory
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.PagingRequestHelper
import java.util.concurrent.Executors
internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask,
private val monarchy: Monarchy,
@ -43,10 +41,9 @@ internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask,
private val taskExecutor: TaskExecutor) {
fun instantiate(roomId: String): Room {
val helper = PagingRequestHelper(Executors.newSingleThreadExecutor())
val timelineBoundaryCallback = TimelineBoundaryCallback(roomId, taskExecutor, paginationTask, monarchy, helper)
val roomMemberExtractor = RoomMemberExtractor(monarchy, roomId)
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, timelineBoundaryCallback, contextOfEventTask, roomMemberExtractor)
val roomMemberExtractor = RoomMemberExtractor(roomId)
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor)
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask)
val sendService = DefaultSendService(roomId, eventFactory, monarchy)
val readService = DefaultReadService(roomId, monarchy, setReadMarkersTask, taskExecutor)
return DefaultRoom(

View File

@ -22,11 +22,7 @@ import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTas
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
import im.vector.matrix.android.internal.session.room.send.EventFactory
import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
import im.vector.matrix.android.internal.session.room.timeline.*
import org.koin.dsl.module.module
import retrofit2.Retrofit
@ -41,7 +37,7 @@ class RoomModule {
}
scope(DefaultSession.SCOPE) {
DefaultLoadRoomMembersTask(get(), get(), get()) as LoadRoomMembersTask
DefaultLoadRoomMembersTask(get(), get(), get(), get()) as LoadRoomMembersTask
}
scope(DefaultSession.SCOPE) {
@ -57,7 +53,7 @@ class RoomModule {
}
scope(DefaultSession.SCOPE) {
DefaultSetReadMarkersTask(get(), get(),get()) as SetReadMarkersTask
DefaultSetReadMarkersTask(get(), get(), get()) as SetReadMarkersTask
}
scope(DefaultSession.SCOPE) {

View File

@ -1,74 +1,79 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* * 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.
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.room
import android.content.Context
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.last
import im.vector.matrix.android.internal.database.query.latestEvent
import im.vector.matrix.android.internal.database.query.prev
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver
import im.vector.matrix.android.internal.session.room.members.RoomMembers
import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary
import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications
import io.realm.Realm
import io.realm.kotlin.createObject
internal class RoomSummaryUpdater(monarchy: Monarchy,
internal class RoomSummaryUpdater(private val credentials: Credentials,
private val roomDisplayNameResolver: RoomDisplayNameResolver,
private val roomAvatarResolver: RoomAvatarResolver,
private val context: Context,
private val credentials: Credentials
) : RealmLiveEntityObserver<RoomEntity>(monarchy) {
private val roomAvatarResolver: RoomAvatarResolver) {
override val query = Monarchy.Query<RoomEntity> { RoomEntity.where(it) }
fun update(realm: Realm,
roomId: String,
roomSummary: RoomSyncSummary? = null,
unreadNotifications: RoomSyncUnreadNotifications? = null) {
override fun process(inserted: List<RoomEntity>, updated: List<RoomEntity>, deleted: List<RoomEntity>) {
val rooms = (inserted + updated).map { it.roomId }
monarchy.writeAsync { realm ->
rooms.forEach { updateRoom(realm, it) }
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
if (roomSummary != null) {
if (roomSummary.heroes.isNotEmpty()) {
roomSummaryEntity.heroes.clear()
roomSummaryEntity.heroes.addAll(roomSummary.heroes)
}
if (roomSummary.invitedMembersCount != null) {
roomSummaryEntity.invitedMembersCount = roomSummary.invitedMembersCount
}
if (roomSummary.joinedMembersCount != null) {
roomSummaryEntity.joinedMembersCount = roomSummary.joinedMembersCount
}
}
}
private fun updateRoom(realm: Realm, roomId: String?) {
if (roomId == null) {
return
if (unreadNotifications?.highlightCount != null) {
roomSummaryEntity.highlightCount = unreadNotifications.highlightCount
}
if (unreadNotifications?.notificationCount != null) {
roomSummaryEntity.notificationCount = unreadNotifications.notificationCount
}
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
val lastEvent = EventEntity.latestEvent(realm, roomId, includedTypes = listOf(EventType.MESSAGE))
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).last()?.asDomain()
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain()
val otherRoomMembers = RoomMembers(realm, roomId).getLoaded().filterKeys { it != credentials.userId }
roomSummary.displayName = roomDisplayNameResolver.resolve(context, roomId).toString()
roomSummary.avatarUrl = roomAvatarResolver.resolve(roomId)
roomSummary.topic = lastTopicEvent?.content.toModel<RoomTopicContent>()?.topic
roomSummary.lastMessage = lastEvent
roomSummary.otherMemberIds.clear()
roomSummary.otherMemberIds.addAll(otherRoomMembers.keys)
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
roomSummaryEntity.topic = lastTopicEvent?.content.toModel<RoomTopicContent>()?.topic
roomSummaryEntity.lastMessage = lastEvent
roomSummaryEntity.otherMemberIds.clear()
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers.keys)
}
}

View File

@ -24,9 +24,11 @@ import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.tryTransactionSync
import io.realm.kotlin.createObject
internal interface LoadRoomMembersTask : Task<LoadRoomMembersTask.Params, Boolean> {
@ -38,17 +40,17 @@ internal interface LoadRoomMembersTask : Task<LoadRoomMembersTask.Params, Boolea
internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI,
private val monarchy: Monarchy,
private val syncTokenStore: SyncTokenStore
private val syncTokenStore: SyncTokenStore,
private val roomSummaryUpdater: RoomSummaryUpdater
) : LoadRoomMembersTask {
override fun execute(params: LoadRoomMembersTask.Params): Try<Boolean> {
return if (areAllMembersAlreadyLoaded(params.roomId)) {
Try.just(true)
} else {
//TODO use this token
val lastToken = syncTokenStore.getLastToken()
executeRequest<RoomMembersResponse> {
apiCall = roomAPI.getMembers(params.roomId, null, null, params.excludeMembership?.value)
apiCall = roomAPI.getMembers(params.roomId, lastToken, null, params.excludeMembership?.value)
}.flatMap { response ->
insertInDb(response, params.roomId)
}.map { true }
@ -60,22 +62,24 @@ internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI,
.tryTransactionSync { realm ->
// We ignore all the already known members
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: throw IllegalStateException("You shouldn't use this method without a room")
?: realm.createObject(roomId)
val roomMembers = RoomMembers(realm, roomId).getLoaded()
val eventsToInsert = response.roomMemberEvents.filter { !roomMembers.containsKey(it.stateKey) }
roomEntity.addStateEvents(eventsToInsert)
roomEntity.areAllMembersLoaded = true
roomSummaryUpdater.update(realm, roomId)
}
.map { response }
}
private fun areAllMembersAlreadyLoaded(roomId: String): Boolean {
return monarchy
.fetchAllCopiedSync { RoomEntity.where(it, roomId) }
.firstOrNull()
?.areAllMembersLoaded ?: false
.fetchAllCopiedSync { RoomEntity.where(it, roomId) }
.firstOrNull()
?.areAllMembersLoaded ?: false
}
}

View File

@ -30,13 +30,14 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.last
import im.vector.matrix.android.internal.database.query.prev
import im.vector.matrix.android.internal.database.query.where
/**
* This class computes room display name
*/
internal class RoomDisplayNameResolver(private val monarchy: Monarchy,
internal class RoomDisplayNameResolver(private val context: Context,
private val monarchy: Monarchy,
private val roomMemberDisplayNameResolver: RoomMemberDisplayNameResolver,
private val credentials: Credentials
) {
@ -44,11 +45,10 @@ internal class RoomDisplayNameResolver(private val monarchy: Monarchy,
/**
* Compute the room display name
*
* @param context
* @param roomId: the roomId to resolve the name of.
* @return the room display name
*/
fun resolve(context: Context, roomId: String): CharSequence {
fun resolve(roomId: String): CharSequence {
// this algorithm is the one defined in
// https://github.com/matrix-org/matrix-js-sdk/blob/develop/lib/models/room.js#L617
// calculateRoomName(room, userId)
@ -58,19 +58,19 @@ internal class RoomDisplayNameResolver(private val monarchy: Monarchy,
var name: CharSequence? = null
monarchy.doWithRealm { realm ->
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_NAME).last()?.asDomain()
val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_NAME).prev()?.asDomain()
name = roomName?.content.toModel<RoomNameContent>()?.name
if (!name.isNullOrEmpty()) {
return@doWithRealm
}
val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).last()?.asDomain()
val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).prev()?.asDomain()
name = canonicalAlias?.content.toModel<RoomCanonicalAliasContent>()?.canonicalAlias
if (!name.isNullOrEmpty()) {
return@doWithRealm
}
val aliases = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).last()?.asDomain()
val aliases = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev()?.asDomain()
name = aliases?.content.toModel<RoomAliasesContent>()?.aliases?.firstOrNull()
if (!name.isNullOrEmpty()) {
return@doWithRealm

View File

@ -16,20 +16,19 @@
package im.vector.matrix.android.internal.session.room.members
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.last
import im.vector.matrix.android.internal.database.query.next
import im.vector.matrix.android.internal.database.query.prev
import im.vector.matrix.android.internal.database.query.where
import io.realm.Realm
import io.realm.RealmQuery
internal class RoomMemberExtractor(private val monarchy: Monarchy,
private val roomId: String) {
internal class RoomMemberExtractor(private val roomId: String) {
private val cached = HashMap<String, RoomMember?>()
@ -44,30 +43,25 @@ internal class RoomMemberExtractor(private val monarchy: Monarchy,
// When stateIndex is negative, we try to get the next stateEvent prevContent()
// If prevContent is null we fallback to the Int.MIN state events content()
val content = if (event.stateIndex <= 0) {
baseQuery(monarchy, roomId, sender, unlinked).next(from = event.stateIndex)?.prevContent
?: baseQuery(monarchy, roomId, sender, unlinked).last(since = event.stateIndex)?.content
baseQuery(event.realm, roomId, sender, unlinked).next(from = event.stateIndex)?.prevContent
?: baseQuery(event.realm, roomId, sender, unlinked).prev(since = event.stateIndex)?.content
} else {
baseQuery(monarchy, roomId, sender, unlinked).last(since = event.stateIndex)?.content
baseQuery(event.realm, roomId, sender, unlinked).prev(since = event.stateIndex)?.content
}
val roomMember: RoomMember? = ContentMapper.map(content).toModel()
cached[cacheKey] = roomMember
return roomMember
}
private fun baseQuery(monarchy: Monarchy,
private fun baseQuery(realm: Realm,
roomId: String,
sender: String,
isUnlinked: Boolean): RealmQuery<EventEntity> {
lateinit var query: RealmQuery<EventEntity>
val filterMode = if (isUnlinked) EventEntity.LinkFilterMode.UNLINKED_ONLY else EventEntity.LinkFilterMode.LINKED_ONLY
monarchy.doWithRealm { realm ->
query = EventEntity
.where(realm, roomId = roomId, type = EventType.STATE_ROOM_MEMBER, linkFilterMode = filterMode)
.equalTo(EventEntityFields.STATE_KEY, sender)
}
return query
return EventEntity
.where(realm, roomId = roomId, type = EventType.STATE_ROOM_MEMBER, linkFilterMode = filterMode)
.equalTo(EventEntityFields.STATE_KEY, sender)
}
}

View File

@ -34,7 +34,7 @@ internal class EventsPruner(monarchy: Monarchy) :
override val query = Monarchy.Query<EventEntity> { EventEntity.where(it, type = EventType.REDACTION) }
override fun process(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
val redactionEvents = inserted.map { it.asDomain() }
val pruneEventWorkerParams = PruneEventWorker.Params(redactionEvents)
val workData = WorkerParamsFactory.toData(pruneEventWorkerParams)

View File

@ -56,8 +56,8 @@ internal class DefaultSetReadMarkersTask(private val roomAPI: RoomAPI,
markers[READ_MARKER] = params.fullyReadEventId
}
if (params.readReceiptEventId != null
&& MatrixPatterns.isEventId(params.readReceiptEventId)
&& !isEventRead(params.roomId, params.readReceiptEventId)) {
&& MatrixPatterns.isEventId(params.readReceiptEventId)
&& !isEventRead(params.roomId, params.readReceiptEventId)) {
updateNotificationCountIfNecessary(params.roomId, params.readReceiptEventId)
markers[READ_RECEIPT] = params.readReceiptEventId
@ -76,7 +76,7 @@ internal class DefaultSetReadMarkersTask(private val roomAPI: RoomAPI,
val isLatestReceived = EventEntity.latestEvent(realm, roomId)?.eventId == eventId
if (isLatestReceived) {
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
?: return@tryTransactionAsync
?: return@tryTransactionAsync
roomSummary.notificationCount = 0
roomSummary.highlightCount = 0
}
@ -87,13 +87,14 @@ internal class DefaultSetReadMarkersTask(private val roomAPI: RoomAPI,
var isEventRead = false
monarchy.doWithRealm {
val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst()
?: return@doWithRealm
?: return@doWithRealm
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId)
?: return@doWithRealm
?: return@doWithRealm
val readReceiptIndex = liveChunk.events.find(readReceipt.eventId)?.displayIndex
?: Int.MAX_VALUE
val eventToCheckIndex = liveChunk.events.find(eventId)?.displayIndex ?: -1
isEventRead = eventToCheckIndex >= readReceiptIndex
?: Int.MIN_VALUE
val eventToCheckIndex = liveChunk.events.find(eventId)?.displayIndex
?: Int.MAX_VALUE
isEventRead = eventToCheckIndex <= readReceiptIndex
}
return isEventRead
}

View File

@ -23,7 +23,6 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.send.SendService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.helper.add
import im.vector.matrix.android.internal.database.helper.updateDisplayIndexes
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
@ -49,7 +48,6 @@ internal class DefaultSendService(private val roomId: String,
val chunkEntity = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
?: return@tryTransactionAsync
chunkEntity.add(roomId, event, PaginationDirection.FORWARDS)
chunkEntity.updateDisplayIndexes()
}
val sendContentWorkerParams = SendEventWorker.Params(roomId, event)

View File

@ -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<GetContextOfEventTask.Params, TokenChunkEvent> {
internal interface GetContextOfEventTask : Task<GetContextOfEventTask.Params, TokenChunkEventPersistor.Result> {
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<EventContextResponse> {
override fun execute(params: GetContextOfEventTask.Params): Try<TokenChunkEventPersistor.Result> {
val filter = FilterUtil.createRoomEventFilter(true)?.toJSONString()
return executeRequest<EventContextResponse> {
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)
}
}

View File

@ -23,7 +23,7 @@ import im.vector.matrix.android.internal.task.Task
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(
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<Boolean> {
override fun execute(params: PaginationTask.Params): Try<TokenChunkEventPersistor.Result> {
val filter = FilterUtil.createRoomEventFilter(true)?.toJSONString()
return executeRequest<PaginationResponse> {
apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter)

View File

@ -0,0 +1,448 @@
/*
*
* * Copyright 2019 New Vector Ltd
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
*
*/
package im.vector.matrix.android.internal.session.room.timeline
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.Timeline
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
import im.vector.matrix.android.internal.database.query.findIncludingEvent
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.Sort
import timber.log.Timber
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
private const val INITIAL_LOAD_SIZE = 20
private const val MIN_FETCHING_COUNT = 30
private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE
private const val THREAD_NAME = "TIMELINE_DB_THREAD"
internal class DefaultTimeline(
private val roomId: String,
private val initialEventId: String? = null,
private val realmConfiguration: RealmConfiguration,
private val taskExecutor: TaskExecutor,
private val contextOfEventTask: GetContextOfEventTask,
private val timelineEventFactory: TimelineEventFactory,
private val paginationTask: PaginationTask,
private val allowedTypes: List<String>?
) : Timeline {
override var listener: Timeline.Listener? = null
set(value) {
field = value
backgroundHandler.get()?.post {
val snapshot = snapshot()
mainHandler.post { listener?.onUpdated(snapshot) }
}
}
private val isStarted = AtomicBoolean(false)
private val isReady = AtomicBoolean(false)
private val backgroundHandlerThread = AtomicReference<HandlerThread>()
private val backgroundHandler = AtomicReference<Handler>()
private val mainHandler = Handler(Looper.getMainLooper())
private val backgroundRealm = AtomicReference<Realm>()
private val cancelableBag = CancelableBag()
private lateinit var liveEvents: RealmResults<EventEntity>
private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
private val isLive = initialEventId == null
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
private val backwardsPaginationState = AtomicReference(PaginationState())
private val forwardsPaginationState = AtomicReference(PaginationState())
private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<EventEntity>> { _, changeSet ->
// TODO HANDLE CHANGES
changeSet.insertionRanges.forEach { range ->
val (startDisplayIndex, direction) = if (range.startIndex == 0) {
Pair(liveEvents[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS)
} else {
Pair(liveEvents[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS)
}
val state = getPaginationState(direction)
if (state.isPaginating) {
// We are getting new items from pagination
val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, state.requestedCount)
if (shouldPostSnapshot) {
postSnapshot()
}
} else {
// We are getting new items from sync
buildTimelineEvents(startDisplayIndex, direction, range.length.toLong())
postSnapshot()
}
}
}
// Public methods ******************************************************************************
override fun paginate(direction: Timeline.Direction, count: Int) {
backgroundHandler.get()?.post {
if (!canPaginate(direction)) {
return@post
}
Timber.v("Paginate $direction of $count items")
val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex
val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, count)
if (shouldPostSnapshot) {
postSnapshot()
}
}
}
override fun start() {
if (isStarted.compareAndSet(false, true)) {
Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId")
val handlerThread = HandlerThread(THREAD_NAME + hashCode())
handlerThread.start()
val handler = Handler(handlerThread.looper)
this.backgroundHandlerThread.set(handlerThread)
this.backgroundHandler.set(handler)
handler.post {
val realm = Realm.getInstance(realmConfiguration)
backgroundRealm.set(realm)
clearUnlinkedEvents(realm)
isReady.set(true)
liveEvents = buildEventQuery(realm)
.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
.findAll()
.also { it.addChangeListener(eventsChangeListener) }
handleInitialLoad()
}
}
}
override fun dispose() {
if (isStarted.compareAndSet(true, false)) {
Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId")
backgroundHandler.get()?.post {
cancelableBag.cancel()
liveEvents.removeAllChangeListeners()
backgroundRealm.getAndSet(null).also {
it.close()
}
backgroundHandler.set(null)
backgroundHandlerThread.getAndSet(null)?.quit()
}
}
}
override fun hasMoreToLoad(direction: Timeline.Direction): Boolean {
return hasMoreInCache(direction) || !hasReachedEnd(direction)
}
// Private methods *****************************************************************************
private fun hasMoreInCache(direction: Timeline.Direction): Boolean {
val localRealm = Realm.getInstance(realmConfiguration)
val eventEntity = buildEventQuery(localRealm).findFirst(direction) ?: return false
val hasMoreInCache = if (direction == Timeline.Direction.FORWARDS) {
val firstEvent = builtEvents.firstOrNull() ?: return true
firstEvent.displayIndex < eventEntity.displayIndex
} else {
val lastEvent = builtEvents.lastOrNull() ?: return true
lastEvent.displayIndex > eventEntity.displayIndex
}
localRealm.close()
return hasMoreInCache
}
private fun hasReachedEnd(direction: Timeline.Direction): Boolean {
val localRealm = Realm.getInstance(realmConfiguration)
val currentChunk = findCurrentChunk(localRealm) ?: return false
val hasReachedEnd = if (direction == Timeline.Direction.FORWARDS) {
currentChunk.isLastForward
} else {
val eventEntity = buildEventQuery(localRealm).findFirst(direction)
currentChunk.isLastBackward || eventEntity?.type == EventType.STATE_ROOM_CREATE
}
localRealm.close()
return hasReachedEnd
}
/**
* This has to be called on TimelineThread as it access realm live results
* @return true if snapshot should be posted
*/
private fun paginateInternal(startDisplayIndex: Int,
direction: Timeline.Direction,
count: Int): Boolean {
updatePaginationState(direction) { it.copy(requestedCount = count, isPaginating = true) }
val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong())
val shouldFetchMore = builtCount < count && !hasReachedEnd(direction)
if (shouldFetchMore) {
val newRequestedCount = count - builtCount
updatePaginationState(direction) { it.copy(requestedCount = newRequestedCount) }
val fetchingCount = Math.max(MIN_FETCHING_COUNT, newRequestedCount)
executePaginationTask(direction, fetchingCount)
} else {
updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) }
}
return !shouldFetchMore
}
private fun snapshot(): List<TimelineEvent> {
return builtEvents.toList()
}
private fun canPaginate(direction: Timeline.Direction): Boolean {
return isReady.get() && !getPaginationState(direction).isPaginating && hasMoreToLoad(direction)
}
private fun getPaginationState(direction: Timeline.Direction): PaginationState {
return when (direction) {
Timeline.Direction.FORWARDS -> forwardsPaginationState.get()
Timeline.Direction.BACKWARDS -> backwardsPaginationState.get()
}
}
private fun updatePaginationState(direction: Timeline.Direction, update: (PaginationState) -> PaginationState) {
val stateReference = when (direction) {
Timeline.Direction.FORWARDS -> forwardsPaginationState
Timeline.Direction.BACKWARDS -> backwardsPaginationState
}
val currentValue = stateReference.get()
val newValue = update(currentValue)
stateReference.set(newValue)
}
/**
* This has to be called on TimelineThread as it access realm live results
*/
private fun handleInitialLoad() {
var shouldFetchInitialEvent = false
val initialDisplayIndex = if (isLive) {
liveEvents.firstOrNull()?.displayIndex
} else {
val initialEvent = liveEvents.where().equalTo(EventEntityFields.EVENT_ID, initialEventId).findFirst()
shouldFetchInitialEvent = initialEvent == null
initialEvent?.displayIndex
} ?: DISPLAY_INDEX_UNKNOWN
prevDisplayIndex = initialDisplayIndex
nextDisplayIndex = initialDisplayIndex
if (initialEventId != null && shouldFetchInitialEvent) {
fetchEvent(initialEventId)
} else {
val count = Math.min(INITIAL_LOAD_SIZE, liveEvents.size)
if (isLive) {
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count)
} else {
paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2)
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count / 2)
}
}
postSnapshot()
}
/**
* This has to be called on TimelineThread as it access realm live results
*/
private fun executePaginationTask(direction: Timeline.Direction, limit: Int) {
val token = getTokenLive(direction) ?: return
val params = PaginationTask.Params(roomId = roomId,
from = token,
direction = direction.toPaginationDirection(),
limit = limit)
Timber.v("Should fetch $limit items $direction")
paginationTask.configureWith(params)
.enableRetry()
.dispatchTo(object : MatrixCallback<TokenChunkEventPersistor.Result> {
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
if (data == TokenChunkEventPersistor.Result.SUCCESS) {
Timber.v("Success fetching $limit items $direction from pagination request")
} else {
// Database won't be updated, so we force pagination request
backgroundHandler.get()?.post {
executePaginationTask(direction, limit)
}
}
}
override fun onFailure(failure: Throwable) {
Timber.v("Failure fetching $limit items $direction from pagination request")
}
})
.executeBy(taskExecutor)
.addTo(cancelableBag)
}
/**
* This has to be called on TimelineThread as it access realm live results
*/
private fun getTokenLive(direction: Timeline.Direction): String? {
val chunkEntity = getLiveChunk() ?: return null
return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken
}
/**
* This has to be called on TimelineThread as it access realm live results
*/
private fun 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 buildTimelineEvents(startDisplayIndex: Int,
direction: Timeline.Direction,
count: Long): Int {
if (count < 1) {
return 0
}
val offsetResults = getOffsetResults(startDisplayIndex, direction, count)
if (offsetResults.isEmpty()) {
return 0
}
val offsetIndex = offsetResults.last()!!.displayIndex
if (direction == Timeline.Direction.BACKWARDS) {
prevDisplayIndex = offsetIndex - 1
} else {
nextDisplayIndex = offsetIndex + 1
}
offsetResults.forEach { eventEntity ->
val timelineEvent = timelineEventFactory.create(eventEntity)
val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size
builtEvents.add(position, timelineEvent)
}
Timber.v("Built ${offsetResults.size} items from db")
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<EventEntity> {
val offsetQuery = liveEvents.where()
if (direction == Timeline.Direction.BACKWARDS) {
offsetQuery
.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
.lessThanOrEqualTo(EventEntityFields.DISPLAY_INDEX, startDisplayIndex)
} else {
offsetQuery
.sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
.greaterThanOrEqualTo(EventEntityFields.DISPLAY_INDEX, startDisplayIndex)
}
return offsetQuery
.filterAllowedTypes()
.limit(count)
.findAll()
}
private fun buildEventQuery(realm: Realm): RealmQuery<EventEntity> {
return if (initialEventId == null) {
EventEntity
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY)
.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(initialEventId))
}
}
private fun findCurrentChunk(realm: Realm): ChunkEntity? {
return if (initialEventId == null) {
ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
} else {
ChunkEntity.findIncludingEvent(realm, initialEventId)
}
}
private fun clearUnlinkedEvents(realm: Realm) {
realm.executeTransaction {
val unlinkedChunks = ChunkEntity
.where(it, roomId = roomId)
.equalTo(ChunkEntityFields.EVENTS.IS_UNLINKED, true)
.findAll()
unlinkedChunks.deleteAllFromRealm()
}
}
private fun fetchEvent(eventId: String) {
val params = GetContextOfEventTask.Params(roomId, eventId)
contextOfEventTask.configureWith(params).executeBy(taskExecutor)
}
private fun postSnapshot() {
val snapshot = snapshot()
mainHandler.post { listener?.onUpdated(snapshot) }
}
// Extension methods ***************************************************************************
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
}
private fun RealmQuery<EventEntity>.findFirst(direction: Timeline.Direction): EventEntity? {
return if (direction == Timeline.Direction.FORWARDS) {
sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
} else {
sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
}
.filterAllowedTypes()
.findFirst()
}
private fun RealmQuery<EventEntity>.filterAllowedTypes(): RealmQuery<EventEntity> {
if (allowedTypes != null) {
`in`(EventEntityFields.TYPE, allowedTypes.toTypedArray())
}
return this
}
}
private data class PaginationState(
val isPaginating: Boolean = false,
val requestedCount: Int = 0
)

View File

@ -16,129 +16,21 @@
package im.vector.matrix.android.internal.session.room.timeline
import androidx.lifecycle.LiveData
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.room.timeline.TimelineEventInterceptor
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineData
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.LiveDataUtils
import im.vector.matrix.android.internal.util.PagingRequestHelper
import im.vector.matrix.android.internal.util.tryTransactionAsync
import io.realm.Realm
import io.realm.RealmQuery
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,
private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor,
private val boundaryCallback: TimelineBoundaryCallback,
private val contextOfEventTask: GetContextOfEventTask,
private val roomMemberExtractor: RoomMemberExtractor
private val timelineEventFactory: TimelineEventFactory,
private val paginationTask: PaginationTask
) : TimelineService {
private val eventInterceptors = ArrayList<TimelineEventInterceptor>()
override fun timeline(eventId: String?): LiveData<TimelineData> {
clearUnlinkedEvents()
val initialLoadKey = getInitialLoadKey(eventId)
val realmDataSourceFactory = monarchy.createDataSourceFactory {
buildDataSourceFactoryQuery(it, eventId)
}
val domainSourceFactory = realmDataSourceFactory
.map { eventEntity ->
val roomMember = roomMemberExtractor.extractFrom(eventEntity)
TimelineEvent(eventEntity.asDomain(), eventEntity.localId, roomMember)
}
val pagedListConfig = buildPagedListConfig()
val livePagedListBuilder = LivePagedListBuilder(domainSourceFactory, pagedListConfig)
.setBoundaryCallback(boundaryCallback)
.setInitialLoadKey(initialLoadKey)
val eventsLiveData = monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder)
return LiveDataUtils.combine(eventsLiveData, boundaryCallback.status) { events, status ->
val isLoadingForward = status.before == PagingRequestHelper.Status.RUNNING
val isLoadingBackward = status.after == PagingRequestHelper.Status.RUNNING
TimelineData(events, isLoadingForward, isLoadingBackward)
}
override fun createTimeline(eventId: String?, allowedTypes: List<String>?): Timeline {
return DefaultTimeline(roomId, eventId, monarchy.realmConfiguration, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask, allowedTypes)
}
// PRIVATE FUNCTIONS ***************************************************************************
private fun getInitialLoadKey(eventId: String?): Int {
var initialLoadKey = 0
if (eventId != null) {
val indexOfEvent = indexOfEvent(eventId)
if (indexOfEvent == EVENT_NOT_FOUND_INDEX) {
fetchEvent(eventId)
} else {
initialLoadKey = indexOfEvent
}
}
return initialLoadKey
}
private fun fetchEvent(eventId: String) {
val params = GetContextOfEventTask.Params(roomId, eventId)
contextOfEventTask.configureWith(params).executeBy(taskExecutor)
}
private fun 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
.where(realm, roomId = roomId)
.equalTo(EventEntityFields.IS_UNLINKED, true)
.findAll()
unlinkedEvents.deleteAllFromRealm()
}
}
private fun indexOfEvent(eventId: String): Int {
var displayIndex = EVENT_NOT_FOUND_INDEX
monarchy.doWithRealm {
displayIndex = EventEntity.where(it, eventId = eventId).findFirst()?.displayIndex ?: EVENT_NOT_FOUND_INDEX
}
return displayIndex
}
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)
}
}

View File

@ -1,110 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.room.timeline
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.query.findIncludingEvent
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.PagingRequestHelper
internal class TimelineBoundaryCallback(private val roomId: String,
private val taskExecutor: TaskExecutor,
private val paginationTask: PaginationTask,
private val monarchy: Monarchy,
private val helper: PagingRequestHelper
) : PagedList.BoundaryCallback<TimelineEvent>() {
var limit = 30
val status = object : LiveData<PagingRequestHelper.StatusReport>() {
init {
value = PagingRequestHelper.StatusReport.createDefault()
}
val listener = PagingRequestHelper.Listener { postValue(it) }
override fun onActive() {
helper.addListener(listener)
}
override fun onInactive() {
helper.removeListener(listener)
}
}
override fun onZeroItemsLoaded() {
// actually, it's not possible
}
override fun onItemAtEndLoaded(itemAtEnd: TimelineEvent) {
val token = itemAtEnd.root.eventId?.let { getToken(it, PaginationDirection.BACKWARDS) }
?: return
helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) {
executePaginationTask(it, token, PaginationDirection.BACKWARDS)
}
}
override fun onItemAtFrontLoaded(itemAtFront: TimelineEvent) {
val token = itemAtFront.root.eventId?.let { getToken(it, PaginationDirection.FORWARDS) }
?: return
helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE) {
executePaginationTask(it, token, PaginationDirection.FORWARDS)
}
}
private fun getToken(eventId: String, direction: PaginationDirection): String? {
var token: String? = null
monarchy.doWithRealm { realm ->
val chunkEntity = ChunkEntity.findIncludingEvent(realm, eventId)
token = if (direction == PaginationDirection.FORWARDS) chunkEntity?.nextToken else chunkEntity?.prevToken
}
return token
}
private fun executePaginationTask(requestCallback: PagingRequestHelper.Request.Callback,
from: String,
direction: PaginationDirection) {
val params = PaginationTask.Params(roomId = roomId,
from = from,
direction = direction,
limit = limit)
paginationTask.configureWith(params)
.enableRetry()
.dispatchTo(object : MatrixCallback<Boolean> {
override fun onSuccess(data: Boolean) {
requestCallback.recordSuccess()
}
override fun onFailure(failure: Throwable) {
requestCallback.recordFailure(failure)
}
})
.executeBy(taskExecutor)
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.room.timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
internal class TimelineEventFactory(private val roomMemberExtractor: RoomMemberExtractor) {
fun create(eventEntity: EventEntity): TimelineEvent {
val roomMember = roomMemberExtractor.extractFrom(eventEntity)
return TimelineEvent(
eventEntity.asDomain(),
eventEntity.localId,
eventEntity.displayIndex,
roomMember
)
}
}

View File

@ -18,7 +18,12 @@ package im.vector.matrix.android.internal.session.room.timeline
import arrow.core.Try
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.helper.*
import im.vector.matrix.android.internal.database.helper.addAll
import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.helper.addStateEvents
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
import im.vector.matrix.android.internal.database.helper.isUnlinked
import im.vector.matrix.android.internal.database.helper.merge
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.create
@ -26,6 +31,8 @@ import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.util.tryTransactionSync
import io.realm.kotlin.createObject
import timber.log.Timber
/**
* Insert Chunk in DB, and eventually merge with existing chunk event
@ -94,17 +101,22 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
* ========================================================================================================
* </pre>
*/
enum class Result {
SHOULD_FETCH_MORE,
SUCCESS
}
fun insertInDb(receivedChunk: TokenChunkEvent,
roomId: String,
direction: PaginationDirection): Try<Boolean> {
direction: PaginationDirection): Try<Result> {
if (receivedChunk.events.isEmpty() && receivedChunk.stateEvents.isEmpty()) {
return Try.just(false)
}
return monarchy
.tryTransactionSync { realm ->
Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: throw IllegalStateException("You shouldn't use this method without a room")
?: realm.createObject(roomId)
val nextToken: String?
val prevToken: String?
@ -118,7 +130,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken)
val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken)
// The current chunk is the one we will keep all along the merge process.
// The current chunk is the one we will keep all along the merge processChanges.
// We try to look for a chunk next to the token,
// otherwise we create a whole new one
@ -127,28 +139,39 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
} else {
nextChunk?.apply { this.prevToken = prevToken }
}
?: ChunkEntity.create(realm, prevToken, nextToken)
?: ChunkEntity.create(realm, prevToken, nextToken)
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
// 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) {
Timber.v("Reach end of $roomId")
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)
}
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
// Then we merge chunks if needed
if (currentChunk != prevChunk && prevChunk != null) {
currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
} 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,
@ -157,6 +180,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
otherChunk: ChunkEntity): ChunkEntity {
// We always merge the bottom chunk into top chunk, so we are always merging backwards
Timber.v("Merge ${currentChunk.prevToken} | ${currentChunk.nextToken} with ${otherChunk.prevToken} | ${otherChunk.nextToken}")
return if (direction == PaginationDirection.BACKWARDS) {
currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS)
roomEntity.deleteOnCascade(otherChunk)

View File

@ -28,22 +28,17 @@ import im.vector.matrix.android.internal.database.helper.addStateEvents
import im.vector.matrix.android.internal.database.helper.lastStateIndex
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
import im.vector.matrix.android.internal.session.sync.model.RoomSync
import im.vector.matrix.android.internal.session.sync.model.RoomSyncAccountData
import im.vector.matrix.android.internal.session.sync.model.RoomSyncEphemeral
import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary
import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications
import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse
import im.vector.matrix.android.internal.session.sync.model.*
import io.realm.Realm
import io.realm.kotlin.createObject
internal class RoomSyncHandler(private val monarchy: Monarchy,
private val readReceiptHandler: ReadReceiptHandler,
private val roomSummaryUpdater: RoomSummaryUpdater,
private val roomTagHandler: RoomTagHandler) {
sealed class HandlingStrategy {
@ -76,7 +71,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
roomSync: RoomSync): RoomEntity {
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
?: realm.createObject(roomId)
if (roomEntity.membership == MyMembership.INVITED) {
roomEntity.chunks.deleteAllFromRealm()
@ -107,14 +102,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
)
roomEntity.addOrUpdate(chunkEntity)
}
if (roomSync.summary != null) {
handleRoomSummary(realm, roomId, roomSync.summary)
}
if (roomSync.unreadNotifications != null) {
handleUnreadNotifications(realm, roomId, roomSync.unreadNotifications)
}
roomSummaryUpdater.update(realm, roomId, roomSync.summary, roomSync.unreadNotifications)
if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
handleEphemeral(realm, roomId, roomSync.ephemeral)
@ -164,32 +152,12 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
realm.createObject<ChunkEntity>().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
}
private fun handleRoomSummary(realm: Realm,
roomId: String,
roomSummary: RoomSyncSummary) {
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: RoomSummaryEntity(roomId)
if (roomSummary.heroes.isNotEmpty()) {
roomSummaryEntity.heroes.clear()
roomSummaryEntity.heroes.addAll(roomSummary.heroes)
}
if (roomSummary.invitedMembersCount != null) {
roomSummaryEntity.invitedMembersCount = roomSummary.invitedMembersCount
}
if (roomSummary.joinedMembersCount != null) {
roomSummaryEntity.joinedMembersCount = roomSummary.joinedMembersCount
}
realm.insertOrUpdate(roomSummaryEntity)
}
private fun handleEphemeral(realm: Realm,
roomId: String,
ephemeral: RoomSyncEphemeral) {
@ -199,20 +167,6 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
.flatMap { readReceiptHandler.handle(realm, roomId, it) }
}
private fun handleUnreadNotifications(realm: Realm, roomId: String, unreadNotifications: RoomSyncUnreadNotifications) {
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: RoomSummaryEntity(roomId)
if (unreadNotifications.highlightCount != null) {
roomSummaryEntity.highlightCount = unreadNotifications.highlightCount
}
if (unreadNotifications.notificationCount != null) {
roomSummaryEntity.notificationCount = unreadNotifications.notificationCount
}
realm.insertOrUpdate(roomSummaryEntity)
}
private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) {
accountData.events
.filter { it.type == EventType.TAG }

View File

@ -40,7 +40,7 @@ internal class SyncModule {
}
scope(DefaultSession.SCOPE) {
RoomSyncHandler(get(), get(), get())
RoomSyncHandler(get(), get(), get(), get())
}
scope(DefaultSession.SCOPE) {

View File

@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.sync
import arrow.core.Try
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
import timber.log.Timber
import kotlin.system.measureTimeMillis
internal class SyncResponseHandler(private val roomSyncHandler: RoomSyncHandler,
private val userAccountDataSyncHandler: UserAccountDataSyncHandler,
@ -26,16 +27,19 @@ internal class SyncResponseHandler(private val roomSyncHandler: RoomSyncHandler,
fun handleResponse(syncResponse: SyncResponse, fromToken: String?, isCatchingUp: Boolean): Try<SyncResponse> {
return Try {
Timber.v("Handle sync response")
if (syncResponse.rooms != null) {
roomSyncHandler.handle(syncResponse.rooms)
}
if (syncResponse.groups != null) {
groupSyncHandler.handle(syncResponse.groups)
}
if (syncResponse.accountData != null) {
userAccountDataSyncHandler.handle(syncResponse.accountData)
Timber.v("Start handling sync")
val measure = measureTimeMillis {
if (syncResponse.rooms != null) {
roomSyncHandler.handle(syncResponse.rooms)
}
if (syncResponse.groups != null) {
groupSyncHandler.handle(syncResponse.groups)
}
if (syncResponse.accountData != null) {
userAccountDataSyncHandler.handle(syncResponse.accountData)
}
}
Timber.v("Finish handling sync in $measure ms")
syncResponse
}
}

View File

@ -42,7 +42,7 @@ internal class UserEntityUpdater(monarchy: Monarchy,
}
override fun process(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
val roomMembersEvents = inserted.map { it.eventId }
val taskParams = UpdateUserTask.Params(roomMembersEvents)
updateUserTask

View File

@ -1,530 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.util;
import androidx.annotation.AnyThread;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.util.Arrays;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A helper class for {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}s and
* {@link androidx.paging.DataSource}s to help with tracking network requests.
* <p>
* It is designed to support 3 types of requests, {@link RequestType#INITIAL INITIAL},
* {@link RequestType#BEFORE BEFORE} and {@link RequestType#AFTER AFTER} and runs only 1 request
* for each of them via {@link #runIfNotRunning(RequestType, Request)}.
* <p>
* It tracks a {@link Status} and an {@code error} for each {@link RequestType}.
* <p>
* A sample usage of this class to limit requests looks like this:
* <pre>
* class PagingBoundaryCallback extends PagedList.BoundaryCallback&lt;MyItem> {
* // TODO replace with an executor from your application
* Executor executor = Executors.newSingleThreadExecutor();
* PagingRequestHelper helper = new PagingRequestHelper(executor);
* // imaginary API service, using Retrofit
* MyApi api;
*
* {@literal @}Override
* public void onItemAtFrontLoaded({@literal @}NonNull MyItem itemAtFront) {
* helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE,
* helperCallback -> api.getTopBefore(itemAtFront.getName(), 10).enqueue(
* new Callback&lt;ApiResponse>() {
* {@literal @}Override
* public void onResponse(Call&lt;ApiResponse> call,
* Response&lt;ApiResponse> response) {
* // TODO insert new records into database
* helperCallback.recordSuccess();
* }
*
* {@literal @}Override
* public void onFailure(Call&lt;ApiResponse> call, Throwable t) {
* helperCallback.recordFailure(t);
* }
* }));
* }
*
* {@literal @}Override
* public void onItemAtEndLoaded({@literal @}NonNull MyItem itemAtEnd) {
* helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER,
* helperCallback -> api.getTopBefore(itemAtEnd.getName(), 10).enqueue(
* new Callback&lt;ApiResponse>() {
* {@literal @}Override
* public void onResponse(Call&lt;ApiResponse> call,
* Response&lt;ApiResponse> response) {
* // TODO insert new records into database
* helperCallback.recordSuccess();
* }
*
* {@literal @}Override
* public void onFailure(Call&lt;ApiResponse> call, Throwable t) {
* helperCallback.recordFailure(t);
* }
* }));
* }
* }
* </pre>
* <p>
* The helper provides an API to observe combined request status, which can be reported back to the
* application based on your business rules.
* <pre>
* MutableLiveData&lt;PagingRequestHelper.Status> combined = new MutableLiveData&lt;>();
* helper.addListener(status -> {
* // merge multiple states per request type into one, or dispatch separately depending on
* // your application logic.
* if (status.hasRunning()) {
* combined.postValue(PagingRequestHelper.Status.RUNNING);
* } else if (status.hasError()) {
* // can also obtain the error via {@link StatusReport#getErrorFor(RequestType)}
* combined.postValue(PagingRequestHelper.Status.FAILED);
* } else {
* combined.postValue(PagingRequestHelper.Status.SUCCESS);
* }
* });
* </pre>
*/
// THIS class is likely to be moved into the library in a future release. Feel free to copy it
// from this sample.
public class PagingRequestHelper {
private final Object mLock = new Object();
private final Executor mRetryService;
@GuardedBy("mLock")
private final RequestQueue[] mRequestQueues = new RequestQueue[]
{new RequestQueue(RequestType.INITIAL),
new RequestQueue(RequestType.BEFORE),
new RequestQueue(RequestType.AFTER)};
@NonNull
final CopyOnWriteArrayList<Listener> mListeners = new CopyOnWriteArrayList<>();
/**
* Creates a new PagingRequestHelper with the given {@link Executor} which is used to run
* retry actions.
*
* @param retryService The {@link Executor} that can run the retry actions.
*/
public PagingRequestHelper(@NonNull Executor retryService) {
mRetryService = retryService;
}
/**
* Adds a new listener that will be notified when any request changes {@link Status state}.
*
* @param listener The listener that will be notified each time a request's status changes.
* @return True if it is added, false otherwise (e.g. it already exists in the list).
*/
@AnyThread
public boolean addListener(@NonNull Listener listener) {
return mListeners.add(listener);
}
/**
* Removes the given listener from the listeners list.
*
* @param listener The listener that will be removed.
* @return True if the listener is removed, false otherwise (e.g. it never existed)
*/
public boolean removeListener(@NonNull Listener listener) {
return mListeners.remove(listener);
}
/**
* Runs the given {@link Request} if no other requests in the given request type is already
* running.
* <p>
* If run, the request will be run in the current thread.
*
* @param type The type of the request.
* @param request The request to run.
* @return True if the request is run, false otherwise.
*/
@SuppressWarnings("WeakerAccess")
@AnyThread
public boolean runIfNotRunning(@NonNull RequestType type, @NonNull Request request) {
boolean hasListeners = !mListeners.isEmpty();
StatusReport report = null;
synchronized (mLock) {
RequestQueue queue = mRequestQueues[type.ordinal()];
if (queue.mRunning != null) {
return false;
}
queue.mRunning = request;
queue.mStatus = Status.RUNNING;
queue.mFailed = null;
queue.mLastError = null;
if (hasListeners) {
report = prepareStatusReportLocked();
}
}
if (report != null) {
dispatchReport(report);
}
final RequestWrapper wrapper = new RequestWrapper(request, this, type);
wrapper.run();
return true;
}
@GuardedBy("mLock")
private StatusReport prepareStatusReportLocked() {
Throwable[] errors = new Throwable[]{
mRequestQueues[0].mLastError,
mRequestQueues[1].mLastError,
mRequestQueues[2].mLastError
};
return new StatusReport(
getStatusForLocked(RequestType.INITIAL),
getStatusForLocked(RequestType.BEFORE),
getStatusForLocked(RequestType.AFTER),
errors
);
}
@GuardedBy("mLock")
private Status getStatusForLocked(RequestType type) {
return mRequestQueues[type.ordinal()].mStatus;
}
@AnyThread
@VisibleForTesting
void recordResult(@NonNull RequestWrapper wrapper, @Nullable Throwable throwable) {
StatusReport report = null;
final boolean success = throwable == null;
boolean hasListeners = !mListeners.isEmpty();
synchronized (mLock) {
RequestQueue queue = mRequestQueues[wrapper.mType.ordinal()];
queue.mRunning = null;
queue.mLastError = throwable;
if (success) {
queue.mFailed = null;
queue.mStatus = Status.SUCCESS;
} else {
queue.mFailed = wrapper;
queue.mStatus = Status.FAILED;
}
if (hasListeners) {
report = prepareStatusReportLocked();
}
}
if (report != null) {
dispatchReport(report);
}
}
private void dispatchReport(StatusReport report) {
for (Listener listener : mListeners) {
listener.onStatusChange(report);
}
}
/**
* Retries all failed requests.
*
* @return True if any request is retried, false otherwise.
*/
public boolean retryAllFailed() {
final RequestWrapper[] toBeRetried = new RequestWrapper[RequestType.values().length];
boolean retried = false;
synchronized (mLock) {
for (int i = 0; i < RequestType.values().length; i++) {
toBeRetried[i] = mRequestQueues[i].mFailed;
mRequestQueues[i].mFailed = null;
}
}
for (RequestWrapper failed : toBeRetried) {
if (failed != null) {
failed.retry(mRetryService);
retried = true;
}
}
return retried;
}
static class RequestWrapper implements Runnable {
@NonNull
final Request mRequest;
@NonNull
final PagingRequestHelper mHelper;
@NonNull
final RequestType mType;
RequestWrapper(@NonNull Request request, @NonNull PagingRequestHelper helper,
@NonNull RequestType type) {
mRequest = request;
mHelper = helper;
mType = type;
}
@Override
public void run() {
mRequest.run(new Request.Callback(this, mHelper));
}
void retry(Executor service) {
service.execute(new Runnable() {
@Override
public void run() {
mHelper.runIfNotRunning(mType, mRequest);
}
});
}
}
/**
* Runner class that runs a request tracked by the {@link PagingRequestHelper}.
* <p>
* When a request is invoked, it must call one of {@link Callback#recordFailure(Throwable)}
* or {@link Callback#recordSuccess()} once and only once. This call
* can be made any time. Until that method call is made, {@link PagingRequestHelper} will
* consider the request is running.
*/
@FunctionalInterface
public interface Request {
/**
* Should run the request and call the given {@link Callback} with the result of the
* request.
*
* @param callback The callback that should be invoked with the result.
*/
void run(Callback callback);
/**
* Callback class provided to the {@link #run(Callback)} method to report the result.
*/
class Callback {
private final AtomicBoolean mCalled = new AtomicBoolean();
private final RequestWrapper mWrapper;
private final PagingRequestHelper mHelper;
Callback(RequestWrapper wrapper, PagingRequestHelper helper) {
mWrapper = wrapper;
mHelper = helper;
}
/**
* Call this method when the request succeeds and new data is fetched.
*/
@SuppressWarnings("unused")
public final void recordSuccess() {
if (mCalled.compareAndSet(false, true)) {
mHelper.recordResult(mWrapper, null);
} else {
throw new IllegalStateException(
"already called recordSuccess or recordFailure");
}
}
/**
* Call this method with the failure message and the request can be retried via
* {@link #retryAllFailed()}.
*
* @param throwable The error that occured while carrying out the request.
*/
@SuppressWarnings("unused")
public final void recordFailure(@NonNull Throwable throwable) {
//noinspection ConstantConditions
if (throwable == null) {
throw new IllegalArgumentException("You must provide a throwable describing"
+ " the error to record the failure");
}
if (mCalled.compareAndSet(false, true)) {
mHelper.recordResult(mWrapper, throwable);
} else {
throw new IllegalStateException(
"already called recordSuccess or recordFailure");
}
}
}
}
/**
* Data class that holds the information about the current status of the ongoing requests
* using this helper.
*/
public static final class StatusReport {
/**
* Status of the latest request that were submitted with {@link RequestType#INITIAL}.
*/
@NonNull
public final Status initial;
/**
* Status of the latest request that were submitted with {@link RequestType#BEFORE}.
*/
@NonNull
public final Status before;
/**
* Status of the latest request that were submitted with {@link RequestType#AFTER}.
*/
@NonNull
public final Status after;
@NonNull
private final Throwable[] mErrors;
public static StatusReport createDefault() {
final Throwable[] errors = {};
return new StatusReport(Status.SUCCESS, Status.SUCCESS, Status.SUCCESS, errors);
}
StatusReport(@NonNull Status initial, @NonNull Status before, @NonNull Status after,
@NonNull Throwable[] errors) {
this.initial = initial;
this.before = before;
this.after = after;
this.mErrors = errors;
}
/**
* Convenience method to check if there are any running requests.
*
* @return True if there are any running requests, false otherwise.
*/
public boolean hasRunning() {
return initial == Status.RUNNING
|| before == Status.RUNNING
|| after == Status.RUNNING;
}
/**
* Convenience method to check if there are any requests that resulted in an error.
*
* @return True if there are any requests that finished with error, false otherwise.
*/
public boolean hasError() {
return initial == Status.FAILED
|| before == Status.FAILED
|| after == Status.FAILED;
}
/**
* Returns the error for the given request type.
*
* @param type The request type for which the error should be returned.
* @return The {@link Throwable} returned by the failing request with the given type or
* {@code null} if the request for the given type did not fail.
*/
@Nullable
public Throwable getErrorFor(@NonNull RequestType type) {
return mErrors[type.ordinal()];
}
@Override
public String toString() {
return "StatusReport{"
+ "initial=" + initial
+ ", before=" + before
+ ", after=" + after
+ ", mErrors=" + Arrays.toString(mErrors)
+ '}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StatusReport that = (StatusReport) o;
if (initial != that.initial) return false;
if (before != that.before) return false;
if (after != that.after) return false;
// Probably incorrect - comparing Object[] arrays with Arrays.equals
return Arrays.equals(mErrors, that.mErrors);
}
@Override
public int hashCode() {
int result = initial.hashCode();
result = 31 * result + before.hashCode();
result = 31 * result + after.hashCode();
result = 31 * result + Arrays.hashCode(mErrors);
return result;
}
}
/**
* Listener interface to get notified by request status changes.
*/
public interface Listener {
/**
* Called when the status for any of the requests has changed.
*
* @param report The current status report that has all the information about the requests.
*/
void onStatusChange(@NonNull StatusReport report);
}
/**
* Represents the status of a Request for each {@link RequestType}.
*/
public enum Status {
/**
* There is current a running request.
*/
RUNNING,
/**
* The last request has succeeded or no such requests have ever been run.
*/
SUCCESS,
/**
* The last request has failed.
*/
FAILED
}
/**
* Available request types.
*/
public enum RequestType {
/**
* Corresponds to an initial request made to a {@link androidx.paging.DataSource} or the empty state for
* a {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}.
*/
INITIAL,
/**
* Corresponds to the {@code loadBefore} calls in {@link androidx.paging.DataSource} or
* {@code onItemAtFrontLoaded} in
* {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}.
*/
BEFORE,
/**
* Corresponds to the {@code loadAfter} calls in {@link androidx.paging.DataSource} or
* {@code onItemAtEndLoaded} in
* {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}.
*/
AFTER
}
class RequestQueue {
@NonNull
final RequestType mRequestType;
@Nullable
RequestWrapper mFailed;
@Nullable
Request mRunning;
@Nullable
Throwable mLastError;
@NonNull
Status mStatus = Status.SUCCESS;
RequestQueue(@NonNull RequestType requestType) {
mRequestType = requestType;
}
}
}