Merge branch 'feature/permalink' into develop

This commit is contained in:
ganfra 2018-12-11 16:55:45 +01:00
commit 43a462f9cc
27 changed files with 520 additions and 251 deletions

View File

@ -5,6 +5,7 @@
<w>coroutine</w>
<w>merlins</w>
<w>moshi</w>
<w>persistor</w>
<w>synchronizer</w>
<w>untimelined</w>
</words>

View File

@ -17,6 +17,10 @@ class TimelineEventController(private val roomId: String,
EpoxyAsyncUtil.getAsyncBackgroundHandler()
) {

init {
setFilterDuplicates(true)
}

private val pagedListCallback = object : PagedList.Callback() {
override fun onChanged(position: Int, count: Int) {
buildSnapshotList()

View File

@ -20,6 +20,7 @@ repositories {

android {
compileSdkVersion 28
testOptions.unitTests.includeAndroidResources = true

defaultConfig {
minSdkVersion 21
@ -45,6 +46,7 @@ dependencies {
def support_version = '28.0.0'
def moshi_version = '1.8.0'
def lifecycle_version = "1.1.1"
def powermock_version = "2.0.0-RC.4"

implementation fileTree(dir: 'libs', include: ['*.aar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
@ -94,7 +96,14 @@ dependencies {


testImplementation 'junit:junit:4.12'
testImplementation 'org.robolectric:robolectric:4.0.2'
testImplementation 'org.robolectric:shadows-support-v4:3.0'
testImplementation "io.mockk:mockk:1.8.13.kotlin13"
testImplementation 'org.amshove.kluent:kluent-android:1.44'

androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'


}

View File

@ -1,26 +0,0 @@
package im.vector.matrix.android;

import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.*;

/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();

assertEquals("im.vector.matrix.android.test", appContext.getPackageName());
}
}

View File

@ -0,0 +1,15 @@
package im.vector.matrix.android

import android.content.Context
import android.support.test.InstrumentationRegistry
import java.io.File

abstract class InstrumentedTest {
fun context(): Context {
return InstrumentationRegistry.getTargetContext()
}

fun cacheDir(): File {
return context().cacheDir
}
}

View File

@ -0,0 +1,177 @@
package im.vector.matrix.android.session.room.timeline

import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.session.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
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 io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.kotlin.createObject
import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue
import org.amshove.kluent.shouldEqual
import org.junit.Before
import org.junit.Test
import kotlin.random.Random


internal class ChunkEntityTest : InstrumentedTest() {

private lateinit var monarchy: Monarchy

@Before
fun setup() {
Realm.init(context())
val testConfig = RealmConfiguration.Builder().inMemory().name("test-realm").build()
monarchy = Monarchy.Builder().setRealmConfiguration(testConfig).build()
}


@Test
fun add_shouldAdd_whenNotAlreadyIncluded() {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeEvent(false)
chunk.add(fakeEvent, PaginationDirection.FORWARDS)
chunk.events.size shouldEqual 1
}
}

@Test
fun add_shouldNotAdd_whenAlreadyIncluded() {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeEvent(false)
chunk.add(fakeEvent, PaginationDirection.FORWARDS)
chunk.add(fakeEvent, PaginationDirection.FORWARDS)
chunk.events.size shouldEqual 1
}
}

@Test
fun add_shouldStateIndexIncremented_whenStateEventIsAddedForward() {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeEvent(true)
chunk.add(fakeEvent, PaginationDirection.FORWARDS)
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 1
}
}

@Test
fun add_shouldStateIndexNotIncremented_whenNoStateEventIsAdded() {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeEvent(false)
chunk.add(fakeEvent, PaginationDirection.FORWARDS)
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 0
}
}

@Test
fun addAll_shouldStateIndexIncremented_whenStateEventsAreAddedForward() {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvents = createFakeListOfEvents(30)
val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size
chunk.addAll(fakeEvents, PaginationDirection.FORWARDS)
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual numberOfStateEvents
}
}

@Test
fun addAll_shouldStateIndexDecremented_whenStateEventsAreAddedBackward() {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvents = createFakeListOfEvents(30)
val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size
val lastIsState = fakeEvents.last().isStateEvent()
val expectedStateIndex = if (lastIsState) -numberOfStateEvents + 1 else -numberOfStateEvents
chunk.addAll(fakeEvents, PaginationDirection.BACKWARDS)
chunk.lastStateIndex(PaginationDirection.BACKWARDS) shouldEqual expectedStateIndex
}
}

@Test
fun merge_shouldAddEvents_whenMergingBackward() {
monarchy.runTransactionSync { realm ->
val chunk1: ChunkEntity = realm.createObject()
val chunk2: ChunkEntity = realm.createObject()
chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge(chunk2, PaginationDirection.BACKWARDS)
chunk1.events.size shouldEqual 60
}
}

@Test
fun merge_shouldEventsBeLinked_whenMergingLinkedWithUnlinked() {
monarchy.runTransactionSync { realm ->
val chunk1: ChunkEntity = realm.createObject()
val chunk2: ChunkEntity = realm.createObject()
chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = false)
chunk1.merge(chunk2, PaginationDirection.BACKWARDS)
chunk1.isUnlinked().shouldBeFalse()
}
}

@Test
fun merge_shouldEventsBeUnlinked_whenMergingUnlinkedWithUnlinked() {
monarchy.runTransactionSync { realm ->
val chunk1: ChunkEntity = realm.createObject()
val chunk2: ChunkEntity = realm.createObject()
chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk1.merge(chunk2, PaginationDirection.BACKWARDS)
chunk1.isUnlinked().shouldBeTrue()
}
}

@Test
fun merge_shouldPrevTokenMerged_whenMergingForwards() {
monarchy.runTransactionSync { realm ->
val chunk1: ChunkEntity = realm.createObject()
val chunk2: ChunkEntity = realm.createObject()
val prevToken = "prev_token"
chunk1.prevToken = prevToken
chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk1.merge(chunk2, PaginationDirection.FORWARDS)
chunk1.prevToken shouldEqual prevToken
}
}

@Test
fun merge_shouldNextTokenMerged_whenMergingBackwards() {
monarchy.runTransactionSync { realm ->
val chunk1: ChunkEntity = realm.createObject()
val chunk2: ChunkEntity = realm.createObject()
val nextToken = "next_token"
chunk1.nextToken = nextToken
chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk1.merge(chunk2, PaginationDirection.BACKWARDS)
chunk1.nextToken shouldEqual nextToken
}
}


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

@ -2,36 +2,55 @@ package im.vector.matrix.android.internal.database.helper

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.asEntity
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
import im.vector.matrix.android.internal.database.query.fastContains
import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import io.realm.Sort

internal fun ChunkEntity.deleteOnCascade() {
this.events.deleteAllFromRealm()
this.deleteFromRealm()
}

internal fun ChunkEntity.merge(chunkEntity: ChunkEntity,
// By default if a chunk is empty we consider it unlinked
internal fun ChunkEntity.isUnlinked(): Boolean {
return events.where().equalTo(EventEntityFields.IS_UNLINKED, false).findAll().isEmpty()
}

internal fun ChunkEntity.merge(chunkToMerge: ChunkEntity,
direction: PaginationDirection) {

val isChunkToMergeUnlinked = chunkToMerge.isUnlinked()
val isCurrentChunkUnlinked = this.isUnlinked()
val isUnlinked = isCurrentChunkUnlinked && isChunkToMergeUnlinked

chunkEntity.events.forEach {
addOrUpdate(it.asDomain(), direction)
if (isCurrentChunkUnlinked && !isChunkToMergeUnlinked) {
this.events.forEach { it.isUnlinked = false }
}
val eventsToMerge: List<EventEntity>
if (direction == PaginationDirection.FORWARDS) {
nextToken = chunkEntity.nextToken
this.nextToken = chunkToMerge.nextToken
this.isLast = chunkToMerge.isLast
eventsToMerge = chunkToMerge.events.reversed()
} else {
prevToken = chunkEntity.prevToken
this.prevToken = chunkToMerge.prevToken
eventsToMerge = chunkToMerge.events
}
eventsToMerge.forEach {
add(it, direction, isUnlinked = isUnlinked)
}
}

internal fun ChunkEntity.addAll(events: List<Event>,
direction: PaginationDirection,
stateIndexOffset: Int = 0) {
stateIndexOffset: Int = 0,
isUnlinked: Boolean = false) {

events.forEach { event ->
addOrUpdate(event, direction, stateIndexOffset)
add(event, direction, stateIndexOffset, isUnlinked)
}
}

@ -39,19 +58,27 @@ internal fun ChunkEntity.updateDisplayIndexes() {
events.forEachIndexed { index, eventEntity -> eventEntity.displayIndex = index }
}

internal fun ChunkEntity.addOrUpdate(event: Event,
direction: PaginationDirection,
stateIndexOffset: Int = 0) {
internal fun ChunkEntity.add(event: Event,
direction: PaginationDirection,
stateIndexOffset: Int = 0,
isUnlinked: Boolean = false) {
add(event.asEntity(), direction, stateIndexOffset, isUnlinked)
}

internal fun ChunkEntity.add(eventEntity: EventEntity,
direction: PaginationDirection,
stateIndexOffset: Int = 0,
isUnlinked: Boolean = false) {
if (!isManaged) {
throw IllegalStateException("Chunk entity should be managed to use fast contains")
}

if (event.eventId == null) {
if (eventEntity.eventId.isEmpty() || events.fastContains(eventEntity.eventId)) {
return
}

var currentStateIndex = lastStateIndex(direction, defaultValue = stateIndexOffset)
if (direction == PaginationDirection.FORWARDS && event.isStateEvent()) {
if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(eventEntity.type)) {
currentStateIndex += 1
} else if (direction == PaginationDirection.BACKWARDS && events.isNotEmpty()) {
val lastEventType = events.last()?.type ?: ""
@ -60,20 +87,15 @@ internal fun ChunkEntity.addOrUpdate(event: Event,
}
}

if (!events.fastContains(event.eventId)) {
val eventEntity = event.asEntity()
eventEntity.stateIndex = currentStateIndex
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size
events.add(position, eventEntity)
} else {
val eventEntity = events.find(event.eventId)
eventEntity?.stateIndex = currentStateIndex
}
eventEntity.stateIndex = currentStateIndex
eventEntity.isUnlinked = isUnlinked
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size
events.add(position, eventEntity)
}

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

View File

@ -8,8 +8,7 @@ import im.vector.matrix.android.internal.database.model.RoomEntity

internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) {
chunks.remove(chunkEntity)
chunkEntity.events.deleteAllFromRealm()
chunkEntity.deleteFromRealm()
chunkEntity.deleteOnCascade()
}

internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) {
@ -19,7 +18,9 @@ internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) {
}
}

internal fun RoomEntity.addStateEvents(stateEvents: List<Event>, stateIndex: Int = Int.MIN_VALUE) {
internal fun RoomEntity.addStateEvents(stateEvents: List<Event>,
stateIndex: Int = Int.MIN_VALUE,
isUnlinked: Boolean = false) {
if (!isManaged) {
throw IllegalStateException("Chunk entity should be managed to use fast contains")
}
@ -29,6 +30,7 @@ internal fun RoomEntity.addStateEvents(stateEvents: List<Event>, stateIndex: Int
}
val eventEntity = event.asEntity()
eventEntity.stateIndex = stateIndex
eventEntity.isUnlinked = isUnlinked
untimelinedStateEvents.add(eventEntity)
}
}

View File

@ -14,9 +14,16 @@ internal open class EventEntity(var eventId: String = "",
var age: Long? = 0,
var redacts: String? = null,
var stateIndex: Int = 0,
var displayIndex: Int = 0
var displayIndex: Int = 0,
var isUnlinked: Boolean = false
) : RealmObject() {

enum class LinkFilterMode {
LINKED_ONLY,
UNLINKED_ONLY,
BOTH
}

companion object {
const val DEFAULT_STATE_INDEX = Int.MIN_VALUE
}

View File

@ -6,6 +6,7 @@ import im.vector.matrix.android.internal.database.model.RoomEntityFields
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.kotlin.createObject
import io.realm.kotlin.where

internal fun ChunkEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<ChunkEntity> {
@ -34,4 +35,11 @@ internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds
return realm.where<ChunkEntity>()
.`in`(ChunkEntityFields.EVENTS.EVENT_ID, eventIds.toTypedArray())
.findAll()
}

internal fun ChunkEntity.Companion.create(realm: Realm, prevToken: String?, nextToken: String?): ChunkEntity {
return realm.createObject<ChunkEntity>().apply {
this.prevToken = prevToken
this.nextToken = nextToken
}
}

View File

@ -2,6 +2,7 @@ package im.vector.matrix.android.internal.database.query

import im.vector.matrix.android.internal.database.model.ChunkEntityFields
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.*
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.model.RoomEntityFields
import io.realm.Realm
@ -15,7 +16,10 @@ internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQu
.equalTo(EventEntityFields.EVENT_ID, eventId)
}

internal fun EventEntity.Companion.where(realm: Realm, roomId: String? = null, type: String? = null): RealmQuery<EventEntity> {
internal fun EventEntity.Companion.where(realm: Realm,
roomId: String? = null,
type: String? = null,
linkFilterMode: EventEntity.LinkFilterMode = LINKED_ONLY): RealmQuery<EventEntity> {
val query = realm.where<EventEntity>()
if (roomId != null) {
query.beginGroup()
@ -27,7 +31,11 @@ internal fun EventEntity.Companion.where(realm: Realm, roomId: String? = null, t
if (type != null) {
query.equalTo(EventEntityFields.TYPE, type)
}
return query
return when (linkFilterMode) {
LINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, false)
UNLINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, true)
BOTH -> query
}
}

internal fun RealmQuery<EventEntity>.next(from: Int? = null, strict: Boolean = true): EventEntity? {
@ -61,6 +69,7 @@ internal fun RealmList<EventEntity>.find(eventId: String): EventEntity? {
return this.where().equalTo(EventEntityFields.EVENT_ID, eventId).findFirst()
}

internal fun RealmList<EventEntity>.fastContains(eventId: String): Boolean {
internal fun RealmList<EventEntity>.
fastContains(eventId: String): Boolean {
return this.find(eventId) != null
}

View File

@ -14,6 +14,7 @@ import im.vector.matrix.android.internal.session.room.RoomAvatarResolver
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver
import im.vector.matrix.android.internal.session.room.members.RoomMemberDisplayNameResolver
import im.vector.matrix.android.internal.util.md5
import io.realm.RealmConfiguration
import org.koin.dsl.context.ModuleDefinition
import org.koin.dsl.module.Module
@ -31,12 +32,13 @@ internal class SessionModule(private val sessionParams: SessionParams) : Module

scope(DefaultSession.SCOPE) {
val context = get<Context>()
val directory = File(context.filesDir, sessionParams.credentials.userId)
val childPath = sessionParams.credentials.userId.md5()
val directory = File(context.filesDir, childPath)

RealmConfiguration.Builder()
.directory(directory)
.name("disk_store.realm")
.deleteRealmIfMigrationNeeded()
.inMemory()
.build()
}

@ -47,7 +49,7 @@ internal class SessionModule(private val sessionParams: SessionParams) : Module
}

scope(DefaultSession.SCOPE) {
val retrofitBuilder = get() as Retrofit.Builder
val retrofitBuilder = get<Retrofit.Builder>()
retrofitBuilder
.baseUrl(sessionParams.homeServerConnectionConfig.homeServerUri.toString())
.build()

View File

@ -2,18 +2,13 @@ package im.vector.matrix.android.internal.session.room

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.room.model.MessageContent
import im.vector.matrix.android.internal.network.NetworkConstants
import im.vector.matrix.android.internal.session.room.members.RoomMembersResponse
import im.vector.matrix.android.internal.session.room.send.SendResponse
import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEvent
import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
import retrofit2.http.*

internal interface RoomAPI {

@ -32,7 +27,7 @@ internal interface RoomAPI {
@Query("dir") dir: String,
@Query("limit") limit: Int,
@Query("filter") filter: String?
): Call<TokenChunkEvent>
): Call<PaginationResponse>


/**

View File

@ -7,9 +7,7 @@ import im.vector.matrix.android.api.session.room.send.EventFactory
import im.vector.matrix.android.internal.session.DefaultSession
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersRequest
import im.vector.matrix.android.internal.session.room.send.DefaultSendService
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineHolder
import im.vector.matrix.android.internal.session.room.timeline.PaginationRequest
import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback
import im.vector.matrix.android.internal.session.room.timeline.*
import im.vector.matrix.android.internal.util.PagingRequestHelper
import org.koin.dsl.context.ModuleDefinition
import org.koin.dsl.module.Module
@ -31,10 +29,18 @@ class RoomModule : Module {
LoadRoomMembersRequest(get(), get(), get())
}

scope(DefaultSession.SCOPE) {
TokenChunkEventPersistor(get())
}

scope(DefaultSession.SCOPE) {
PaginationRequest(get(), get(), get())
}

scope(DefaultSession.SCOPE) {
GetContextOfEventRequest(get(), get(), get())
}

scope(DefaultSession.SCOPE) {
val sessionParams = get<SessionParams>()
EventFactory(sessionParams.credentials)
@ -43,10 +49,9 @@ class RoomModule : Module {
factory { (roomId: String) ->
val helper = PagingRequestHelper(Executors.newSingleThreadExecutor())
val timelineBoundaryCallback = TimelineBoundaryCallback(roomId, get(), get(), helper)
DefaultTimelineHolder(roomId, get(), timelineBoundaryCallback) as TimelineHolder
DefaultTimelineHolder(roomId, get(), timelineBoundaryCallback, get()) as TimelineHolder
}


factory { (roomId: String) ->
DefaultSendService(roomId, get(), get()) as SendService
}

View File

@ -16,22 +16,26 @@ internal class RoomMemberExtractor(private val realm: Realm,

fun extractFrom(event: EventEntity): RoomMember? {
val sender = event.sender ?: return null
// If the event is unlinked we want to fetch unlinked state events
val unlinked = event.isUnlinked
// 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()
return if (event.stateIndex <= 0) {
baseQuery(realm, roomId, sender).next(from = event.stateIndex)?.asDomain()?.prevContent()
?: baseQuery(realm, roomId, sender).last(since = event.stateIndex)?.asDomain()?.content()
baseQuery(realm, roomId, sender, unlinked).next(from = event.stateIndex)?.asDomain()?.prevContent()
?: baseQuery(realm, roomId, sender, unlinked).last(since = event.stateIndex)?.asDomain()?.content()
} else {
baseQuery(realm, roomId, sender).last(since = event.stateIndex)?.asDomain()?.content()
baseQuery(realm, roomId, sender, unlinked).last(since = event.stateIndex)?.asDomain()?.content()
}
}

private fun baseQuery(realm: Realm,
roomId: String,
sender: String): RealmQuery<EventEntity> {
sender: String,
isUnlinked: Boolean): RealmQuery<EventEntity> {
val filterMode = if (isUnlinked) EventEntity.LinkFilterMode.UNLINKED_ONLY else EventEntity.LinkFilterMode.LINKED_ONLY

return EventEntity
.where(realm, roomId = roomId, type = EventType.STATE_ROOM_MEMBER)
.where(realm, roomId = roomId, type = EventType.STATE_ROOM_MEMBER, linkFilterMode = filterMode)
.equalTo(EventEntityFields.STATE_KEY, sender)

}

View File

@ -7,7 +7,7 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.SendService
import im.vector.matrix.android.api.session.room.send.EventFactory
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.helper.addOrUpdate
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
@ -33,7 +33,7 @@ internal class DefaultSendService(private val roomId: String,
monarchy.tryTransactionAsync { realm ->
val chunkEntity = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
?: return@tryTransactionAsync
chunkEntity.addOrUpdate(event, PaginationDirection.FORWARDS)
chunkEntity.add(event, PaginationDirection.FORWARDS)
chunkEntity.updateDisplayIndexes()
}


View File

@ -4,6 +4,7 @@ import android.arch.lifecycle.LiveData
import android.arch.paging.LivePagedListBuilder
import android.arch.paging.PagedList
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.interceptor.EnrichedEventInterceptor
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.api.session.room.TimelineHolder
@ -13,6 +14,7 @@ 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.events.interceptor.MessageEventInterceptor
import im.vector.matrix.android.internal.util.tryTransactionSync
import io.realm.Realm
import io.realm.RealmQuery

@ -20,7 +22,8 @@ private const val PAGE_SIZE = 30

internal class DefaultTimelineHolder(private val roomId: String,
private val monarchy: Monarchy,
private val boundaryCallback: TimelineBoundaryCallback
private val boundaryCallback: TimelineBoundaryCallback,
private val contextOfEventRequest: GetContextOfEventRequest
) : TimelineHolder {

private val eventInterceptors = ArrayList<EnrichedEventInterceptor>()
@ -31,8 +34,9 @@ internal class DefaultTimelineHolder(private val roomId: String,
}

override fun timeline(eventId: String?): LiveData<PagedList<EnrichedEvent>> {
clearUnlinkedEvents()
if (eventId != null) {
fetchEventIfNeeded()
fetchEventIfNeeded(eventId)
}
val realmDataSourceFactory = monarchy.createDataSourceFactory {
buildDataSourceFactoryQuery(it, eventId)
@ -60,18 +64,38 @@ internal class DefaultTimelineHolder(private val roomId: String,
return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder)
}

private fun fetchEventIfNeeded() {
private fun clearUnlinkedEvents() {
monarchy.tryTransactionSync { realm ->
val unlinkedEvents = EventEntity
.where(realm, roomId = roomId)
.equalTo(EventEntityFields.IS_UNLINKED, true)
.findAll()
unlinkedEvents.deleteAllFromRealm()
}
}

private fun fetchEventIfNeeded(eventId: String) {
if (!isEventPersisted(eventId)) {
contextOfEventRequest.execute(roomId, eventId, object : MatrixCallback<EventContextResponse> {})
}
}

private fun isEventPersisted(eventId: String): Boolean {
var isEventPersisted = false
monarchy.doWithRealm {
isEventPersisted = EventEntity.where(it, eventId = eventId).findFirst() != null
}
return isEventPersisted
}

private fun buildDataSourceFactoryQuery(realm: Realm, eventId: String?): RealmQuery<EventEntity> {
val query = if (eventId == null) {
EventEntity
.where(realm, roomId = roomId)
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY)
.equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST}", true)
} else {
EventEntity
.where(realm, roomId = roomId)
.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

@ -7,16 +7,14 @@ import im.vector.matrix.android.api.session.events.model.Event
@JsonClass(generateAdapter = true)
data class EventContextResponse(
@Json(name = "event") val event: Event,
@Json(name = "start") val prevToken: String? = null,
@Json(name = "start") override val start: String? = null,
@Json(name = "events_before") val eventsBefore: List<Event> = emptyList(),
@Json(name = "events_after") val eventsAfter: List<Event> = emptyList(),
@Json(name = "end") val nextToken: String? = null,
@Json(name = "state") val stateEvents: List<Event> = emptyList()
) {

val timelineEvents: List<Event> by lazy {
eventsBefore + event + eventsAfter
}
@Json(name = "end") override val end: String? = null,
@Json(name = "state") override val stateEvents: List<Event> = emptyList()
) : TokenChunkEvent {

override val events: List<Event>
get() = listOf(event)

}

View File

@ -1,31 +1,18 @@
package im.vector.matrix.android.internal.session.room.timeline

import arrow.core.Try
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
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.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.find
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.legacy.util.FilterUtil
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.util.CancelableCoroutine
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.tryTransactionSync
import io.realm.kotlin.createObject
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

internal class GetContextOfEventRequest(private val roomAPI: RoomAPI,
private val monarchy: Monarchy,
private val tokenChunkEventPersistor: TokenChunkEventPersistor,
private val coroutineDispatchers: MatrixCoroutineDispatchers
) {

@ -46,54 +33,11 @@ internal class GetContextOfEventRequest(private val roomAPI: RoomAPI,
filter: String?) = withContext(coroutineDispatchers.io) {

executeRequest<EventContextResponse> {
apiCall = roomAPI.getContextOfEvent(roomId, eventId, 1, filter)
apiCall = roomAPI.getContextOfEvent(roomId, eventId, 0, filter)
}.flatMap { response ->
insertInDb(response, roomId)
tokenChunkEventPersistor.insertInDb(response, roomId, PaginationDirection.BACKWARDS).map { response }
}
}

private fun insertInDb(response: EventContextResponse, roomId: String): Try<EventContextResponse> {
return monarchy
.tryTransactionSync { realm ->
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: throw IllegalStateException("You shouldn't use this method without a room")

val currentChunk = realm.createObject<ChunkEntity>().apply {
prevToken = response.prevToken
nextToken = response.nextToken
}

currentChunk.addOrUpdate(response.event, PaginationDirection.FORWARDS)
currentChunk.addAll(response.eventsAfter, PaginationDirection.FORWARDS)
currentChunk.addAll(response.eventsBefore, PaginationDirection.BACKWARDS)

// Now, handles chunk merge
val prevChunk = ChunkEntity.find(realm, roomId, nextToken = response.prevToken)
val nextChunk = ChunkEntity.find(realm, roomId, prevToken = response.nextToken)

if (prevChunk != null) {
currentChunk.merge(prevChunk, PaginationDirection.BACKWARDS)
roomEntity.deleteOnCascade(prevChunk)
}
if (nextChunk != null) {
currentChunk.merge(nextChunk, PaginationDirection.FORWARDS)
roomEntity.deleteOnCascade(nextChunk)
}
/*
val eventIds = response.timelineEvents.mapNotNull { it.eventId }
ChunkEntity
.findAllIncludingEvents(realm, eventIds)
.filter { it != currentChunk }
.forEach { overlapped ->
currentChunk.merge(overlapped, direction)
roomEntity.deleteOnCascade(overlapped)
}
*/
roomEntity.addOrUpdate(currentChunk)
roomEntity.addStateEvents(response.stateEvents)
}
.map { response }
}


}

View File

@ -13,4 +13,11 @@ internal enum class PaginationDirection(val value: String) {
*/
BACKWARDS("b");

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

}

View File

@ -1,40 +1,26 @@
package im.vector.matrix.android.internal.session.room.timeline

import arrow.core.Try
import arrow.core.failure
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
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.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.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.legacy.util.FilterUtil
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.util.CancelableCoroutine
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.tryTransactionSync
import io.realm.kotlin.createObject
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

internal class PaginationRequest(private val roomAPI: RoomAPI,
private val monarchy: Monarchy,
private val tokenChunkEventPersistor: TokenChunkEventPersistor,
private val coroutineDispatchers: MatrixCoroutineDispatchers
) {

fun execute(roomId: String,
from: String?,
direction: PaginationDirection,
limit: Int = 10,
limit: Int,
callback: MatrixCallback<TokenChunkEvent>
): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
@ -48,54 +34,19 @@ internal class PaginationRequest(private val roomAPI: RoomAPI,
private suspend fun execute(roomId: String,
from: String?,
direction: PaginationDirection,
limit: Int = 10,
limit: Int,
filter: String?) = withContext(coroutineDispatchers.io) {

if (from == null) {
return@withContext RuntimeException("From token shouldn't be null").failure<TokenChunkEvent>()
}
executeRequest<TokenChunkEvent> {
executeRequest<PaginationResponse> {
apiCall = roomAPI.getRoomMessagesFrom(roomId, from, direction.value, limit, filter)
}.flatMap { chunk ->
insertInDb(chunk, roomId, direction)
tokenChunkEventPersistor
.insertInDb(chunk, roomId, direction)
.map { chunk }
}
}

private fun insertInDb(receivedChunk: TokenChunkEvent, roomId: String, direction: PaginationDirection): Try<TokenChunkEvent> {
return monarchy
.tryTransactionSync { realm ->
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: throw IllegalStateException("You shouldn't use this method without a room")

val currentChunk = ChunkEntity.find(realm, roomId, prevToken = receivedChunk.nextToken)
?: realm.createObject()

currentChunk.prevToken = receivedChunk.prevToken
currentChunk.addAll(receivedChunk.events, direction)

// Now, handles chunk merge

val prevChunk = ChunkEntity.find(realm, roomId, nextToken = receivedChunk.prevToken)
if (prevChunk != null) {
currentChunk.merge(prevChunk, direction)
roomEntity.deleteOnCascade(prevChunk)
} else {
val eventIds = receivedChunk.events.mapNotNull { it.eventId }
ChunkEntity
.findAllIncludingEvents(realm, eventIds)
.filter { it != currentChunk }
.forEach { overlapped ->
currentChunk.merge(overlapped, direction)
roomEntity.deleteOnCascade(overlapped)
}
}

roomEntity.addOrUpdate(currentChunk)
// TODO : there is an issue with the pagination sending unwanted room member events
roomEntity.addStateEvents(receivedChunk.stateEvents)
}
.map { receivedChunk }
}


}

View File

@ -0,0 +1,13 @@
package im.vector.matrix.android.internal.session.room.timeline

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.Event

@JsonClass(generateAdapter = true)
internal data class PaginationResponse(
@Json(name = "start") override val start: String? = null,
@Json(name = "end") override val end: String? = null,
@Json(name = "chunk") override val events: List<Event> = emptyList(),
@Json(name = "state") override val stateEvents: List<Event> = emptyList()
) : TokenChunkEvent

View File

@ -8,7 +8,6 @@ import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
import im.vector.matrix.android.internal.util.PagingRequestHelper
import java.util.*
import java.util.concurrent.Executor

internal class TimelineBoundaryCallback(private val roomId: String,
private val paginationRequest: PaginationRequest,
@ -24,28 +23,37 @@ internal class TimelineBoundaryCallback(private val roomId: String,

override fun onItemAtEndLoaded(itemAtEnd: EnrichedEvent) {
helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) {
monarchy.doWithRealm { realm ->
if (itemAtEnd.root.eventId == null) {
return@doWithRealm
}
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(itemAtEnd.root.eventId)).firstOrNull()
paginationRequest.execute(roomId, chunkEntity?.prevToken, PaginationDirection.BACKWARDS, limit, callback = createCallback(it))
}
runPaginationRequest(it, itemAtEnd, PaginationDirection.BACKWARDS)
}
}

override fun onItemAtFrontLoaded(itemAtFront: EnrichedEvent) {
helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE) {
monarchy.doWithRealm { realm ->
if (itemAtFront.root.eventId == null) {
return@doWithRealm
}
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(itemAtFront.root.eventId)).firstOrNull()
paginationRequest.execute(roomId, chunkEntity?.nextToken, PaginationDirection.FORWARDS, limit, callback = createCallback(it))
}
runPaginationRequest(it, itemAtFront, PaginationDirection.FORWARDS)
}
}

private fun runPaginationRequest(requestCallback: PagingRequestHelper.Request.Callback,
item: EnrichedEvent,
direction: PaginationDirection) {
var token: String? = null
monarchy.doWithRealm { realm ->
if (item.root.eventId == null) {
return@doWithRealm
}
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(item.root.eventId)).firstOrNull()
token = if (direction == PaginationDirection.FORWARDS) chunkEntity?.nextToken else chunkEntity?.prevToken
}
paginationRequest.execute(
roomId = roomId,
from = token,
direction = direction,
limit = limit,
callback = createCallback(requestCallback)
)
}


private fun createCallback(pagingRequestCallback: PagingRequestHelper.Request.Callback) = object : MatrixCallback<TokenChunkEvent> {
override fun onSuccess(data: TokenChunkEvent) {
pagingRequestCallback.recordSuccess()

View File

@ -1,13 +1,10 @@
package im.vector.matrix.android.internal.session.room.timeline

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.Event

@JsonClass(generateAdapter = true)
internal data class TokenChunkEvent(
@Json(name = "start") val nextToken: String? = null,
@Json(name = "end") val prevToken: String? = null,
@Json(name = "chunk") val events: List<Event> = emptyList(),
@Json(name = "state") val stateEvents: List<Event> = emptyList()
)
internal interface TokenChunkEvent {
val start: String?
val end: String?
val events: List<Event>
val stateEvents: List<Event>
}

View File

@ -0,0 +1,93 @@
package im.vector.matrix.android.internal.session.room.timeline

import arrow.core.Try
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.helper.addAll
import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.helper.addStateEvents
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
import im.vector.matrix.android.internal.database.helper.isUnlinked
import im.vector.matrix.android.internal.database.helper.merge
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.create
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


internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {

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

return monarchy
.tryTransactionSync { realm ->
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: throw IllegalStateException("You shouldn't use this method without a room")

val nextToken: String?
val prevToken: String?
if (direction == PaginationDirection.FORWARDS) {
nextToken = receivedChunk.end
prevToken = receivedChunk.start
} else {
nextToken = receivedChunk.start
prevToken = receivedChunk.end
}
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.
// We try to look for a chunk next to the token,
// otherwise we create a whole new one

var currentChunk = if (direction == PaginationDirection.FORWARDS) {
prevChunk?.apply { this.nextToken = nextToken }
?: ChunkEntity.create(realm, prevToken, nextToken)
} else {
nextChunk?.apply { this.prevToken = prevToken }
?: ChunkEntity.create(realm, prevToken, nextToken)
}

currentChunk.addAll(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())
}
}

private fun handleMerge(roomEntity: RoomEntity,
direction: PaginationDirection,
currentChunk: ChunkEntity,
otherChunk: ChunkEntity): ChunkEntity {

// We always merge the bottom chunk into top chunk, so we are always merging backwards
return if (direction == PaginationDirection.BACKWARDS) {
currentChunk.merge(otherChunk, PaginationDirection.BACKWARDS)
roomEntity.deleteOnCascade(otherChunk)
currentChunk
} else {
otherChunk.merge(currentChunk, PaginationDirection.BACKWARDS)
roomEntity.deleteOnCascade(currentChunk)
otherChunk
}
}

}

View File

@ -0,0 +1,17 @@
package im.vector.matrix.android.internal.util

import java.security.MessageDigest

fun String.md5() = try {
val digest = MessageDigest.getInstance("md5")
digest.update(toByteArray())
val bytes = digest.digest()
val sb = StringBuilder()
for (i in bytes.indices) {
sb.append(String.format("%02X", bytes[i]))
}
sb.toString().toLowerCase()
} catch (exc: Exception) {
// Should not happen, but just in case
hashCode().toString()
}

View File

@ -1,17 +0,0 @@
package im.vector.matrix.android;

import org.junit.Test;

import static org.junit.Assert.*;

/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}