diff --git a/.idea/dictionaries/ganfra.xml b/.idea/dictionaries/ganfra.xml index 1ca7a97a77..7e1fdcddbb 100644 --- a/.idea/dictionaries/ganfra.xml +++ b/.idea/dictionaries/ganfra.xml @@ -5,6 +5,7 @@ coroutine merlins moshi + persistor synchronizer untimelined diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index 5f5810bba7..c4197eeffc 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -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() diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 5417dc13d4..49a9a3b72f 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -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' + } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/ExampleInstrumentedTest.java b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/ExampleInstrumentedTest.java deleted file mode 100644 index f9b3c62a4c..0000000000 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/ExampleInstrumentedTest.java +++ /dev/null @@ -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 Testing documentation - */ -@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()); - } -} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt new file mode 100644 index 0000000000..c726b7eb0f --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt @@ -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 + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt new file mode 100644 index 0000000000..b1c8993501 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt @@ -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 { + 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) + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index 15ba170e9f..f8d8207a3c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -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 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, 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 } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt index 07018c3523..4df958b467 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt @@ -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, stateIndex: Int = Int.MIN_VALUE) { +internal fun RoomEntity.addStateEvents(stateEvents: List, + 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, stateIndex: Int } val eventEntity = event.asEntity() eventEntity.stateIndex = stateIndex + eventEntity.isUnlinked = isUnlinked untimelinedStateEvents.add(eventEntity) } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt index 96ba986afb..2ea581bf30 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt @@ -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 } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt index e3b071a2ba..2035af90d3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt @@ -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 { @@ -34,4 +35,11 @@ internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds return realm.where() .`in`(ChunkEntityFields.EVENTS.EVENT_ID, eventIds.toTypedArray()) .findAll() +} + +internal fun ChunkEntity.Companion.create(realm: Realm, prevToken: String?, nextToken: String?): ChunkEntity { + return realm.createObject().apply { + this.prevToken = prevToken + this.nextToken = nextToken + } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt index 91a828c7ed..7b5822876d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt @@ -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 { +internal fun EventEntity.Companion.where(realm: Realm, + roomId: String? = null, + type: String? = null, + linkFilterMode: EventEntity.LinkFilterMode = LINKED_ONLY): RealmQuery { val query = realm.where() 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.next(from: Int? = null, strict: Boolean = true): EventEntity? { @@ -61,6 +69,7 @@ internal fun RealmList.find(eventId: String): EventEntity? { return this.where().equalTo(EventEntityFields.EVENT_ID, eventId).findFirst() } -internal fun RealmList.fastContains(eventId: String): Boolean { +internal fun RealmList. + fastContains(eventId: String): Boolean { return this.find(eventId) != null } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 4b9ddb6623..4dbc8a8cc9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -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() - 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() retrofitBuilder .baseUrl(sessionParams.homeServerConnectionConfig.homeServerUri.toString()) .build() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index 28c9776fb0..7c17f49f4b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -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 + ): Call /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index e2604ded43..9a7e897eaf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -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() 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 } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt index 08012cb5fd..4d473ff3f3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt @@ -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 { + sender: String, + isUnlinked: Boolean): RealmQuery { + 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) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 57e1fc6c9b..a0feebc02c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -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() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt index 5f24e15517..ba7acc2a02 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt @@ -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() @@ -31,8 +34,9 @@ internal class DefaultTimelineHolder(private val roomId: String, } override fun timeline(eventId: String?): LiveData> { + 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 {}) + } + } + + 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 { 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) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt index 79cfac72c9..99fac9154e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt @@ -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 = emptyList(), @Json(name = "events_after") val eventsAfter: List = emptyList(), - @Json(name = "end") val nextToken: String? = null, - @Json(name = "state") val stateEvents: List = emptyList() -) { - - val timelineEvents: List by lazy { - eventsBefore + event + eventsAfter - } + @Json(name = "end") override val end: String? = null, + @Json(name = "state") override val stateEvents: List = emptyList() +) : TokenChunkEvent { + override val events: List + get() = listOf(event) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt index 248c616a47..868d695305 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt @@ -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 { - 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 { - 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().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 } - } - } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationDirection.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationDirection.kt index b65a0ca0b5..ffb8dd0be3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationDirection.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationDirection.kt @@ -13,4 +13,11 @@ internal enum class PaginationDirection(val value: String) { */ BACKWARDS("b"); + fun reversed(): PaginationDirection { + return when (this) { + FORWARDS -> BACKWARDS + BACKWARDS -> FORWARDS + } + } + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationRequest.kt index 29a3e6c936..4f603b9b08 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationRequest.kt @@ -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 ): 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() } - executeRequest { + executeRequest { 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 { - 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 } - } - - } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationResponse.kt new file mode 100644 index 0000000000..eb1aad80cf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationResponse.kt @@ -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 = emptyList(), + @Json(name = "state") override val stateEvents: List = emptyList() +) : TokenChunkEvent \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineBoundaryCallback.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineBoundaryCallback.kt index ff06dbb57f..bab802e24d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineBoundaryCallback.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineBoundaryCallback.kt @@ -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 { override fun onSuccess(data: TokenChunkEvent) { pagingRequestCallback.recordSuccess() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt index 9e25da14b6..64995181ff 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt @@ -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 = emptyList(), - @Json(name = "state") val stateEvents: List = emptyList() -) \ No newline at end of file +internal interface TokenChunkEvent { + val start: String? + val end: String? + val events: List + val stateEvents: List +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt new file mode 100644 index 0000000000..bf4e9b490e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -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 { + + 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 + } + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt new file mode 100644 index 0000000000..fbfad9c2ad --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt @@ -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() +} diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/ExampleUnitTest.java b/matrix-sdk-android/src/test/java/im/vector/matrix/android/ExampleUnitTest.java deleted file mode 100644 index 86ea905e61..0000000000 --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/ExampleUnitTest.java +++ /dev/null @@ -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 Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file