mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-15 01:35:07 +08:00
Timeline : stabilize the pagedList replacement. Seems ok for phase0
This commit is contained in:
parent
ad243ae41f
commit
4154df7c21
@ -19,16 +19,9 @@ package im.vector.riotredesign.features.home
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import im.vector.riotredesign.core.glide.GlideApp
|
import im.vector.riotredesign.core.glide.GlideApp
|
||||||
import im.vector.riotredesign.features.home.group.GroupSummaryController
|
import im.vector.riotredesign.features.home.group.GroupSummaryController
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.CallItemFactory
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.DefaultItemFactory
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.MessageItemFactory
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.RoomHistoryVisibilityItemFactory
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.RoomMemberItemFactory
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.RoomNameItemFactory
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.RoomTopicItemFactory
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory
|
import im.vector.riotredesign.features.home.room.detail.timeline.factory.*
|
||||||
|
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||||
import im.vector.riotredesign.features.home.room.list.RoomSummaryController
|
import im.vector.riotredesign.features.home.room.list.RoomSummaryController
|
||||||
import im.vector.riotredesign.features.html.EventHtmlRenderer
|
import im.vector.riotredesign.features.html.EventHtmlRenderer
|
||||||
@ -57,7 +50,7 @@ class HomeModule {
|
|||||||
|
|
||||||
// Fragment scopes
|
// Fragment scopes
|
||||||
|
|
||||||
scope(ROOM_DETAIL_SCOPE) { (fragment: Fragment) ->
|
factory { (fragment: Fragment) ->
|
||||||
val eventHtmlRenderer = EventHtmlRenderer(GlideApp.with(fragment), fragment.requireContext(), get())
|
val eventHtmlRenderer = EventHtmlRenderer(GlideApp.with(fragment), fragment.requireContext(), get())
|
||||||
val timelineDateFormatter = TimelineDateFormatter(get())
|
val timelineDateFormatter = TimelineDateFormatter(get())
|
||||||
val timelineMediaSizeProvider = TimelineMediaSizeProvider()
|
val timelineMediaSizeProvider = TimelineMediaSizeProvider()
|
||||||
@ -74,11 +67,11 @@ class HomeModule {
|
|||||||
TimelineEventController(timelineDateFormatter, timelineItemFactory, timelineMediaSizeProvider)
|
TimelineEventController(timelineDateFormatter, timelineItemFactory, timelineMediaSizeProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
scope(ROOM_LIST_SCOPE) {
|
factory {
|
||||||
RoomSummaryController(get())
|
RoomSummaryController(get())
|
||||||
}
|
}
|
||||||
|
|
||||||
scope(GROUP_LIST_SCOPE) {
|
factory {
|
||||||
GroupSummaryController()
|
GroupSummaryController()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,8 @@ class HomeNavigator {
|
|||||||
addToBackstack: Boolean = false) {
|
addToBackstack: Boolean = false) {
|
||||||
Timber.v("Open room detail $roomId - $eventId - $addToBackstack")
|
Timber.v("Open room detail $roomId - $eventId - $addToBackstack")
|
||||||
activity?.let {
|
activity?.let {
|
||||||
val args = RoomDetailArgs(roomId, eventId)
|
//TODO enable eventId permalink. It doesn't work enough at the moment.
|
||||||
|
val args = RoomDetailArgs(roomId)
|
||||||
val roomDetailFragment = RoomDetailFragment.newInstance(args)
|
val roomDetailFragment = RoomDetailFragment.newInstance(args)
|
||||||
it.drawerLayout?.closeDrawer(Gravity.LEFT)
|
it.drawerLayout?.closeDrawer(Gravity.LEFT)
|
||||||
if (addToBackstack) {
|
if (addToBackstack) {
|
||||||
|
@ -25,7 +25,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
||||||
@ -111,15 +110,10 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
|||||||
it.dispatchTo(stateRestorer)
|
it.dispatchTo(stateRestorer)
|
||||||
it.dispatchTo(scrollOnNewMessageCallback)
|
it.dispatchTo(scrollOnNewMessageCallback)
|
||||||
}
|
}
|
||||||
recyclerView.addOnScrollListener(object : EndlessRecyclerViewScrollListener(layoutManager, Timeline.Direction.BACKWARDS) {
|
|
||||||
override fun onLoadMore() {
|
recyclerView.addOnScrollListener(
|
||||||
roomDetailViewModel.process(RoomDetailActions.LoadMore(Timeline.Direction.BACKWARDS))
|
EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction ->
|
||||||
}
|
roomDetailViewModel.process(RoomDetailActions.LoadMore(direction))
|
||||||
})
|
|
||||||
recyclerView.addOnScrollListener(object : EndlessRecyclerViewScrollListener(layoutManager, Timeline.Direction.FORWARDS) {
|
|
||||||
override fun onLoadMore() {
|
|
||||||
roomDetailViewModel.process(RoomDetailActions.LoadMore(Timeline.Direction.FORWARDS))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
recyclerView.setController(timelineEventController)
|
recyclerView.setController(timelineEventController)
|
||||||
timelineEventController.callback = this
|
timelineEventController.callback = this
|
||||||
|
@ -25,6 +25,7 @@ import im.vector.matrix.android.api.session.events.model.Event
|
|||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
import im.vector.riotredesign.core.platform.RiotViewModel
|
import im.vector.riotredesign.core.platform.RiotViewModel
|
||||||
import im.vector.riotredesign.features.home.room.VisibleRoomStore
|
import im.vector.riotredesign.features.home.room.VisibleRoomStore
|
||||||
|
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
|
||||||
import io.reactivex.rxkotlin.subscribeBy
|
import io.reactivex.rxkotlin.subscribeBy
|
||||||
import org.koin.android.ext.android.get
|
import org.koin.android.ext.android.get
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@ -38,10 +39,12 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
private val roomId = initialState.roomId
|
private val roomId = initialState.roomId
|
||||||
private val eventId = initialState.eventId
|
private val eventId = initialState.eventId
|
||||||
private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventDisplayed>()
|
private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventDisplayed>()
|
||||||
private val timeline = room.createTimeline(eventId)
|
private val timeline = room.createTimeline(eventId, TimelineDisplayableEvents.DISPLAYABLE_TYPES)
|
||||||
|
|
||||||
companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {
|
companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {
|
||||||
|
|
||||||
|
const val PAGINATION_COUNT = 50
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel? {
|
override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel? {
|
||||||
val currentSession = viewModelContext.activity.get<Session>()
|
val currentSession = viewModelContext.activity.get<Session>()
|
||||||
@ -52,7 +55,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
observeRoomSummary()
|
observeRoomSummary()
|
||||||
observeDisplayedEvents()
|
observeEventDisplayedActions()
|
||||||
room.loadRoomMembersIfNeeded()
|
room.loadRoomMembersIfNeeded()
|
||||||
timeline.start()
|
timeline.start()
|
||||||
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
|
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
|
||||||
@ -82,10 +85,10 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLoadMore(action: RoomDetailActions.LoadMore) {
|
private fun handleLoadMore(action: RoomDetailActions.LoadMore) {
|
||||||
timeline.paginate(action.direction, 50)
|
timeline.paginate(action.direction, PAGINATION_COUNT)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeDisplayedEvents() {
|
private fun observeEventDisplayedActions() {
|
||||||
// We are buffering scroll events for one second
|
// We are buffering scroll events for one second
|
||||||
// and keep the most recent one to set the read receipt on.
|
// and keep the most recent one to set the read receipt on.
|
||||||
displayedEventsObservable
|
displayedEventsObservable
|
||||||
@ -111,5 +114,4 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
timeline.dispose()
|
timeline.dispose()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -18,12 +18,9 @@ package im.vector.riotredesign.features.home.room.detail
|
|||||||
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import im.vector.riotredesign.core.platform.DefaultListUpdateCallback
|
import im.vector.riotredesign.core.platform.DefaultListUpdateCallback
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
|
|
||||||
class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager) : DefaultListUpdateCallback {
|
class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager) : DefaultListUpdateCallback {
|
||||||
|
|
||||||
var isLocked = AtomicBoolean(true)
|
|
||||||
|
|
||||||
override fun onInserted(position: Int, count: Int) {
|
override fun onInserted(position: Int, count: Int) {
|
||||||
if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0) {
|
if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0) {
|
||||||
layoutManager.scrollToPosition(0)
|
layoutManager.scrollToPosition(0)
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package im.vector.riotredesign.features.home.room.detail.timeline
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListUpdateCallback
|
import androidx.recyclerview.widget.ListUpdateCallback
|
||||||
@ -30,10 +31,7 @@ import im.vector.riotredesign.core.epoxy.LoadingItemModel_
|
|||||||
import im.vector.riotredesign.core.epoxy.RiotEpoxyModel
|
import im.vector.riotredesign.core.epoxy.RiotEpoxyModel
|
||||||
import im.vector.riotredesign.core.extensions.localDateTime
|
import im.vector.riotredesign.core.extensions.localDateTime
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory
|
import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineAsyncHelper
|
import im.vector.riotredesign.features.home.room.detail.timeline.helper.*
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem_
|
import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem_
|
||||||
import im.vector.riotredesign.features.media.MediaContentRenderer
|
import im.vector.riotredesign.features.media.MediaContentRenderer
|
||||||
|
|
||||||
@ -43,25 +41,41 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
private val backgroundHandler: Handler = TimelineAsyncHelper.getBackgroundHandler()
|
private val backgroundHandler: Handler = TimelineAsyncHelper.getBackgroundHandler()
|
||||||
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {
|
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun onEventVisible(event: TimelineEvent)
|
||||||
|
fun onUrlClicked(url: String)
|
||||||
|
fun onMediaClicked(mediaData: MediaContentRenderer.Data, view: View)
|
||||||
|
}
|
||||||
|
|
||||||
private val modelCache = arrayListOf<List<EpoxyModel<*>>>()
|
private val modelCache = arrayListOf<List<EpoxyModel<*>>>()
|
||||||
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
||||||
|
private var inSubmitList: Boolean = false
|
||||||
|
private var timeline: Timeline? = null
|
||||||
|
|
||||||
|
var callback: Callback? = null
|
||||||
|
|
||||||
private val listUpdateCallback = object : ListUpdateCallback {
|
private val listUpdateCallback = object : ListUpdateCallback {
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||||
|
assertUpdateCallbacksAllowed()
|
||||||
(position until (position + count)).forEach {
|
(position until (position + count)).forEach {
|
||||||
modelCache[it] = emptyList()
|
modelCache[it] = emptyList()
|
||||||
}
|
}
|
||||||
requestModelBuild()
|
requestModelBuild()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||||
//no-op
|
assertUpdateCallbacksAllowed()
|
||||||
|
val model = modelCache.removeAt(fromPosition)
|
||||||
|
modelCache.add(toPosition, model)
|
||||||
|
requestModelBuild()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun onInserted(position: Int, count: Int) = synchronized(modelCache) {
|
override fun onInserted(position: Int, count: Int) {
|
||||||
|
assertUpdateCallbacksAllowed()
|
||||||
if (modelCache.isNotEmpty() && position == modelCache.size) {
|
if (modelCache.isNotEmpty() && position == modelCache.size) {
|
||||||
modelCache[position - 1] = emptyList()
|
modelCache[position - 1] = emptyList()
|
||||||
}
|
}
|
||||||
@ -71,14 +85,15 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
requestModelBuild()
|
requestModelBuild()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
override fun onRemoved(position: Int, count: Int) {
|
override fun onRemoved(position: Int, count: Int) {
|
||||||
//no-op
|
assertUpdateCallbacksAllowed()
|
||||||
|
(0 until count).forEach {
|
||||||
|
modelCache.removeAt(position)
|
||||||
|
}
|
||||||
|
requestModelBuild()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var timeline: Timeline? = null
|
|
||||||
var callback: Callback? = null
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
requestModelBuild()
|
requestModelBuild()
|
||||||
@ -101,18 +116,34 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
.id("forward_loading_item")
|
.id("forward_loading_item")
|
||||||
.addWhen(Timeline.Direction.FORWARDS)
|
.addWhen(Timeline.Direction.FORWARDS)
|
||||||
|
|
||||||
add(getModels())
|
|
||||||
|
val timelineModels = getModels()
|
||||||
|
add(timelineModels)
|
||||||
|
|
||||||
LoadingItemModel_()
|
LoadingItemModel_()
|
||||||
.id("backward_loading_item")
|
.id("backward_loading_item")
|
||||||
.addWhen(Timeline.Direction.BACKWARDS)
|
.addWhen(Timeline.Direction.BACKWARDS)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun LoadingItemModel_.addWhen(direction: Timeline.Direction) {
|
// Timeline.LISTENER ***************************************************************************
|
||||||
val shouldAdd = timeline?.let {
|
|
||||||
it.hasMoreToLoad(direction) || !it.hasReachedEnd(direction)
|
override fun onUpdated(snapshot: List<TimelineEvent>) {
|
||||||
} ?: false
|
submitSnapshot(snapshot)
|
||||||
addIf(shouldAdd, this@TimelineEventController)
|
}
|
||||||
|
|
||||||
|
private fun submitSnapshot(newSnapshot: List<TimelineEvent>) {
|
||||||
|
backgroundHandler.post {
|
||||||
|
inSubmitList = true
|
||||||
|
val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot)
|
||||||
|
currentSnapshot = newSnapshot
|
||||||
|
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
||||||
|
diffResult.dispatchUpdatesTo(listUpdateCallback)
|
||||||
|
inSubmitList = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertUpdateCallbacksAllowed() {
|
||||||
|
require(inSubmitList || Looper.myLooper() == backgroundHandler.looper)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@ -128,7 +159,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
private fun buildItemModels(currentPosition: Int, items: List<TimelineEvent>): List<EpoxyModel<*>> {
|
private fun buildItemModels(currentPosition: Int, items: List<TimelineEvent>): List<EpoxyModel<*>> {
|
||||||
val epoxyModels = ArrayList<EpoxyModel<*>>()
|
val epoxyModels = ArrayList<EpoxyModel<*>>()
|
||||||
val event = items[currentPosition]
|
val event = items[currentPosition]
|
||||||
val nextEvent = if (currentPosition + 1 < items.size) items[currentPosition + 1] else null
|
val nextEvent = items.nextDisplayableEvent(currentPosition)
|
||||||
|
|
||||||
val date = event.root.localDateTime()
|
val date = event.root.localDateTime()
|
||||||
val nextDate = nextEvent?.root?.localDateTime()
|
val nextDate = nextEvent?.root?.localDateTime()
|
||||||
@ -147,26 +178,11 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
return epoxyModels
|
return epoxyModels
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeline.LISTENER ***************************************************************************
|
private fun LoadingItemModel_.addWhen(direction: Timeline.Direction) {
|
||||||
|
val shouldAdd = timeline?.let {
|
||||||
override fun onUpdated(snapshot: List<TimelineEvent>) {
|
it.hasMoreToLoad(direction)
|
||||||
submitSnapshot(snapshot)
|
} ?: false
|
||||||
}
|
addIf(shouldAdd, this@TimelineEventController)
|
||||||
|
|
||||||
private fun submitSnapshot(newSnapshot: List<TimelineEvent>) {
|
|
||||||
backgroundHandler.post {
|
|
||||||
val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot)
|
|
||||||
currentSnapshot = newSnapshot
|
|
||||||
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
|
||||||
diffResult.dispatchUpdatesTo(listUpdateCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
interface Callback {
|
|
||||||
fun onEventVisible(event: TimelineEvent)
|
|
||||||
fun onUrlClicked(url: String)
|
|
||||||
fun onMediaClicked(mediaData: MediaContentRenderer.Data, view: View)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -24,9 +24,9 @@ class TimelineItemAnimator : DefaultItemAnimator() {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
addDuration = ANIM_DURATION_IN_MILLIS
|
addDuration = ANIM_DURATION_IN_MILLIS
|
||||||
removeDuration = ANIM_DURATION_IN_MILLIS
|
removeDuration = 0
|
||||||
moveDuration = ANIM_DURATION_IN_MILLIS
|
moveDuration = 0
|
||||||
changeDuration = ANIM_DURATION_IN_MILLIS
|
changeDuration = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -42,8 +42,6 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
private val timelineDateFormatter: TimelineDateFormatter,
|
private val timelineDateFormatter: TimelineDateFormatter,
|
||||||
private val htmlRenderer: EventHtmlRenderer) {
|
private val htmlRenderer: EventHtmlRenderer) {
|
||||||
|
|
||||||
private val messagesDisplayedWithInformation = HashSet<String?>()
|
|
||||||
|
|
||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
nextEvent: TimelineEvent?,
|
nextEvent: TimelineEvent?,
|
||||||
callback: TimelineEventController.Callback?
|
callback: TimelineEventController.Callback?
|
||||||
@ -58,15 +56,12 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
|
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
|
||||||
?: false
|
?: false
|
||||||
|
|
||||||
if (addDaySeparator
|
val showInformation = addDaySeparator
|
||||||
|| nextRoomMember != roomMember
|
|| nextRoomMember != roomMember
|
||||||
|| nextEvent?.root?.type != EventType.MESSAGE
|
|| nextEvent?.root?.type != EventType.MESSAGE
|
||||||
|| isNextMessageReceivedMoreThanOneHourAgo) {
|
|| isNextMessageReceivedMoreThanOneHourAgo
|
||||||
messagesDisplayedWithInformation.add(event.root.eventId)
|
|
||||||
}
|
|
||||||
|
|
||||||
val messageContent: MessageContent = event.root.content.toModel() ?: return null
|
val messageContent: MessageContent = event.root.content.toModel() ?: return null
|
||||||
val showInformation = messagesDisplayedWithInformation.contains(event.root.eventId)
|
|
||||||
val time = timelineDateFormatter.formatMessageHour(date)
|
val time = timelineDateFormatter.formatMessageHour(date)
|
||||||
val avatarUrl = roomMember?.avatarUrl
|
val avatarUrl = roomMember?.avatarUrl
|
||||||
val memberName = roomMember?.displayName ?: event.root.sender
|
val memberName = roomMember?.displayName ?: event.root.sender
|
||||||
|
@ -1,114 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail.timeline.helper;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline;
|
|
||||||
|
|
||||||
public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnScrollListener {
|
|
||||||
// Sets the starting page index
|
|
||||||
private static final int startingPageIndex = 0;
|
|
||||||
// The minimum amount of items to have below your current scroll position
|
|
||||||
// before loading more.
|
|
||||||
private int visibleThreshold = 50;
|
|
||||||
// The total number of items in the dataset after the last load
|
|
||||||
private int previousTotalItemCount = 0;
|
|
||||||
// True if we are still waiting for the last set of data to load.
|
|
||||||
private boolean loading = true;
|
|
||||||
private LinearLayoutManager mLayoutManager;
|
|
||||||
private Timeline.Direction mDirection;
|
|
||||||
|
|
||||||
public EndlessRecyclerViewScrollListener(LinearLayoutManager layoutManager, Timeline.Direction direction) {
|
|
||||||
this.mLayoutManager = layoutManager;
|
|
||||||
this.mDirection = direction;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// This happens many times a second during a scroll, so be wary of the code you place here.
|
|
||||||
// We are given a few useful parameters to help us work out if we need to load some more data,
|
|
||||||
// but first we check if we are waiting for the previous load to finish.
|
|
||||||
@Override
|
|
||||||
public void onScrolled(@NonNull RecyclerView view, int dx, int dy) {
|
|
||||||
int lastVisibleItemPosition = 0;
|
|
||||||
int firstVisibleItemPosition = 0;
|
|
||||||
int totalItemCount = mLayoutManager.getItemCount();
|
|
||||||
|
|
||||||
lastVisibleItemPosition = mLayoutManager.findLastVisibleItemPosition();
|
|
||||||
firstVisibleItemPosition = mLayoutManager.findFirstVisibleItemPosition();
|
|
||||||
|
|
||||||
switch (mDirection) {
|
|
||||||
case BACKWARDS:
|
|
||||||
// If the total item count is zero and the previous isn't, assume the
|
|
||||||
// list is invalidated and should be reset back to initial state
|
|
||||||
if (totalItemCount < previousTotalItemCount) {
|
|
||||||
this.previousTotalItemCount = totalItemCount;
|
|
||||||
if (totalItemCount == 0) {
|
|
||||||
this.loading = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If it’s still loading, we check to see if the dataset count has
|
|
||||||
// changed, if so we conclude it has finished loading and update the current page
|
|
||||||
// number and total item count.
|
|
||||||
if (loading && (totalItemCount > previousTotalItemCount)) {
|
|
||||||
loading = false;
|
|
||||||
previousTotalItemCount = totalItemCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it isn’t currently loading, we check to see if we have breached
|
|
||||||
// the visibleThreshold and need to reload more data.
|
|
||||||
// If we do need to reload some more data, we execute onLoadMore to fetch the data.
|
|
||||||
// threshold should reflect how many total columns there are too
|
|
||||||
if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) {
|
|
||||||
onLoadMore();
|
|
||||||
loading = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case FORWARDS:
|
|
||||||
// If the total item count is zero and the previous isn't, assume the
|
|
||||||
// list is invalidated and should be reset back to initial state
|
|
||||||
if (totalItemCount < previousTotalItemCount) {
|
|
||||||
this.previousTotalItemCount = totalItemCount;
|
|
||||||
if (totalItemCount == 0) {
|
|
||||||
this.loading = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If it’s still loading, we check to see if the dataset count has
|
|
||||||
// changed, if so we conclude it has finished loading and update the current page
|
|
||||||
// number and total item count.
|
|
||||||
if (loading && (totalItemCount > previousTotalItemCount)) {
|
|
||||||
loading = false;
|
|
||||||
previousTotalItemCount = totalItemCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it isn’t currently loading, we check to see if we have breached
|
|
||||||
// the visibleThreshold and need to reload more data.
|
|
||||||
// If we do need to reload some more data, we execute onLoadMore to fetch the data.
|
|
||||||
// threshold should reflect how many total columns there are too
|
|
||||||
if (!loading && firstVisibleItemPosition < visibleThreshold) {
|
|
||||||
onLoadMore();
|
|
||||||
loading = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defines the process for actually loading more data based on page
|
|
||||||
public abstract void onLoadMore();
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotredesign.features.home.room.detail.timeline.helper
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
|
|
||||||
|
class EndlessRecyclerViewScrollListener(private val layoutManager: LinearLayoutManager,
|
||||||
|
private val visibleThreshold: Int,
|
||||||
|
private val onLoadMore: (Timeline.Direction) -> Unit
|
||||||
|
) : RecyclerView.OnScrollListener() {
|
||||||
|
|
||||||
|
// The total number of items in the dataset after the last load
|
||||||
|
private var previousTotalItemCount = 0
|
||||||
|
// True if we are still waiting for the last set of data to load.
|
||||||
|
private var loadingBackwards = true
|
||||||
|
private var loadingForwards = true
|
||||||
|
|
||||||
|
// This happens many times a second during a scroll, so be wary of the code you place here.
|
||||||
|
// We are given a few useful parameters to help us work out if we need to load some more data,
|
||||||
|
// but first we check if we are waiting for the previous load to finish.
|
||||||
|
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition()
|
||||||
|
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
||||||
|
val totalItemCount = layoutManager.itemCount
|
||||||
|
|
||||||
|
// The minimum amount of items to have below your current scroll position
|
||||||
|
// before loading more.
|
||||||
|
// If the total item count is zero and the previous isn't, assume the
|
||||||
|
// list is invalidated and should be reset back to initial state
|
||||||
|
if (totalItemCount < previousTotalItemCount) {
|
||||||
|
previousTotalItemCount = totalItemCount
|
||||||
|
if (totalItemCount == 0) {
|
||||||
|
loadingForwards = true
|
||||||
|
loadingBackwards = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If it’s still loading, we check to see if the dataset count has
|
||||||
|
// changed, if so we conclude it has finished loading
|
||||||
|
if (totalItemCount > previousTotalItemCount) {
|
||||||
|
loadingBackwards = false
|
||||||
|
loadingForwards = false
|
||||||
|
previousTotalItemCount = totalItemCount
|
||||||
|
}
|
||||||
|
// If it isn’t currently loading, we check to see if we have breached
|
||||||
|
// the visibleThreshold and need to reload more data.
|
||||||
|
if (!loadingBackwards && lastVisibleItemPosition + visibleThreshold > totalItemCount) {
|
||||||
|
loadingBackwards = true
|
||||||
|
onLoadMore(Timeline.Direction.BACKWARDS)
|
||||||
|
}
|
||||||
|
if (!loadingForwards && firstVisibleItemPosition < visibleThreshold) {
|
||||||
|
loadingForwards = true
|
||||||
|
onLoadMore(Timeline.Direction.FORWARDS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -25,23 +25,16 @@ private const val THREAD_NAME = "Timeline_Building_Thread"
|
|||||||
|
|
||||||
object TimelineAsyncHelper {
|
object TimelineAsyncHelper {
|
||||||
|
|
||||||
private var backgroundHandlerThread: HandlerThread? = null
|
|
||||||
private var backgroundHandler: Handler? = null
|
private var backgroundHandler: Handler? = null
|
||||||
|
|
||||||
fun getBackgroundHandler(): Handler {
|
fun getBackgroundHandler(): Handler {
|
||||||
if (backgroundHandler != null) {
|
return backgroundHandler ?: createBackgroundHandler().also { backgroundHandler = it }
|
||||||
backgroundHandler?.removeCallbacksAndMessages(null)
|
|
||||||
}
|
|
||||||
if (backgroundHandlerThread != null) {
|
|
||||||
backgroundHandlerThread?.quit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createBackgroundHandler(): Handler {
|
||||||
val handlerThread = HandlerThread(THREAD_NAME)
|
val handlerThread = HandlerThread(THREAD_NAME)
|
||||||
.also {
|
handlerThread.start()
|
||||||
backgroundHandlerThread = it
|
return Handler(handlerThread.looper)
|
||||||
it.start()
|
|
||||||
}
|
|
||||||
val looper = handlerThread.looper
|
|
||||||
return Handler(looper).also { backgroundHandler = it }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotredesign.features.home.room.detail.timeline.helper
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
|
||||||
|
object TimelineDisplayableEvents {
|
||||||
|
|
||||||
|
val DISPLAYABLE_TYPES = listOf(
|
||||||
|
EventType.MESSAGE,
|
||||||
|
EventType.STATE_ROOM_NAME,
|
||||||
|
EventType.STATE_ROOM_TOPIC,
|
||||||
|
EventType.STATE_ROOM_MEMBER,
|
||||||
|
EventType.STATE_HISTORY_VISIBILITY,
|
||||||
|
EventType.CALL_INVITE,
|
||||||
|
EventType.CALL_HANGUP,
|
||||||
|
EventType.CALL_ANSWER,
|
||||||
|
EventType.ENCRYPTED,
|
||||||
|
EventType.ENCRYPTION,
|
||||||
|
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||||
|
EventType.STICKER,
|
||||||
|
EventType.STATE_ROOM_CREATE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TimelineEvent.isDisplayable(): Boolean {
|
||||||
|
return TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<TimelineEvent>.filterDisplayableEvents(): List<TimelineEvent> {
|
||||||
|
return this.filter {
|
||||||
|
it.isDisplayable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<TimelineEvent>.nextDisplayableEvent(index: Int): TimelineEvent? {
|
||||||
|
return if (index == size - 1) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
subList(index + 1, this.size).firstOrNull { it.isDisplayable() }
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<View xmlns:android="http://schemas.android.com/apk/res/android"
|
<View xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="1dp"/>
|
android:layout_height="0dp" />
|
@ -8,7 +8,7 @@ buildscript {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.3.0'
|
classpath 'com.android.tools.build:gradle:3.3.2'
|
||||||
classpath "com.airbnb.okreplay:gradle-plugin:1.4.0"
|
classpath "com.airbnb.okreplay:gradle-plugin:1.4.0"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
|
||||||
|
@ -21,13 +21,10 @@ import androidx.test.annotation.UiThreadTest
|
|||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.InstrumentedTest
|
import im.vector.matrix.android.InstrumentedTest
|
||||||
import im.vector.matrix.android.LiveDataTestObserver
|
import im.vector.matrix.android.LiveDataTestObserver
|
||||||
import im.vector.matrix.android.MainThreadExecutor
|
|
||||||
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
|
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
|
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback
|
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
|
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.util.PagingRequestHelper
|
|
||||||
import im.vector.matrix.android.testCoroutineDispatchers
|
import im.vector.matrix.android.testCoroutineDispatchers
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
@ -56,10 +53,8 @@ internal class TimelineHolderTest : InstrumentedTest {
|
|||||||
val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy)
|
val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy)
|
||||||
val paginationTask = FakePaginationTask(tokenChunkEventPersistor)
|
val paginationTask = FakePaginationTask(tokenChunkEventPersistor)
|
||||||
val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor)
|
val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor)
|
||||||
val boundaryCallback = TimelineBoundaryCallback(roomId, taskExecutor, paginationTask, monarchy, PagingRequestHelper(MainThreadExecutor()))
|
|
||||||
|
|
||||||
RoomDataHelper.fakeInitialSync(monarchy, roomId)
|
RoomDataHelper.fakeInitialSync(monarchy, roomId)
|
||||||
val timelineHolder = DefaultTimelineService(roomId, monarchy, taskExecutor, boundaryCallback, getContextOfEventTask, RoomMemberExtractor(monarchy, roomId))
|
val timelineHolder = DefaultTimelineService(roomId, monarchy, taskExecutor, getContextOfEventTask, RoomMemberExtractor(roomId))
|
||||||
val timelineObserver = LiveDataTestObserver.test(timelineHolder.timeline())
|
val timelineObserver = LiveDataTestObserver.test(timelineHolder.timeline())
|
||||||
timelineObserver.awaitNextValue().assertHasValue()
|
timelineObserver.awaitNextValue().assertHasValue()
|
||||||
var timelineData = timelineObserver.value()
|
var timelineData = timelineObserver.value()
|
||||||
|
@ -18,35 +18,67 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.api.session.room.timeline
|
package im.vector.matrix.android.api.session.room.timeline
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Timeline instance represents a contiguous sequence of events in a room.
|
||||||
|
* <p>
|
||||||
|
* There are two kinds of timeline:
|
||||||
|
* <p>
|
||||||
|
* - live timelines: they process live events from the sync. You can paginate
|
||||||
|
* backwards but not forwards.
|
||||||
|
* <p>
|
||||||
|
* - past timelines: they start in the past from an `initialEventId`. You can paginate
|
||||||
|
* backwards and forwards.
|
||||||
|
*
|
||||||
|
*/
|
||||||
interface Timeline {
|
interface Timeline {
|
||||||
|
|
||||||
var listener: Timeline.Listener?
|
var listener: Timeline.Listener?
|
||||||
|
|
||||||
fun hasMoreToLoad(direction: Direction): Boolean
|
/**
|
||||||
fun hasReachedEnd(direction: Direction): Boolean
|
* This should be called before any other method after creating the timeline. It ensures the underlying database is open
|
||||||
fun size(): Int
|
*/
|
||||||
fun snapshot(): List<TimelineEvent>
|
|
||||||
fun paginate(direction: Direction, count: Int)
|
|
||||||
fun start()
|
fun start()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This should be called when you don't need the timeline. It ensures the underlying database get closed.
|
||||||
|
*/
|
||||||
fun dispose()
|
fun dispose()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the timeline can be enriched by paginating.
|
||||||
|
* @param the direction to check in
|
||||||
|
* @return true if timeline can be enriched
|
||||||
|
*/
|
||||||
|
fun hasMoreToLoad(direction: Direction): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the main method to enrich the timeline with new data.
|
||||||
|
* It will call the onUpdated method from [Listener] when the data will be processed.
|
||||||
|
* It also ensures only one pagination by direction is launched at a time, so you can safely call this multiple time in a row.
|
||||||
|
*/
|
||||||
|
fun paginate(direction: Direction, count: Int)
|
||||||
|
|
||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
|
/**
|
||||||
|
* Call when the timeline has been updated through pagination or sync.
|
||||||
|
* @param snapshot the most uptodate snapshot
|
||||||
|
*/
|
||||||
fun onUpdated(snapshot: List<TimelineEvent>)
|
fun onUpdated(snapshot: List<TimelineEvent>)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Direction(val value: String) {
|
|
||||||
/**
|
/**
|
||||||
* Forwards when the event is added to the end of the timeline.
|
* This is used to paginate in one or another direction.
|
||||||
* These events come from the /sync stream or from forwards pagination.
|
|
||||||
*/
|
*/
|
||||||
FORWARDS("f"),
|
enum class Direction {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Backwards when the event is added to the start of the timeline.
|
* It represents future events.
|
||||||
* These events come from a back pagination.
|
|
||||||
*/
|
*/
|
||||||
BACKWARDS("b");
|
FORWARDS,
|
||||||
|
/**
|
||||||
|
* It represents past events.
|
||||||
|
*/
|
||||||
|
BACKWARDS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -21,6 +21,13 @@ package im.vector.matrix.android.api.session.room.timeline
|
|||||||
*/
|
*/
|
||||||
interface TimelineService {
|
interface TimelineService {
|
||||||
|
|
||||||
fun createTimeline(eventId: String?): Timeline
|
/**
|
||||||
|
* Instantiate a [Timeline] with an optional initial eventId, to be used with permalink.
|
||||||
|
* You can filter the type you want to grab with the allowedTypes param.
|
||||||
|
* @param eventId the optional initial eventId.
|
||||||
|
* @param allowedTypes the optional filter types
|
||||||
|
* @return the instantiated timeline
|
||||||
|
*/
|
||||||
|
fun createTimeline(eventId: String?, allowedTypes: List<String>? = null): Timeline
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * Copyright 2019 New Vector Ltd
|
||||||
|
* *
|
||||||
|
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* * you may not use this file except in compliance with the License.
|
||||||
|
* * You may obtain a copy of the License at
|
||||||
|
* *
|
||||||
|
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* *
|
||||||
|
* * Unless required by applicable law or agreed to in writing, software
|
||||||
|
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* * See the License for the specific language governing permissions and
|
||||||
|
* * limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.database
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import io.realm.*
|
||||||
|
|
||||||
|
class RealmLiveData<T : RealmModel>(private val realmConfiguration: RealmConfiguration,
|
||||||
|
private val query: (Realm) -> RealmQuery<T>) : LiveData<RealmResults<T>>() {
|
||||||
|
|
||||||
|
private val listener = RealmChangeListener<RealmResults<T>> { results ->
|
||||||
|
value = results
|
||||||
|
}
|
||||||
|
|
||||||
|
private var realm: Realm? = null
|
||||||
|
private var results: RealmResults<T>? = null
|
||||||
|
|
||||||
|
override fun onActive() {
|
||||||
|
val realm = Realm.getInstance(realmConfiguration)
|
||||||
|
val results = query.invoke(realm).findAll()
|
||||||
|
value = results
|
||||||
|
results.addChangeListener(listener)
|
||||||
|
this.realm = realm
|
||||||
|
this.results = results
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInactive() {
|
||||||
|
results?.removeChangeListener(listener)
|
||||||
|
results = null
|
||||||
|
realm?.close()
|
||||||
|
realm = null
|
||||||
|
}
|
||||||
|
}
|
@ -54,11 +54,11 @@ internal fun ChunkEntity.merge(roomId: String,
|
|||||||
if (direction == PaginationDirection.FORWARDS) {
|
if (direction == PaginationDirection.FORWARDS) {
|
||||||
this.nextToken = chunkToMerge.nextToken
|
this.nextToken = chunkToMerge.nextToken
|
||||||
this.isLastForward = chunkToMerge.isLastForward
|
this.isLastForward = chunkToMerge.isLastForward
|
||||||
eventsToMerge = chunkToMerge.events.reversed()
|
eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
|
||||||
} else {
|
} else {
|
||||||
this.prevToken = chunkToMerge.prevToken
|
this.prevToken = chunkToMerge.prevToken
|
||||||
this.isLastBackward = chunkToMerge.isLastBackward
|
this.isLastBackward = chunkToMerge.isLastBackward
|
||||||
eventsToMerge = chunkToMerge.events
|
eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||||
}
|
}
|
||||||
eventsToMerge.forEach {
|
eventsToMerge.forEach {
|
||||||
add(roomId, it.asDomain(), direction, isUnlinked = isUnlinked)
|
add(roomId, it.asDomain(), direction, isUnlinked = isUnlinked)
|
||||||
@ -107,7 +107,8 @@ internal fun ChunkEntity.add(roomId: String,
|
|||||||
this.displayIndex = currentDisplayIndex
|
this.displayIndex = currentDisplayIndex
|
||||||
}
|
}
|
||||||
// We are not using the order of the list, but will be sorting with displayIndex field
|
// We are not using the order of the list, but will be sorting with displayIndex field
|
||||||
events.add(eventEntity)
|
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size
|
||||||
|
events.add(position, eventEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ChunkEntity.assertIsManaged() {
|
private fun ChunkEntity.assertIsManaged() {
|
||||||
|
@ -19,13 +19,14 @@ package im.vector.matrix.android.internal.database.model
|
|||||||
import io.realm.RealmList
|
import io.realm.RealmList
|
||||||
import io.realm.RealmObject
|
import io.realm.RealmObject
|
||||||
import io.realm.RealmResults
|
import io.realm.RealmResults
|
||||||
|
import io.realm.annotations.Index
|
||||||
import io.realm.annotations.LinkingObjects
|
import io.realm.annotations.LinkingObjects
|
||||||
|
|
||||||
internal open class ChunkEntity(var prevToken: String? = null,
|
internal open class ChunkEntity(@Index var prevToken: String? = null,
|
||||||
var nextToken: String? = null,
|
@Index var nextToken: String? = null,
|
||||||
var events: RealmList<EventEntity> = RealmList(),
|
var events: RealmList<EventEntity> = RealmList(),
|
||||||
var isLastForward: Boolean = false,
|
@Index var isLastForward: Boolean = false,
|
||||||
var isLastBackward: Boolean = false
|
@Index var isLastBackward: Boolean = false
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
@LinkingObjects("chunks")
|
@LinkingObjects("chunks")
|
||||||
|
@ -26,17 +26,17 @@ import java.util.*
|
|||||||
internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUID().toString(),
|
internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUID().toString(),
|
||||||
@Index var eventId: String = "",
|
@Index var eventId: String = "",
|
||||||
var roomId: String = "",
|
var roomId: String = "",
|
||||||
var type: String = "",
|
@Index var type: String = "",
|
||||||
var content: String? = null,
|
var content: String? = null,
|
||||||
var prevContent: String? = null,
|
var prevContent: String? = null,
|
||||||
var stateKey: String? = null,
|
@Index var stateKey: String? = null,
|
||||||
var originServerTs: Long? = null,
|
var originServerTs: Long? = null,
|
||||||
var sender: String? = null,
|
@Index var sender: String? = null,
|
||||||
var age: Long? = 0,
|
var age: Long? = 0,
|
||||||
var redacts: String? = null,
|
var redacts: String? = null,
|
||||||
var stateIndex: Int = 0,
|
@Index var stateIndex: Int = 0,
|
||||||
var displayIndex: Int = 0,
|
@Index var displayIndex: Int = 0,
|
||||||
var isUnlinked: Boolean = false
|
@Index var isUnlinked: Boolean = false
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
enum class LinkFilterMode {
|
enum class LinkFilterMode {
|
||||||
|
@ -77,7 +77,7 @@ internal fun RealmQuery<EventEntity>.next(from: Int? = null, strict: Boolean = t
|
|||||||
.findFirst()
|
.findFirst()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun RealmQuery<EventEntity>.last(since: Int? = null, strict: Boolean = false): EventEntity? {
|
internal fun RealmQuery<EventEntity>.prev(since: Int? = null, strict: Boolean = false): EventEntity? {
|
||||||
if (since != null) {
|
if (since != null) {
|
||||||
if (strict) {
|
if (strict) {
|
||||||
this.lessThan(EventEntityFields.STATE_INDEX, since)
|
this.lessThan(EventEntityFields.STATE_INDEX, since)
|
||||||
|
@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.room.read.ReadService
|
|||||||
import im.vector.matrix.android.api.session.room.send.SendService
|
import im.vector.matrix.android.api.session.room.send.SendService
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
||||||
import im.vector.matrix.android.api.util.Cancelable
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
|
import im.vector.matrix.android.internal.database.RealmLiveData
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
|
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
|
||||||
@ -50,13 +51,11 @@ internal class DefaultRoom(
|
|||||||
ReadService by readService {
|
ReadService by readService {
|
||||||
|
|
||||||
override val roomSummary: LiveData<RoomSummary> by lazy {
|
override val roomSummary: LiveData<RoomSummary> by lazy {
|
||||||
val liveData = monarchy
|
val liveRealmData = RealmLiveData<RoomSummaryEntity>(monarchy.realmConfiguration) { realm ->
|
||||||
.findAllMappedWithChanges(
|
RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
|
||||||
{ realm -> RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) },
|
}
|
||||||
{ from -> from.asDomain() })
|
Transformations.map(liveRealmData) { results ->
|
||||||
|
results.map { it.asDomain() }.first()
|
||||||
Transformations.map(liveData) {
|
|
||||||
it.first()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ import im.vector.matrix.android.api.session.room.model.RoomAvatarContent
|
|||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||||
import im.vector.matrix.android.internal.database.query.last
|
import im.vector.matrix.android.internal.database.query.prev
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.session.room.members.RoomMembers
|
import im.vector.matrix.android.internal.session.room.members.RoomMembers
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ internal class RoomAvatarResolver(private val monarchy: Monarchy,
|
|||||||
var res: String? = null
|
var res: String? = null
|
||||||
monarchy.doWithRealm { realm ->
|
monarchy.doWithRealm { realm ->
|
||||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||||
val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_AVATAR).last()?.asDomain()
|
val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_AVATAR).prev()?.asDomain()
|
||||||
res = roomName?.content.toModel<RoomAvatarContent>()?.avatarUrl
|
res = roomName?.content.toModel<RoomAvatarContent>()?.avatarUrl
|
||||||
if (!res.isNullOrEmpty()) {
|
if (!res.isNullOrEmpty()) {
|
||||||
return@doWithRealm
|
return@doWithRealm
|
||||||
|
@ -30,8 +30,6 @@ import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEvent
|
|||||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
|
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
|
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.util.PagingRequestHelper
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask,
|
internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||||
private val monarchy: Monarchy,
|
private val monarchy: Monarchy,
|
||||||
@ -43,10 +41,9 @@ internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask,
|
|||||||
private val taskExecutor: TaskExecutor) {
|
private val taskExecutor: TaskExecutor) {
|
||||||
|
|
||||||
fun instantiate(roomId: String): Room {
|
fun instantiate(roomId: String): Room {
|
||||||
val helper = PagingRequestHelper(Executors.newSingleThreadExecutor())
|
val roomMemberExtractor = RoomMemberExtractor(roomId)
|
||||||
val roomMemberExtractor = RoomMemberExtractor(monarchy, roomId)
|
|
||||||
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor)
|
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor)
|
||||||
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask, helper)
|
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask)
|
||||||
val sendService = DefaultSendService(roomId, eventFactory, monarchy)
|
val sendService = DefaultSendService(roomId, eventFactory, monarchy)
|
||||||
val readService = DefaultReadService(roomId, monarchy, setReadMarkersTask, taskExecutor)
|
val readService = DefaultReadService(roomId, monarchy, setReadMarkersTask, taskExecutor)
|
||||||
return DefaultRoom(
|
return DefaultRoom(
|
||||||
|
@ -27,8 +27,8 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
|
|||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.query.last
|
|
||||||
import im.vector.matrix.android.internal.database.query.latestEvent
|
import im.vector.matrix.android.internal.database.query.latestEvent
|
||||||
|
import im.vector.matrix.android.internal.database.query.prev
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver
|
import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver
|
||||||
import im.vector.matrix.android.internal.session.room.members.RoomMembers
|
import im.vector.matrix.android.internal.session.room.members.RoomMembers
|
||||||
@ -59,7 +59,7 @@ internal class RoomSummaryUpdater(monarchy: Monarchy,
|
|||||||
?: realm.createObject(roomId)
|
?: realm.createObject(roomId)
|
||||||
|
|
||||||
val lastEvent = EventEntity.latestEvent(realm, roomId, includedTypes = listOf(EventType.MESSAGE))
|
val lastEvent = EventEntity.latestEvent(realm, roomId, includedTypes = listOf(EventType.MESSAGE))
|
||||||
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).last()?.asDomain()
|
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain()
|
||||||
|
|
||||||
val otherRoomMembers = RoomMembers(realm, roomId).getLoaded().filterKeys { it != credentials.userId }
|
val otherRoomMembers = RoomMembers(realm, roomId).getLoaded().filterKeys { it != credentials.userId }
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
|
|||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.query.last
|
import im.vector.matrix.android.internal.database.query.prev
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -58,19 +58,19 @@ internal class RoomDisplayNameResolver(private val monarchy: Monarchy,
|
|||||||
var name: CharSequence? = null
|
var name: CharSequence? = null
|
||||||
monarchy.doWithRealm { realm ->
|
monarchy.doWithRealm { realm ->
|
||||||
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
|
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
|
||||||
val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_NAME).last()?.asDomain()
|
val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_NAME).prev()?.asDomain()
|
||||||
name = roomName?.content.toModel<RoomNameContent>()?.name
|
name = roomName?.content.toModel<RoomNameContent>()?.name
|
||||||
if (!name.isNullOrEmpty()) {
|
if (!name.isNullOrEmpty()) {
|
||||||
return@doWithRealm
|
return@doWithRealm
|
||||||
}
|
}
|
||||||
|
|
||||||
val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).last()?.asDomain()
|
val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).prev()?.asDomain()
|
||||||
name = canonicalAlias?.content.toModel<RoomCanonicalAliasContent>()?.canonicalAlias
|
name = canonicalAlias?.content.toModel<RoomCanonicalAliasContent>()?.canonicalAlias
|
||||||
if (!name.isNullOrEmpty()) {
|
if (!name.isNullOrEmpty()) {
|
||||||
return@doWithRealm
|
return@doWithRealm
|
||||||
}
|
}
|
||||||
|
|
||||||
val aliases = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).last()?.asDomain()
|
val aliases = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev()?.asDomain()
|
||||||
name = aliases?.content.toModel<RoomAliasesContent>()?.aliases?.firstOrNull()
|
name = aliases?.content.toModel<RoomAliasesContent>()?.aliases?.firstOrNull()
|
||||||
if (!name.isNullOrEmpty()) {
|
if (!name.isNullOrEmpty()) {
|
||||||
return@doWithRealm
|
return@doWithRealm
|
||||||
|
@ -16,20 +16,19 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.session.room.members
|
package im.vector.matrix.android.internal.session.room.members
|
||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||||
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||||
import im.vector.matrix.android.internal.database.query.last
|
|
||||||
import im.vector.matrix.android.internal.database.query.next
|
import im.vector.matrix.android.internal.database.query.next
|
||||||
|
import im.vector.matrix.android.internal.database.query.prev
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
import io.realm.Realm
|
||||||
import io.realm.RealmQuery
|
import io.realm.RealmQuery
|
||||||
|
|
||||||
internal class RoomMemberExtractor(private val monarchy: Monarchy,
|
internal class RoomMemberExtractor(private val roomId: String) {
|
||||||
private val roomId: String) {
|
|
||||||
|
|
||||||
private val cached = HashMap<String, RoomMember?>()
|
private val cached = HashMap<String, RoomMember?>()
|
||||||
|
|
||||||
@ -44,30 +43,25 @@ internal class RoomMemberExtractor(private val monarchy: Monarchy,
|
|||||||
// When stateIndex is negative, we try to get the next stateEvent prevContent()
|
// 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()
|
// If prevContent is null we fallback to the Int.MIN state events content()
|
||||||
val content = if (event.stateIndex <= 0) {
|
val content = if (event.stateIndex <= 0) {
|
||||||
baseQuery(monarchy, roomId, sender, unlinked).next(from = event.stateIndex)?.prevContent
|
baseQuery(event.realm, roomId, sender, unlinked).next(from = event.stateIndex)?.prevContent
|
||||||
?: baseQuery(monarchy, roomId, sender, unlinked).last(since = event.stateIndex)?.content
|
?: baseQuery(event.realm, roomId, sender, unlinked).prev(since = event.stateIndex)?.content
|
||||||
} else {
|
} else {
|
||||||
baseQuery(monarchy, roomId, sender, unlinked).last(since = event.stateIndex)?.content
|
baseQuery(event.realm, roomId, sender, unlinked).prev(since = event.stateIndex)?.content
|
||||||
}
|
}
|
||||||
val roomMember: RoomMember? = ContentMapper.map(content).toModel()
|
val roomMember: RoomMember? = ContentMapper.map(content).toModel()
|
||||||
cached[cacheKey] = roomMember
|
cached[cacheKey] = roomMember
|
||||||
return roomMember
|
return roomMember
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun baseQuery(monarchy: Monarchy,
|
private fun baseQuery(realm: Realm,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
sender: String,
|
sender: String,
|
||||||
isUnlinked: Boolean): RealmQuery<EventEntity> {
|
isUnlinked: Boolean): RealmQuery<EventEntity> {
|
||||||
|
|
||||||
lateinit var query: RealmQuery<EventEntity>
|
|
||||||
val filterMode = if (isUnlinked) EventEntity.LinkFilterMode.UNLINKED_ONLY else EventEntity.LinkFilterMode.LINKED_ONLY
|
val filterMode = if (isUnlinked) EventEntity.LinkFilterMode.UNLINKED_ONLY else EventEntity.LinkFilterMode.LINKED_ONLY
|
||||||
monarchy.doWithRealm { realm ->
|
return EventEntity
|
||||||
query = EventEntity
|
|
||||||
.where(realm, roomId = roomId, type = EventType.STATE_ROOM_MEMBER, linkFilterMode = filterMode)
|
.where(realm, roomId = roomId, type = EventType.STATE_ROOM_MEMBER, linkFilterMode = filterMode)
|
||||||
.equalTo(EventEntityFields.STATE_KEY, sender)
|
.equalTo(EventEntityFields.STATE_KEY, sender)
|
||||||
}
|
}
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -20,6 +20,7 @@ package im.vector.matrix.android.internal.session.room.timeline
|
|||||||
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.HandlerThread
|
import android.os.HandlerThread
|
||||||
|
import android.os.Looper
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
@ -30,19 +31,21 @@ import im.vector.matrix.android.internal.database.model.ChunkEntity
|
|||||||
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
|
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
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.model.EventEntityFields
|
||||||
|
import im.vector.matrix.android.internal.database.query.findIncludingEvent
|
||||||
|
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
import im.vector.matrix.android.internal.util.PagingRequestHelper
|
|
||||||
import io.realm.*
|
import io.realm.*
|
||||||
|
import timber.log.Timber
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
|
|
||||||
private const val INITIAL_LOAD_SIZE = 20
|
private const val INITIAL_LOAD_SIZE = 20
|
||||||
|
private const val MIN_FETCHING_COUNT = 30
|
||||||
|
private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE
|
||||||
private const val THREAD_NAME = "TIMELINE_DB_THREAD"
|
private const val THREAD_NAME = "TIMELINE_DB_THREAD"
|
||||||
|
|
||||||
internal class DefaultTimeline(
|
internal class DefaultTimeline(
|
||||||
@ -53,153 +56,237 @@ internal class DefaultTimeline(
|
|||||||
private val contextOfEventTask: GetContextOfEventTask,
|
private val contextOfEventTask: GetContextOfEventTask,
|
||||||
private val timelineEventFactory: TimelineEventFactory,
|
private val timelineEventFactory: TimelineEventFactory,
|
||||||
private val paginationTask: PaginationTask,
|
private val paginationTask: PaginationTask,
|
||||||
private val helper: PagingRequestHelper
|
private val allowedTypes: List<String>?
|
||||||
) : Timeline {
|
) : Timeline {
|
||||||
|
|
||||||
override var listener: Timeline.Listener? = null
|
override var listener: Timeline.Listener? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
listener?.onUpdated(snapshot())
|
backgroundHandler.get()?.post {
|
||||||
|
val snapshot = snapshot()
|
||||||
|
mainHandler.post { listener?.onUpdated(snapshot) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val isStarted = AtomicBoolean(false)
|
private val isStarted = AtomicBoolean(false)
|
||||||
private val handlerThread = AtomicReference<HandlerThread>()
|
private val isReady = AtomicBoolean(false)
|
||||||
private val handler = AtomicReference<Handler>()
|
private val backgroundHandlerThread = AtomicReference<HandlerThread>()
|
||||||
private val realm = AtomicReference<Realm>()
|
private val backgroundHandler = AtomicReference<Handler>()
|
||||||
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
|
private val backgroundRealm = AtomicReference<Realm>()
|
||||||
private val cancelableBag = CancelableBag()
|
private val cancelableBag = CancelableBag()
|
||||||
|
|
||||||
private lateinit var liveEvents: RealmResults<EventEntity>
|
private lateinit var liveEvents: RealmResults<EventEntity>
|
||||||
private var prevDisplayIndex: Int = 0
|
|
||||||
private var nextDisplayIndex: Int = 0
|
private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
|
||||||
|
private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
|
||||||
private val isLive = initialEventId == null
|
private val isLive = initialEventId == null
|
||||||
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
|
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
|
||||||
|
|
||||||
|
private val backwardsPaginationState = AtomicReference(PaginationState())
|
||||||
|
private val forwardsPaginationState = AtomicReference(PaginationState())
|
||||||
|
|
||||||
private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<EventEntity>> { _, changeSet ->
|
private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<EventEntity>> { _, changeSet ->
|
||||||
if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) {
|
// TODO HANDLE CHANGES
|
||||||
handleInitialLoad()
|
changeSet.insertionRanges.forEach { range ->
|
||||||
|
val (startDisplayIndex, direction) = if (range.startIndex == 0) {
|
||||||
|
Pair(liveEvents[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS)
|
||||||
} else {
|
} else {
|
||||||
changeSet.insertionRanges.forEach {
|
Pair(liveEvents[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS)
|
||||||
val (startDisplayIndex, direction) = if (it.startIndex == 0) {
|
|
||||||
Pair(liveEvents[it.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS)
|
|
||||||
} else {
|
|
||||||
Pair(liveEvents[it.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS)
|
|
||||||
}
|
}
|
||||||
insertFromLiveResults(startDisplayIndex, direction, it.length.toLong())
|
val state = getPaginationState(direction)
|
||||||
|
if (state.isPaginating) {
|
||||||
|
// We are getting new items from pagination
|
||||||
|
paginateInternal(startDisplayIndex, direction, state.requestedCount)
|
||||||
|
} else {
|
||||||
|
// We are getting new items from sync
|
||||||
|
buildTimelineEvents(startDisplayIndex, direction, range.length.toLong())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Public methods ******************************************************************************
|
||||||
|
|
||||||
override fun paginate(direction: Timeline.Direction, count: Int) {
|
override fun paginate(direction: Timeline.Direction, count: Int) {
|
||||||
handler.get()?.post {
|
backgroundHandler.get()?.post {
|
||||||
if (!hasMoreToLoadLive(direction) && hasReachedEndLive(direction)) {
|
if (!canPaginate(direction)) {
|
||||||
return@post
|
return@post
|
||||||
}
|
}
|
||||||
|
Timber.v("Paginate $direction of $count items")
|
||||||
val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex
|
val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex
|
||||||
val builtCountItems = insertFromLiveResults(startDisplayIndex, direction, count.toLong())
|
paginateInternal(startDisplayIndex, direction, count)
|
||||||
if (builtCountItems < count) {
|
|
||||||
val limit = count - builtCountItems
|
|
||||||
val token = getTokenLive(direction) ?: return@post
|
|
||||||
helper.runIfNotRunning(direction.toRequestType()) { executePaginationTask(it, token, direction, limit) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
if (isStarted.compareAndSet(false, true)) {
|
if (isStarted.compareAndSet(false, true)) {
|
||||||
val handlerThread = HandlerThread(THREAD_NAME)
|
Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId")
|
||||||
|
val handlerThread = HandlerThread(THREAD_NAME + hashCode())
|
||||||
handlerThread.start()
|
handlerThread.start()
|
||||||
val handler = Handler(handlerThread.looper)
|
val handler = Handler(handlerThread.looper)
|
||||||
this.handlerThread.set(handlerThread)
|
this.backgroundHandlerThread.set(handlerThread)
|
||||||
this.handler.set(handler)
|
this.backgroundHandler.set(handler)
|
||||||
handler.post {
|
handler.post {
|
||||||
val realm = Realm.getInstance(realmConfiguration)
|
val realm = Realm.getInstance(realmConfiguration)
|
||||||
this.realm.set(realm)
|
backgroundRealm.set(realm)
|
||||||
liveEvents = buildEventQuery(realm).findAllAsync()
|
clearUnlinkedEvents(realm)
|
||||||
liveEvents.addChangeListener(eventsChangeListener)
|
isReady.set(true)
|
||||||
|
liveEvents = buildEventQuery(realm)
|
||||||
|
.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||||
|
.findAll()
|
||||||
|
.also { it.addChangeListener(eventsChangeListener) }
|
||||||
|
handleInitialLoad()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
if (isStarted.compareAndSet(true, false)) {
|
if (isStarted.compareAndSet(true, false)) {
|
||||||
handler.get()?.post {
|
Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId")
|
||||||
|
backgroundHandler.get()?.post {
|
||||||
cancelableBag.cancel()
|
cancelableBag.cancel()
|
||||||
liveEvents.removeAllChangeListeners()
|
liveEvents.removeAllChangeListeners()
|
||||||
realm.getAndSet(null)?.close()
|
backgroundRealm.getAndSet(null).also {
|
||||||
handler.set(null)
|
it.close()
|
||||||
handlerThread.getAndSet(null)?.quit()
|
}
|
||||||
|
backgroundHandler.set(null)
|
||||||
|
backgroundHandlerThread.getAndSet(null)?.quit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun snapshot(): List<TimelineEvent> = synchronized(builtEvents) {
|
|
||||||
return builtEvents.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun size(): Int = synchronized(builtEvents) {
|
|
||||||
return builtEvents.size
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hasReachedEnd(direction: Timeline.Direction): Boolean {
|
|
||||||
return handler.get()?.postAndWait {
|
|
||||||
hasReachedEndLive(direction)
|
|
||||||
} ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hasMoreToLoad(direction: Timeline.Direction): Boolean {
|
override fun hasMoreToLoad(direction: Timeline.Direction): Boolean {
|
||||||
return handler.get()?.postAndWait {
|
return hasMoreInCache(direction) || !hasReachedEnd(direction)
|
||||||
hasMoreToLoadLive(direction)
|
}
|
||||||
} ?: false
|
|
||||||
|
// Private methods *****************************************************************************
|
||||||
|
|
||||||
|
private fun hasMoreInCache(direction: Timeline.Direction): Boolean {
|
||||||
|
val localRealm = Realm.getInstance(realmConfiguration)
|
||||||
|
val eventEntity = buildEventQuery(localRealm).findFirst(direction) ?: return false
|
||||||
|
val hasMoreInCache = if (direction == Timeline.Direction.FORWARDS) {
|
||||||
|
val firstEvent = builtEvents.firstOrNull() ?: return true
|
||||||
|
firstEvent.displayIndex < eventEntity.displayIndex
|
||||||
|
} else {
|
||||||
|
val lastEvent = builtEvents.lastOrNull() ?: return true
|
||||||
|
lastEvent.displayIndex > eventEntity.displayIndex
|
||||||
|
}
|
||||||
|
localRealm.close()
|
||||||
|
return hasMoreInCache
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasReachedEnd(direction: Timeline.Direction): Boolean {
|
||||||
|
val localRealm = Realm.getInstance(realmConfiguration)
|
||||||
|
val currentChunk = findCurrentChunk(localRealm) ?: return false
|
||||||
|
val hasReachedEnd = if (direction == Timeline.Direction.FORWARDS) {
|
||||||
|
currentChunk.isLastForward
|
||||||
|
} else {
|
||||||
|
val eventEntity = buildEventQuery(localRealm).findFirst(direction)
|
||||||
|
currentChunk.isLastBackward || eventEntity?.type == EventType.STATE_ROOM_CREATE
|
||||||
|
}
|
||||||
|
localRealm.close()
|
||||||
|
return hasReachedEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This has to be called on TimelineThread as it access realm live results
|
||||||
|
*/
|
||||||
|
private fun paginateInternal(startDisplayIndex: Int,
|
||||||
|
direction: Timeline.Direction,
|
||||||
|
count: Int) {
|
||||||
|
updatePaginationState(direction) { it.copy(requestedCount = count, isPaginating = true) }
|
||||||
|
val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong())
|
||||||
|
if (builtCount < count && !hasReachedEnd(direction)) {
|
||||||
|
val newRequestedCount = count - builtCount
|
||||||
|
updatePaginationState(direction) { it.copy(requestedCount = newRequestedCount) }
|
||||||
|
val fetchingCount = Math.max(MIN_FETCHING_COUNT, newRequestedCount)
|
||||||
|
executePaginationTask(direction, fetchingCount)
|
||||||
|
} else {
|
||||||
|
updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun snapshot(): List<TimelineEvent> {
|
||||||
|
return builtEvents.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun canPaginate(direction: Timeline.Direction): Boolean {
|
||||||
|
return isReady.get() && !getPaginationState(direction).isPaginating && hasMoreToLoad(direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPaginationState(direction: Timeline.Direction): PaginationState {
|
||||||
|
return when (direction) {
|
||||||
|
Timeline.Direction.FORWARDS -> forwardsPaginationState.get()
|
||||||
|
Timeline.Direction.BACKWARDS -> backwardsPaginationState.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePaginationState(direction: Timeline.Direction, update: (PaginationState) -> PaginationState) {
|
||||||
|
val stateReference = when (direction) {
|
||||||
|
Timeline.Direction.FORWARDS -> forwardsPaginationState
|
||||||
|
Timeline.Direction.BACKWARDS -> backwardsPaginationState
|
||||||
|
}
|
||||||
|
val currentValue = stateReference.get()
|
||||||
|
val newValue = update(currentValue)
|
||||||
|
stateReference.set(newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This has to be called on TimelineThread as it access realm live results
|
* This has to be called on TimelineThread as it access realm live results
|
||||||
*/
|
*/
|
||||||
private fun handleInitialLoad() = synchronized(builtEvents) {
|
private fun handleInitialLoad() {
|
||||||
|
var shouldFetchInitialEvent = false
|
||||||
val initialDisplayIndex = if (isLive) {
|
val initialDisplayIndex = if (isLive) {
|
||||||
liveEvents.firstOrNull()?.displayIndex
|
liveEvents.firstOrNull()?.displayIndex
|
||||||
} else {
|
} else {
|
||||||
liveEvents.where().equalTo(EventEntityFields.EVENT_ID, initialEventId).findFirst()?.displayIndex
|
val initialEvent = liveEvents.where().equalTo(EventEntityFields.EVENT_ID, initialEventId).findFirst()
|
||||||
} ?: 0
|
shouldFetchInitialEvent = initialEvent == null
|
||||||
|
initialEvent?.displayIndex
|
||||||
|
} ?: DISPLAY_INDEX_UNKNOWN
|
||||||
|
|
||||||
prevDisplayIndex = initialDisplayIndex
|
prevDisplayIndex = initialDisplayIndex
|
||||||
nextDisplayIndex = initialDisplayIndex
|
nextDisplayIndex = initialDisplayIndex
|
||||||
val count = Math.min(INITIAL_LOAD_SIZE, liveEvents.size).toLong()
|
if (initialEventId != null && shouldFetchInitialEvent) {
|
||||||
if (count == 0L) {
|
fetchEvent(initialEventId)
|
||||||
return@synchronized
|
|
||||||
}
|
|
||||||
if (isLive) {
|
|
||||||
insertFromLiveResults(initialDisplayIndex, Timeline.Direction.BACKWARDS, count)
|
|
||||||
} else {
|
} else {
|
||||||
val forwardCount = count / 2L
|
val count = Math.min(INITIAL_LOAD_SIZE, liveEvents.size)
|
||||||
val backwardCount = count - forwardCount
|
if (isLive) {
|
||||||
insertFromLiveResults(initialDisplayIndex, Timeline.Direction.BACKWARDS, backwardCount)
|
paginate(Timeline.Direction.BACKWARDS, count)
|
||||||
insertFromLiveResults(initialDisplayIndex, Timeline.Direction.BACKWARDS, forwardCount)
|
} else {
|
||||||
|
paginate(Timeline.Direction.FORWARDS, count / 2)
|
||||||
|
paginate(Timeline.Direction.BACKWARDS, count / 2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun executePaginationTask(requestCallback: PagingRequestHelper.Request.Callback,
|
/**
|
||||||
from: String,
|
* This has to be called on TimelineThread as it access realm live results
|
||||||
direction: Timeline.Direction,
|
*/
|
||||||
limit: Int) {
|
private fun executePaginationTask(direction: Timeline.Direction, limit: Int) {
|
||||||
|
val token = getTokenLive(direction) ?: return
|
||||||
val params = PaginationTask.Params(roomId = roomId,
|
val params = PaginationTask.Params(roomId = roomId,
|
||||||
from = from,
|
from = token,
|
||||||
direction = direction.toPaginationDirection(),
|
direction = direction.toPaginationDirection(),
|
||||||
limit = limit)
|
limit = limit)
|
||||||
|
|
||||||
|
Timber.v("Should fetch $limit items $direction")
|
||||||
paginationTask.configureWith(params)
|
paginationTask.configureWith(params)
|
||||||
.enableRetry()
|
.enableRetry()
|
||||||
.dispatchTo(object : MatrixCallback<TokenChunkEventPersistor.Result> {
|
.dispatchTo(object : MatrixCallback<TokenChunkEventPersistor.Result> {
|
||||||
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
|
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
|
||||||
requestCallback.recordSuccess()
|
if (data == TokenChunkEventPersistor.Result.SUCCESS) {
|
||||||
if (data == TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE) {
|
Timber.v("Success fetching $limit items $direction from pagination request")
|
||||||
paginate(direction, limit)
|
} else {
|
||||||
|
// Database won't be updated, so we force pagination request
|
||||||
|
backgroundHandler.get()?.post {
|
||||||
|
executePaginationTask(direction, limit)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
override fun onFailure(failure: Throwable) {
|
||||||
requestCallback.recordFailure(failure)
|
Timber.v("Failure fetching $limit items $direction from pagination request")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
@ -209,36 +296,12 @@ internal class DefaultTimeline(
|
|||||||
/**
|
/**
|
||||||
* This has to be called on TimelineThread as it access realm live results
|
* This has to be called on TimelineThread as it access realm live results
|
||||||
*/
|
*/
|
||||||
|
|
||||||
private fun getTokenLive(direction: Timeline.Direction): String? {
|
private fun getTokenLive(direction: Timeline.Direction): String? {
|
||||||
val chunkEntity = getLiveChunk() ?: return null
|
val chunkEntity = getLiveChunk() ?: return null
|
||||||
return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken
|
return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This has to be called on TimelineThread as it access realm live results
|
|
||||||
*/
|
|
||||||
private fun hasReachedEndLive(direction: Timeline.Direction): Boolean {
|
|
||||||
val liveChunk = getLiveChunk() ?: return false
|
|
||||||
return if (direction == Timeline.Direction.FORWARDS) {
|
|
||||||
liveChunk.isLastForward
|
|
||||||
} else {
|
|
||||||
liveChunk.isLastBackward || liveEvents.lastOrNull()?.type == EventType.STATE_ROOM_CREATE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This has to be called on TimelineThread as it access realm live results
|
|
||||||
*/
|
|
||||||
private fun hasMoreToLoadLive(direction: Timeline.Direction): Boolean {
|
|
||||||
if (liveEvents.isEmpty()) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return if (direction == Timeline.Direction.FORWARDS) {
|
|
||||||
builtEvents.firstOrNull()?.displayIndex != liveEvents.firstOrNull()?.displayIndex
|
|
||||||
} else {
|
|
||||||
builtEvents.lastOrNull()?.displayIndex != liveEvents.lastOrNull()?.displayIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This has to be called on TimelineThread as it access realm live results
|
* This has to be called on TimelineThread as it access realm live results
|
||||||
@ -251,11 +314,11 @@ internal class DefaultTimeline(
|
|||||||
* This has to be called on TimelineThread as it access realm live results
|
* This has to be called on TimelineThread as it access realm live results
|
||||||
* @return number of items who have been added
|
* @return number of items who have been added
|
||||||
*/
|
*/
|
||||||
private fun insertFromLiveResults(startDisplayIndex: Int,
|
private fun buildTimelineEvents(startDisplayIndex: Int,
|
||||||
direction: Timeline.Direction,
|
direction: Timeline.Direction,
|
||||||
count: Long): Int = synchronized(builtEvents) {
|
count: Long): Int {
|
||||||
if (count < 1) {
|
if (count < 1) {
|
||||||
throw java.lang.IllegalStateException("You should provide a count superior to 0")
|
return 0
|
||||||
}
|
}
|
||||||
val offsetResults = getOffsetResults(startDisplayIndex, direction, count)
|
val offsetResults = getOffsetResults(startDisplayIndex, direction, count)
|
||||||
if (offsetResults.isEmpty()) {
|
if (offsetResults.isEmpty()) {
|
||||||
@ -272,7 +335,9 @@ internal class DefaultTimeline(
|
|||||||
val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size
|
val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size
|
||||||
builtEvents.add(position, timelineEvent)
|
builtEvents.add(position, timelineEvent)
|
||||||
}
|
}
|
||||||
listener?.onUpdated(snapshot())
|
Timber.v("Built ${offsetResults.size} items from db")
|
||||||
|
val snapshot = snapshot()
|
||||||
|
mainHandler.post { listener?.onUpdated(snapshot) }
|
||||||
return offsetResults.size
|
return offsetResults.size
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,11 +357,15 @@ internal class DefaultTimeline(
|
|||||||
.sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
|
.sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
|
||||||
.greaterThanOrEqualTo(EventEntityFields.DISPLAY_INDEX, startDisplayIndex)
|
.greaterThanOrEqualTo(EventEntityFields.DISPLAY_INDEX, startDisplayIndex)
|
||||||
}
|
}
|
||||||
return offsetQuery.limit(count).findAll()
|
return offsetQuery
|
||||||
|
.filterAllowedTypes()
|
||||||
|
.limit(count)
|
||||||
|
.findAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun buildEventQuery(realm: Realm): RealmQuery<EventEntity> {
|
private fun buildEventQuery(realm: Realm): RealmQuery<EventEntity> {
|
||||||
val query = if (initialEventId == null) {
|
return if (initialEventId == null) {
|
||||||
EventEntity
|
EventEntity
|
||||||
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY)
|
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY)
|
||||||
.equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST_FORWARD}", true)
|
.equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST_FORWARD}", true)
|
||||||
@ -305,30 +374,56 @@ internal class DefaultTimeline(
|
|||||||
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH)
|
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH)
|
||||||
.`in`("${EventEntityFields.CHUNK}.${ChunkEntityFields.EVENTS.EVENT_ID}", arrayOf(initialEventId))
|
.`in`("${EventEntityFields.CHUNK}.${ChunkEntityFields.EVENTS.EVENT_ID}", arrayOf(initialEventId))
|
||||||
}
|
}
|
||||||
query.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
|
||||||
return query
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> Handler.postAndWait(runnable: () -> T): T {
|
private fun findCurrentChunk(realm: Realm): ChunkEntity? {
|
||||||
val lock = CountDownLatch(1)
|
return if (initialEventId == null) {
|
||||||
val atomicReference = AtomicReference<T>()
|
ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
|
||||||
post {
|
} else {
|
||||||
val result = runnable()
|
ChunkEntity.findIncludingEvent(realm, initialEventId)
|
||||||
atomicReference.set(result)
|
|
||||||
lock.countDown()
|
|
||||||
}
|
}
|
||||||
lock.await()
|
|
||||||
return atomicReference.get()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Timeline.Direction.toRequestType(): PagingRequestHelper.RequestType {
|
private fun clearUnlinkedEvents(realm: Realm) {
|
||||||
return if (this == Timeline.Direction.BACKWARDS) PagingRequestHelper.RequestType.BEFORE else PagingRequestHelper.RequestType.AFTER
|
realm.executeTransaction {
|
||||||
|
val unlinkedChunks = ChunkEntity
|
||||||
|
.where(it, roomId = roomId)
|
||||||
|
.equalTo(ChunkEntityFields.EVENTS.IS_UNLINKED, true)
|
||||||
|
.findAll()
|
||||||
|
unlinkedChunks.deleteAllFromRealm()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Todo : remove that
|
private fun fetchEvent(eventId: String) {
|
||||||
|
val params = GetContextOfEventTask.Params(roomId, eventId)
|
||||||
|
contextOfEventTask.configureWith(params).executeBy(taskExecutor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extension methods ***************************************************************************
|
||||||
|
|
||||||
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
|
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
|
||||||
return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
|
return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun RealmQuery<EventEntity>.findFirst(direction: Timeline.Direction): EventEntity? {
|
||||||
|
return if (direction == Timeline.Direction.FORWARDS) {
|
||||||
|
sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||||
|
} else {
|
||||||
|
sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
|
||||||
}
|
}
|
||||||
|
.filterAllowedTypes()
|
||||||
|
.findFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RealmQuery<EventEntity>.filterAllowedTypes(): RealmQuery<EventEntity> {
|
||||||
|
if (allowedTypes != null) {
|
||||||
|
`in`(EventEntityFields.TYPE, allowedTypes.toTypedArray())
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class PaginationState(
|
||||||
|
val isPaginating: Boolean = false,
|
||||||
|
val requestedCount: Int = 0
|
||||||
|
)
|
||||||
|
@ -19,68 +19,18 @@ package im.vector.matrix.android.internal.session.room.timeline
|
|||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
||||||
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.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
|
||||||
import im.vector.matrix.android.internal.util.PagingRequestHelper
|
|
||||||
import im.vector.matrix.android.internal.util.tryTransactionAsync
|
|
||||||
|
|
||||||
private const val EVENT_NOT_FOUND_INDEX = -1
|
|
||||||
|
|
||||||
internal class DefaultTimelineService(private val roomId: String,
|
internal class DefaultTimelineService(private val roomId: String,
|
||||||
private val monarchy: Monarchy,
|
private val monarchy: Monarchy,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val contextOfEventTask: GetContextOfEventTask,
|
private val contextOfEventTask: GetContextOfEventTask,
|
||||||
private val timelineEventFactory: TimelineEventFactory,
|
private val timelineEventFactory: TimelineEventFactory,
|
||||||
private val paginationTask: PaginationTask,
|
private val paginationTask: PaginationTask
|
||||||
private val helper: PagingRequestHelper
|
|
||||||
) : TimelineService {
|
) : TimelineService {
|
||||||
|
|
||||||
override fun createTimeline(eventId: String?): Timeline {
|
override fun createTimeline(eventId: String?, allowedTypes: List<String>?): Timeline {
|
||||||
return DefaultTimeline(roomId, eventId, monarchy.realmConfiguration, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask, helper)
|
return DefaultTimeline(roomId, eventId, monarchy.realmConfiguration, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask, allowedTypes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PRIVATE FUNCTIONS ***************************************************************************
|
|
||||||
|
|
||||||
private fun getInitialLoadKey(eventId: String?): Int {
|
|
||||||
var initialLoadKey = 0
|
|
||||||
if (eventId != null) {
|
|
||||||
val indexOfEvent = indexOfEvent(eventId)
|
|
||||||
if (indexOfEvent == EVENT_NOT_FOUND_INDEX) {
|
|
||||||
fetchEvent(eventId)
|
|
||||||
} else {
|
|
||||||
initialLoadKey = indexOfEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return initialLoadKey
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun fetchEvent(eventId: String) {
|
|
||||||
val params = GetContextOfEventTask.Params(roomId, eventId)
|
|
||||||
contextOfEventTask.configureWith(params).executeBy(taskExecutor)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearUnlinkedEvents() {
|
|
||||||
monarchy.tryTransactionAsync { realm ->
|
|
||||||
val unlinkedEvents = EventEntity
|
|
||||||
.where(realm, roomId = roomId)
|
|
||||||
.equalTo(EventEntityFields.IS_UNLINKED, true)
|
|
||||||
.findAll()
|
|
||||||
unlinkedEvents.deleteAllFromRealm()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun indexOfEvent(eventId: String): Int {
|
|
||||||
var displayIndex = EVENT_NOT_FOUND_INDEX
|
|
||||||
monarchy.doWithRealm {
|
|
||||||
displayIndex = EventEntity.where(it, eventId = eventId).findFirst()?.displayIndex
|
|
||||||
?: EVENT_NOT_FOUND_INDEX
|
|
||||||
}
|
|
||||||
return displayIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -1,110 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.session.room.timeline
|
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.paging.PagedList
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
|
||||||
import im.vector.matrix.android.internal.database.query.findIncludingEvent
|
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
|
||||||
import im.vector.matrix.android.internal.util.PagingRequestHelper
|
|
||||||
|
|
||||||
internal class TimelineBoundaryCallback(private val roomId: String,
|
|
||||||
private val taskExecutor: TaskExecutor,
|
|
||||||
private val paginationTask: PaginationTask,
|
|
||||||
private val monarchy: Monarchy,
|
|
||||||
private val helper: PagingRequestHelper
|
|
||||||
) : PagedList.BoundaryCallback<TimelineEvent>() {
|
|
||||||
|
|
||||||
var limit = 30
|
|
||||||
|
|
||||||
val status = object : LiveData<PagingRequestHelper.StatusReport>() {
|
|
||||||
|
|
||||||
init {
|
|
||||||
value = PagingRequestHelper.StatusReport.createDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
val listener = PagingRequestHelper.Listener { postValue(it) }
|
|
||||||
|
|
||||||
override fun onActive() {
|
|
||||||
helper.addListener(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInactive() {
|
|
||||||
helper.removeListener(listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onZeroItemsLoaded() {
|
|
||||||
// actually, it's not possible
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemAtEndLoaded(itemAtEnd: TimelineEvent) {
|
|
||||||
val token = itemAtEnd.root.eventId?.let { getToken(it, PaginationDirection.BACKWARDS) }
|
|
||||||
?: return
|
|
||||||
|
|
||||||
helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) {
|
|
||||||
executePaginationTask(it, token, PaginationDirection.BACKWARDS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemAtFrontLoaded(itemAtFront: TimelineEvent) {
|
|
||||||
val token = itemAtFront.root.eventId?.let { getToken(it, PaginationDirection.FORWARDS) }
|
|
||||||
?: return
|
|
||||||
|
|
||||||
helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE) {
|
|
||||||
executePaginationTask(it, token, PaginationDirection.FORWARDS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getToken(eventId: String, direction: PaginationDirection): String? {
|
|
||||||
var token: String? = null
|
|
||||||
monarchy.doWithRealm { realm ->
|
|
||||||
val chunkEntity = ChunkEntity.findIncludingEvent(realm, eventId)
|
|
||||||
token = if (direction == PaginationDirection.FORWARDS) chunkEntity?.nextToken else chunkEntity?.prevToken
|
|
||||||
}
|
|
||||||
return token
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun executePaginationTask(requestCallback: PagingRequestHelper.Request.Callback,
|
|
||||||
from: String,
|
|
||||||
direction: PaginationDirection) {
|
|
||||||
|
|
||||||
val params = PaginationTask.Params(roomId = roomId,
|
|
||||||
from = from,
|
|
||||||
direction = direction,
|
|
||||||
limit = limit)
|
|
||||||
|
|
||||||
paginationTask.configureWith(params)
|
|
||||||
.enableRetry()
|
|
||||||
.dispatchTo(object : MatrixCallback<TokenChunkEventPersistor.Result> {
|
|
||||||
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
|
|
||||||
requestCallback.recordSuccess()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
requestCallback.recordFailure(failure)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.executeBy(taskExecutor)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -18,12 +18,7 @@ package im.vector.matrix.android.internal.session.room.timeline
|
|||||||
|
|
||||||
import arrow.core.Try
|
import arrow.core.Try
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.internal.database.helper.addAll
|
import im.vector.matrix.android.internal.database.helper.*
|
||||||
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.ChunkEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
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.create
|
||||||
@ -32,6 +27,7 @@ import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
|
|||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
import im.vector.matrix.android.internal.util.tryTransactionSync
|
||||||
import io.realm.kotlin.createObject
|
import io.realm.kotlin.createObject
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
||||||
internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
|
internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
|
||||||
@ -47,6 +43,8 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
|
|||||||
|
|
||||||
return monarchy
|
return monarchy
|
||||||
.tryTransactionSync { realm ->
|
.tryTransactionSync { realm ->
|
||||||
|
Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")
|
||||||
|
|
||||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||||
?: realm.createObject(roomId)
|
?: realm.createObject(roomId)
|
||||||
|
|
||||||
@ -76,8 +74,8 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
|
|||||||
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
|
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
|
||||||
currentChunk.isLastBackward = true
|
currentChunk.isLastBackward = true
|
||||||
} else {
|
} else {
|
||||||
|
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken}-${currentChunk.prevToken}")
|
||||||
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
|
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
|
||||||
|
|
||||||
// Then we merge chunks if needed
|
// Then we merge chunks if needed
|
||||||
if (currentChunk != prevChunk && prevChunk != null) {
|
if (currentChunk != prevChunk && prevChunk != null) {
|
||||||
currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
|
currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
|
||||||
@ -111,6 +109,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
|
|||||||
otherChunk: ChunkEntity): ChunkEntity {
|
otherChunk: ChunkEntity): ChunkEntity {
|
||||||
|
|
||||||
// We always merge the bottom chunk into top chunk, so we are always merging backwards
|
// We always merge the bottom chunk into top chunk, so we are always merging backwards
|
||||||
|
Timber.v("Merge ${currentChunk.prevToken}-${currentChunk.nextToken} with ${otherChunk.prevToken}-${otherChunk.nextToken}")
|
||||||
return if (direction == PaginationDirection.BACKWARDS) {
|
return if (direction == PaginationDirection.BACKWARDS) {
|
||||||
currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS)
|
currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS)
|
||||||
roomEntity.deleteOnCascade(otherChunk)
|
roomEntity.deleteOnCascade(otherChunk)
|
||||||
|
@ -1,530 +0,0 @@
|
|||||||
|
|
||||||
/*
|
|
||||||
* Copyright 2019 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package im.vector.matrix.android.internal.util;
|
|
||||||
|
|
||||||
import androidx.annotation.AnyThread;
|
|
||||||
import androidx.annotation.GuardedBy;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.VisibleForTesting;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A helper class for {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}s and
|
|
||||||
* {@link androidx.paging.DataSource}s to help with tracking network requests.
|
|
||||||
* <p>
|
|
||||||
* It is designed to support 3 types of requests, {@link RequestType#INITIAL INITIAL},
|
|
||||||
* {@link RequestType#BEFORE BEFORE} and {@link RequestType#AFTER AFTER} and runs only 1 request
|
|
||||||
* for each of them via {@link #runIfNotRunning(RequestType, Request)}.
|
|
||||||
* <p>
|
|
||||||
* It tracks a {@link Status} and an {@code error} for each {@link RequestType}.
|
|
||||||
* <p>
|
|
||||||
* A sample usage of this class to limit requests looks like this:
|
|
||||||
* <pre>
|
|
||||||
* class PagingBoundaryCallback extends PagedList.BoundaryCallback<MyItem> {
|
|
||||||
* // TODO replace with an executor from your application
|
|
||||||
* Executor executor = Executors.newSingleThreadExecutor();
|
|
||||||
* PagingRequestHelper helper = new PagingRequestHelper(executor);
|
|
||||||
* // imaginary API service, using Retrofit
|
|
||||||
* MyApi api;
|
|
||||||
*
|
|
||||||
* {@literal @}Override
|
|
||||||
* public void onItemAtFrontLoaded({@literal @}NonNull MyItem itemAtFront) {
|
|
||||||
* helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE,
|
|
||||||
* helperCallback -> api.getTopBefore(itemAtFront.getName(), 10).enqueue(
|
|
||||||
* new Callback<ApiResponse>() {
|
|
||||||
* {@literal @}Override
|
|
||||||
* public void onResponse(Call<ApiResponse> call,
|
|
||||||
* Response<ApiResponse> response) {
|
|
||||||
* // TODO insert new records into database
|
|
||||||
* helperCallback.recordSuccess();
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* {@literal @}Override
|
|
||||||
* public void onFailure(Call<ApiResponse> call, Throwable t) {
|
|
||||||
* helperCallback.recordFailure(t);
|
|
||||||
* }
|
|
||||||
* }));
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* {@literal @}Override
|
|
||||||
* public void onItemAtEndLoaded({@literal @}NonNull MyItem itemAtEnd) {
|
|
||||||
* helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER,
|
|
||||||
* helperCallback -> api.getTopBefore(itemAtEnd.getName(), 10).enqueue(
|
|
||||||
* new Callback<ApiResponse>() {
|
|
||||||
* {@literal @}Override
|
|
||||||
* public void onResponse(Call<ApiResponse> call,
|
|
||||||
* Response<ApiResponse> response) {
|
|
||||||
* // TODO insert new records into database
|
|
||||||
* helperCallback.recordSuccess();
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* {@literal @}Override
|
|
||||||
* public void onFailure(Call<ApiResponse> call, Throwable t) {
|
|
||||||
* helperCallback.recordFailure(t);
|
|
||||||
* }
|
|
||||||
* }));
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* <p>
|
|
||||||
* The helper provides an API to observe combined request status, which can be reported back to the
|
|
||||||
* application based on your business rules.
|
|
||||||
* <pre>
|
|
||||||
* MutableLiveData<PagingRequestHelper.Status> combined = new MutableLiveData<>();
|
|
||||||
* helper.addListener(status -> {
|
|
||||||
* // merge multiple states per request type into one, or dispatch separately depending on
|
|
||||||
* // your application logic.
|
|
||||||
* if (status.hasRunning()) {
|
|
||||||
* combined.postValue(PagingRequestHelper.Status.RUNNING);
|
|
||||||
* } else if (status.hasError()) {
|
|
||||||
* // can also obtain the error via {@link StatusReport#getErrorFor(RequestType)}
|
|
||||||
* combined.postValue(PagingRequestHelper.Status.FAILED);
|
|
||||||
* } else {
|
|
||||||
* combined.postValue(PagingRequestHelper.Status.SUCCESS);
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
// THIS class is likely to be moved into the library in a future release. Feel free to copy it
|
|
||||||
// from this sample.
|
|
||||||
public class PagingRequestHelper {
|
|
||||||
private final Object mLock = new Object();
|
|
||||||
private final Executor mRetryService;
|
|
||||||
@GuardedBy("mLock")
|
|
||||||
private final RequestQueue[] mRequestQueues = new RequestQueue[]
|
|
||||||
{new RequestQueue(RequestType.INITIAL),
|
|
||||||
new RequestQueue(RequestType.BEFORE),
|
|
||||||
new RequestQueue(RequestType.AFTER)};
|
|
||||||
@NonNull
|
|
||||||
final CopyOnWriteArrayList<Listener> mListeners = new CopyOnWriteArrayList<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new PagingRequestHelper with the given {@link Executor} which is used to run
|
|
||||||
* retry actions.
|
|
||||||
*
|
|
||||||
* @param retryService The {@link Executor} that can run the retry actions.
|
|
||||||
*/
|
|
||||||
public PagingRequestHelper(@NonNull Executor retryService) {
|
|
||||||
mRetryService = retryService;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new listener that will be notified when any request changes {@link Status state}.
|
|
||||||
*
|
|
||||||
* @param listener The listener that will be notified each time a request's status changes.
|
|
||||||
* @return True if it is added, false otherwise (e.g. it already exists in the list).
|
|
||||||
*/
|
|
||||||
@AnyThread
|
|
||||||
public boolean addListener(@NonNull Listener listener) {
|
|
||||||
return mListeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the given listener from the listeners list.
|
|
||||||
*
|
|
||||||
* @param listener The listener that will be removed.
|
|
||||||
* @return True if the listener is removed, false otherwise (e.g. it never existed)
|
|
||||||
*/
|
|
||||||
public boolean removeListener(@NonNull Listener listener) {
|
|
||||||
return mListeners.remove(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs the given {@link Request} if no other requests in the given request type is already
|
|
||||||
* running.
|
|
||||||
* <p>
|
|
||||||
* If run, the request will be run in the current thread.
|
|
||||||
*
|
|
||||||
* @param type The type of the request.
|
|
||||||
* @param request The request to run.
|
|
||||||
* @return True if the request is run, false otherwise.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
@AnyThread
|
|
||||||
public boolean runIfNotRunning(@NonNull RequestType type, @NonNull Request request) {
|
|
||||||
boolean hasListeners = !mListeners.isEmpty();
|
|
||||||
StatusReport report = null;
|
|
||||||
synchronized (mLock) {
|
|
||||||
RequestQueue queue = mRequestQueues[type.ordinal()];
|
|
||||||
if (queue.mRunning != null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
queue.mRunning = request;
|
|
||||||
queue.mStatus = Status.RUNNING;
|
|
||||||
queue.mFailed = null;
|
|
||||||
queue.mLastError = null;
|
|
||||||
if (hasListeners) {
|
|
||||||
report = prepareStatusReportLocked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (report != null) {
|
|
||||||
dispatchReport(report);
|
|
||||||
}
|
|
||||||
final RequestWrapper wrapper = new RequestWrapper(request, this, type);
|
|
||||||
wrapper.run();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GuardedBy("mLock")
|
|
||||||
private StatusReport prepareStatusReportLocked() {
|
|
||||||
Throwable[] errors = new Throwable[]{
|
|
||||||
mRequestQueues[0].mLastError,
|
|
||||||
mRequestQueues[1].mLastError,
|
|
||||||
mRequestQueues[2].mLastError
|
|
||||||
};
|
|
||||||
return new StatusReport(
|
|
||||||
getStatusForLocked(RequestType.INITIAL),
|
|
||||||
getStatusForLocked(RequestType.BEFORE),
|
|
||||||
getStatusForLocked(RequestType.AFTER),
|
|
||||||
errors
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GuardedBy("mLock")
|
|
||||||
private Status getStatusForLocked(RequestType type) {
|
|
||||||
return mRequestQueues[type.ordinal()].mStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
@AnyThread
|
|
||||||
@VisibleForTesting
|
|
||||||
void recordResult(@NonNull RequestWrapper wrapper, @Nullable Throwable throwable) {
|
|
||||||
StatusReport report = null;
|
|
||||||
final boolean success = throwable == null;
|
|
||||||
boolean hasListeners = !mListeners.isEmpty();
|
|
||||||
synchronized (mLock) {
|
|
||||||
RequestQueue queue = mRequestQueues[wrapper.mType.ordinal()];
|
|
||||||
queue.mRunning = null;
|
|
||||||
queue.mLastError = throwable;
|
|
||||||
if (success) {
|
|
||||||
queue.mFailed = null;
|
|
||||||
queue.mStatus = Status.SUCCESS;
|
|
||||||
} else {
|
|
||||||
queue.mFailed = wrapper;
|
|
||||||
queue.mStatus = Status.FAILED;
|
|
||||||
}
|
|
||||||
if (hasListeners) {
|
|
||||||
report = prepareStatusReportLocked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (report != null) {
|
|
||||||
dispatchReport(report);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void dispatchReport(StatusReport report) {
|
|
||||||
for (Listener listener : mListeners) {
|
|
||||||
listener.onStatusChange(report);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retries all failed requests.
|
|
||||||
*
|
|
||||||
* @return True if any request is retried, false otherwise.
|
|
||||||
*/
|
|
||||||
public boolean retryAllFailed() {
|
|
||||||
final RequestWrapper[] toBeRetried = new RequestWrapper[RequestType.values().length];
|
|
||||||
boolean retried = false;
|
|
||||||
synchronized (mLock) {
|
|
||||||
for (int i = 0; i < RequestType.values().length; i++) {
|
|
||||||
toBeRetried[i] = mRequestQueues[i].mFailed;
|
|
||||||
mRequestQueues[i].mFailed = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (RequestWrapper failed : toBeRetried) {
|
|
||||||
if (failed != null) {
|
|
||||||
failed.retry(mRetryService);
|
|
||||||
retried = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return retried;
|
|
||||||
}
|
|
||||||
|
|
||||||
static class RequestWrapper implements Runnable {
|
|
||||||
@NonNull
|
|
||||||
final Request mRequest;
|
|
||||||
@NonNull
|
|
||||||
final PagingRequestHelper mHelper;
|
|
||||||
@NonNull
|
|
||||||
final RequestType mType;
|
|
||||||
|
|
||||||
RequestWrapper(@NonNull Request request, @NonNull PagingRequestHelper helper,
|
|
||||||
@NonNull RequestType type) {
|
|
||||||
mRequest = request;
|
|
||||||
mHelper = helper;
|
|
||||||
mType = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
mRequest.run(new Request.Callback(this, mHelper));
|
|
||||||
}
|
|
||||||
|
|
||||||
void retry(Executor service) {
|
|
||||||
service.execute(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
mHelper.runIfNotRunning(mType, mRequest);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runner class that runs a request tracked by the {@link PagingRequestHelper}.
|
|
||||||
* <p>
|
|
||||||
* When a request is invoked, it must call one of {@link Callback#recordFailure(Throwable)}
|
|
||||||
* or {@link Callback#recordSuccess()} once and only once. This call
|
|
||||||
* can be made any time. Until that method call is made, {@link PagingRequestHelper} will
|
|
||||||
* consider the request is running.
|
|
||||||
*/
|
|
||||||
@FunctionalInterface
|
|
||||||
public interface Request {
|
|
||||||
/**
|
|
||||||
* Should run the request and call the given {@link Callback} with the result of the
|
|
||||||
* request.
|
|
||||||
*
|
|
||||||
* @param callback The callback that should be invoked with the result.
|
|
||||||
*/
|
|
||||||
void run(Callback callback);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback class provided to the {@link #run(Callback)} method to report the result.
|
|
||||||
*/
|
|
||||||
class Callback {
|
|
||||||
private final AtomicBoolean mCalled = new AtomicBoolean();
|
|
||||||
private final RequestWrapper mWrapper;
|
|
||||||
private final PagingRequestHelper mHelper;
|
|
||||||
|
|
||||||
Callback(RequestWrapper wrapper, PagingRequestHelper helper) {
|
|
||||||
mWrapper = wrapper;
|
|
||||||
mHelper = helper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call this method when the request succeeds and new data is fetched.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public final void recordSuccess() {
|
|
||||||
if (mCalled.compareAndSet(false, true)) {
|
|
||||||
mHelper.recordResult(mWrapper, null);
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"already called recordSuccess or recordFailure");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call this method with the failure message and the request can be retried via
|
|
||||||
* {@link #retryAllFailed()}.
|
|
||||||
*
|
|
||||||
* @param throwable The error that occured while carrying out the request.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public final void recordFailure(@NonNull Throwable throwable) {
|
|
||||||
//noinspection ConstantConditions
|
|
||||||
if (throwable == null) {
|
|
||||||
throw new IllegalArgumentException("You must provide a throwable describing"
|
|
||||||
+ " the error to record the failure");
|
|
||||||
}
|
|
||||||
if (mCalled.compareAndSet(false, true)) {
|
|
||||||
mHelper.recordResult(mWrapper, throwable);
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"already called recordSuccess or recordFailure");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data class that holds the information about the current status of the ongoing requests
|
|
||||||
* using this helper.
|
|
||||||
*/
|
|
||||||
public static final class StatusReport {
|
|
||||||
/**
|
|
||||||
* Status of the latest request that were submitted with {@link RequestType#INITIAL}.
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public final Status initial;
|
|
||||||
/**
|
|
||||||
* Status of the latest request that were submitted with {@link RequestType#BEFORE}.
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public final Status before;
|
|
||||||
/**
|
|
||||||
* Status of the latest request that were submitted with {@link RequestType#AFTER}.
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public final Status after;
|
|
||||||
@NonNull
|
|
||||||
private final Throwable[] mErrors;
|
|
||||||
|
|
||||||
public static StatusReport createDefault() {
|
|
||||||
final Throwable[] errors = {};
|
|
||||||
return new StatusReport(Status.SUCCESS, Status.SUCCESS, Status.SUCCESS, errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
StatusReport(@NonNull Status initial, @NonNull Status before, @NonNull Status after,
|
|
||||||
@NonNull Throwable[] errors) {
|
|
||||||
this.initial = initial;
|
|
||||||
this.before = before;
|
|
||||||
this.after = after;
|
|
||||||
this.mErrors = errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience method to check if there are any running requests.
|
|
||||||
*
|
|
||||||
* @return True if there are any running requests, false otherwise.
|
|
||||||
*/
|
|
||||||
public boolean hasRunning() {
|
|
||||||
return initial == Status.RUNNING
|
|
||||||
|| before == Status.RUNNING
|
|
||||||
|| after == Status.RUNNING;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience method to check if there are any requests that resulted in an error.
|
|
||||||
*
|
|
||||||
* @return True if there are any requests that finished with error, false otherwise.
|
|
||||||
*/
|
|
||||||
public boolean hasError() {
|
|
||||||
return initial == Status.FAILED
|
|
||||||
|| before == Status.FAILED
|
|
||||||
|| after == Status.FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the error for the given request type.
|
|
||||||
*
|
|
||||||
* @param type The request type for which the error should be returned.
|
|
||||||
* @return The {@link Throwable} returned by the failing request with the given type or
|
|
||||||
* {@code null} if the request for the given type did not fail.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public Throwable getErrorFor(@NonNull RequestType type) {
|
|
||||||
return mErrors[type.ordinal()];
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "StatusReport{"
|
|
||||||
+ "initial=" + initial
|
|
||||||
+ ", before=" + before
|
|
||||||
+ ", after=" + after
|
|
||||||
+ ", mErrors=" + Arrays.toString(mErrors)
|
|
||||||
+ '}';
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
StatusReport that = (StatusReport) o;
|
|
||||||
if (initial != that.initial) return false;
|
|
||||||
if (before != that.before) return false;
|
|
||||||
if (after != that.after) return false;
|
|
||||||
// Probably incorrect - comparing Object[] arrays with Arrays.equals
|
|
||||||
return Arrays.equals(mErrors, that.mErrors);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
int result = initial.hashCode();
|
|
||||||
result = 31 * result + before.hashCode();
|
|
||||||
result = 31 * result + after.hashCode();
|
|
||||||
result = 31 * result + Arrays.hashCode(mErrors);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener interface to get notified by request status changes.
|
|
||||||
*/
|
|
||||||
public interface Listener {
|
|
||||||
/**
|
|
||||||
* Called when the status for any of the requests has changed.
|
|
||||||
*
|
|
||||||
* @param report The current status report that has all the information about the requests.
|
|
||||||
*/
|
|
||||||
void onStatusChange(@NonNull StatusReport report);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the status of a Request for each {@link RequestType}.
|
|
||||||
*/
|
|
||||||
public enum Status {
|
|
||||||
/**
|
|
||||||
* There is current a running request.
|
|
||||||
*/
|
|
||||||
RUNNING,
|
|
||||||
/**
|
|
||||||
* The last request has succeeded or no such requests have ever been run.
|
|
||||||
*/
|
|
||||||
SUCCESS,
|
|
||||||
/**
|
|
||||||
* The last request has failed.
|
|
||||||
*/
|
|
||||||
FAILED
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Available request types.
|
|
||||||
*/
|
|
||||||
public enum RequestType {
|
|
||||||
/**
|
|
||||||
* Corresponds to an initial request made to a {@link androidx.paging.DataSource} or the empty state for
|
|
||||||
* a {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}.
|
|
||||||
*/
|
|
||||||
INITIAL,
|
|
||||||
/**
|
|
||||||
* Corresponds to the {@code loadBefore} calls in {@link androidx.paging.DataSource} or
|
|
||||||
* {@code onItemAtFrontLoaded} in
|
|
||||||
* {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}.
|
|
||||||
*/
|
|
||||||
BEFORE,
|
|
||||||
/**
|
|
||||||
* Corresponds to the {@code loadAfter} calls in {@link androidx.paging.DataSource} or
|
|
||||||
* {@code onItemAtEndLoaded} in
|
|
||||||
* {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}.
|
|
||||||
*/
|
|
||||||
AFTER
|
|
||||||
}
|
|
||||||
|
|
||||||
class RequestQueue {
|
|
||||||
@NonNull
|
|
||||||
final RequestType mRequestType;
|
|
||||||
@Nullable
|
|
||||||
RequestWrapper mFailed;
|
|
||||||
@Nullable
|
|
||||||
Request mRunning;
|
|
||||||
@Nullable
|
|
||||||
Throwable mLastError;
|
|
||||||
@NonNull
|
|
||||||
Status mStatus = Status.SUCCESS;
|
|
||||||
|
|
||||||
RequestQueue(@NonNull RequestType requestType) {
|
|
||||||
mRequestType = requestType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user