diff --git a/changelog.d/6461.bugfix b/changelog.d/6461.bugfix new file mode 100644 index 0000000000..1d3e4e14c5 --- /dev/null +++ b/changelog.d/6461.bugfix @@ -0,0 +1 @@ +Fix crashes on Timeline [Thread] due to range validation diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index e13f3f454f..7fa36969b1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -490,38 +490,11 @@ internal class TimelineChunk( private fun handleDatabaseChangeSet(results: RealmResults, changeSet: OrderedCollectionChangeSet) { val insertions = changeSet.insertionRanges for (range in insertions) { - // Check if the insertion's displayIndices match our expectations - or skip this insertion. - // Inconsistencies (missing messages) can happen otherwise if we get insertions before having loaded all timeline events of the chunk. - if (builtEvents.isNotEmpty()) { - // Check consistency to item before insertions - if (range.startIndex > 0) { - val firstInsertion = results[range.startIndex]!! - val lastBeforeInsertion = builtEvents[range.startIndex - 1] - if (firstInsertion.displayIndex + 1 != lastBeforeInsertion.displayIndex) { - Timber.i( - "handleDatabaseChangeSet: skip insertion at ${range.startIndex}/${builtEvents.size}, " + - "displayIndex mismatch at ${range.startIndex}: ${firstInsertion.displayIndex} -> ${lastBeforeInsertion.displayIndex}" - ) - continue - } - } - // Check consistency to item after insertions - if (range.startIndex < builtEvents.size) { - val lastInsertion = results[range.startIndex + range.length - 1]!! - val firstAfterInsertion = builtEvents[range.startIndex] - if (firstAfterInsertion.displayIndex + 1 != lastInsertion.displayIndex) { - Timber.i( - "handleDatabaseChangeSet: skip insertion at ${range.startIndex}/${builtEvents.size}, " + - "displayIndex mismatch at ${range.startIndex + range.length}: " + - "${firstAfterInsertion.displayIndex} -> ${lastInsertion.displayIndex}" - ) - continue - } - } - } + if (!validateInsertion(range, results)) continue val newItems = results .subList(range.startIndex, range.startIndex + range.length) .map { it.buildAndDecryptIfNeeded() } + builtEventsIndexes.entries.filter { it.value >= range.startIndex }.forEach { it.setValue(it.value + range.length) } newItems.mapIndexed { index, timelineEvent -> if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) { @@ -536,12 +509,9 @@ internal class TimelineChunk( for (range in modifications) { for (modificationIndex in (range.startIndex until range.startIndex + range.length)) { val updatedEntity = results[modificationIndex] ?: continue - val displayIndex = builtEventsIndexes[updatedEntity.eventId] - if (displayIndex == null) { - continue - } + val builtEventIndex = builtEventsIndexes[updatedEntity.eventId] ?: continue try { - builtEvents[displayIndex] = updatedEntity.buildAndDecryptIfNeeded() + builtEvents[builtEventIndex] = updatedEntity.buildAndDecryptIfNeeded() } catch (failure: Throwable) { Timber.v("Fail to update items at index: $modificationIndex") } @@ -558,6 +528,21 @@ internal class TimelineChunk( } } + private fun validateInsertion(range: OrderedCollectionChangeSet.Range, results: RealmResults): Boolean { + // Insertion can only happen from LastForward chunk after a sync. + if (isLastForward.get()) { + val firstBuiltEvent = builtEvents.firstOrNull() + if (firstBuiltEvent != null) { + val lastInsertion = results[range.startIndex + range.length - 1] ?: return false + if (firstBuiltEvent.displayIndex + 1 != lastInsertion.displayIndex) { + Timber.v("There is no continuation in the chunk, chunk is not fully loaded yet, skip insert.") + return false + } + } + } + return true + } + private fun getNextDisplayIndex(direction: Timeline.Direction): Int? { if (timelineEventEntities.isEmpty()) { return null