Load contents by chunks instead of loading all of them at once

This commit is contained in:
Sylvain Berfini 2025-09-16 17:15:56 +02:00
parent 31e15ddfca
commit ae7a3c5bce
7 changed files with 200 additions and 41 deletions

View file

@ -33,6 +33,7 @@ Group changes to describe their impact on the project, as follows:
### Changed
- Hide SIP address/phone number picker dialog if contact has exactly one SIP address matching both the app default domain & the currently selected account domain
- Improved UI on tablets with screen sw600dp and higher, will look more like our desktop app
- Now loading media/documents contents in conversation by chunks (instead of all of them at once)
- Simplified audio device name in settings
- Reworked some settings (moved calls related ones from advanced settings to advanced calls settings)
- Increased shared media preview size in chat

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
* Copyright (c) 2010-2025 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
@ -21,13 +21,12 @@ package org.linphone.ui.main.chat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.linphone.core.tools.Log
internal abstract class ConversationScrollListener(private val mLayoutManager: LinearLayoutManager) :
internal abstract class RecyclerViewScrollListener(private val layoutManager: LinearLayoutManager, private val visibleThreshold: Int, private val scrollingTopToBottom: Boolean) :
RecyclerView.OnScrollListener() {
companion object {
// The minimum amount of items to have below your current scroll position
// before loading more.
private const val VISIBLE_THRESHOLD = 5
private const val TAG = "[RecyclerView Scroll Listener]"
}
// The total number of items in the data set after the last load
@ -40,9 +39,9 @@ internal abstract class ConversationScrollListener(private val mLayoutManager: L
// 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 totalItemCount = mLayoutManager.itemCount
val firstVisibleItemPosition: Int = mLayoutManager.findFirstVisibleItemPosition()
val lastVisibleItemPosition: Int = mLayoutManager.findLastVisibleItemPosition()
val totalItemCount = layoutManager.itemCount
val firstVisibleItemPosition: Int = layoutManager.findFirstVisibleItemPosition()
val lastVisibleItemPosition: Int = layoutManager.findLastVisibleItemPosition()
// 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
@ -64,21 +63,34 @@ internal abstract class ConversationScrollListener(private val mLayoutManager: L
val userHasScrolledUp = lastVisibleItemPosition != totalItemCount - 1
if (userHasScrolledUp) {
onScrolledUp()
Log.d("$TAG Scrolled up")
} else {
onScrolledToEnd()
Log.d("$TAG Scrolled to end")
}
// If it isnt currently loading, we check to see if we have breached
// the mVisibleThreshold and need to reload more data.
// 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 < VISIBLE_THRESHOLD &&
firstVisibleItemPosition >= 0 &&
lastVisibleItemPosition < totalItemCount - VISIBLE_THRESHOLD
) {
onLoadMore(totalItemCount)
loading = true
if (!loading) {
if (scrollingTopToBottom) {
if (lastVisibleItemPosition >= totalItemCount - visibleThreshold) {
Log.d(
"$TAG Last visible item position [$lastVisibleItemPosition] reached [${totalItemCount - visibleThreshold}], loading more (current total items is [$totalItemCount])"
)
loading = true
onLoadMore(totalItemCount)
}
} else {
if (firstVisibleItemPosition < visibleThreshold) {
Log.d(
"$TAG First visible item position [$firstVisibleItemPosition] < visibleThreshold [$visibleThreshold], loading more (current total items is [$totalItemCount])"
)
loading = true
onLoadMore(totalItemCount)
}
}
}
}

View file

@ -33,6 +33,7 @@ import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import org.linphone.core.tools.Log
import org.linphone.databinding.ChatDocumentsFragmentBinding
import org.linphone.ui.main.chat.RecyclerViewScrollListener
import org.linphone.ui.main.chat.adapter.ConversationsFilesAdapter
import org.linphone.ui.main.chat.model.FileModel
import org.linphone.ui.main.chat.viewmodel.ConversationDocumentsListViewModel
@ -57,6 +58,8 @@ class ConversationDocumentsListFragment : SlidingPaneChildFragment() {
private val args: ConversationMediaListFragmentArgs by navArgs()
private lateinit var scrollListener: RecyclerViewScrollListener
override fun goBack(): Boolean {
try {
return findNavController().popBackStack()
@ -130,6 +133,40 @@ class ConversationDocumentsListFragment : SlidingPaneChildFragment() {
goToFileViewer(model)
}
}
scrollListener = object : RecyclerViewScrollListener(layoutManager, 4, true) {
@UiThread
override fun onLoadMore(totalItemsCount: Int) {
Log.i("$TAG Asking for more data to display, currently displayed items count is [$totalItemsCount]")
viewModel.loadMoreData(totalItemsCount)
}
@UiThread
override fun onScrolledUp() {
}
@UiThread
override fun onScrolledToEnd() {
}
}
}
override fun onResume() {
super.onResume()
if (::scrollListener.isInitialized) {
binding.documentsList.addOnScrollListener(scrollListener)
}
}
override fun onPause() {
super.onPause()
if (::scrollListener.isInitialized) {
binding.documentsList.removeOnScrollListener(scrollListener)
}
}
private fun goToFileViewer(fileModel: FileModel) {

View file

@ -69,7 +69,7 @@ import org.linphone.core.tools.Log
import org.linphone.databinding.ChatConversationFragmentBinding
import org.linphone.databinding.ChatConversationPopupMenuBinding
import org.linphone.ui.GenericActivity
import org.linphone.ui.main.chat.ConversationScrollListener
import org.linphone.ui.main.chat.RecyclerViewScrollListener
import org.linphone.ui.main.chat.adapter.ConversationEventAdapter
import org.linphone.ui.main.chat.adapter.MessageBottomSheetAdapter
import org.linphone.ui.main.chat.model.FileModel
@ -298,7 +298,7 @@ open class ConversationFragment : SlidingPaneChildFragment() {
}
}
private lateinit var scrollListener: ConversationScrollListener
private lateinit var scrollListener: RecyclerViewScrollListener
private lateinit var headerItemDecoration: RecyclerViewHeaderDecoration
@ -1005,7 +1005,7 @@ open class ConversationFragment : SlidingPaneChildFragment() {
binding.sendArea.messageToSend.addTextChangedListener(textObserver)
scrollListener = object : ConversationScrollListener(layoutManager) {
scrollListener = object : RecyclerViewScrollListener(layoutManager, 5, false) {
@UiThread
override fun onLoadMore(totalItemsCount: Int) {
if (viewModel.searchInProgress.value == false) {

View file

@ -36,6 +36,7 @@ import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.ChatMediaFragmentBinding
import org.linphone.ui.GenericActivity
import org.linphone.ui.main.chat.RecyclerViewScrollListener
import org.linphone.ui.main.chat.adapter.ConversationsFilesAdapter
import org.linphone.ui.main.chat.model.FileModel
import org.linphone.ui.main.chat.viewmodel.ConversationMediaListViewModel
@ -58,6 +59,8 @@ class ConversationMediaListFragment : SlidingPaneChildFragment() {
private val args: ConversationMediaListFragmentArgs by navArgs()
private lateinit var scrollListener: RecyclerViewScrollListener
override fun goBack(): Boolean {
try {
return findNavController().popBackStack()
@ -159,6 +162,40 @@ class ConversationMediaListFragment : SlidingPaneChildFragment() {
goToFileViewer(model)
}
}
scrollListener = object : RecyclerViewScrollListener(layoutManager, spanCount, true) {
@UiThread
override fun onLoadMore(totalItemsCount: Int) {
Log.i("$TAG Asking for more data to display, currently displayed items count is [$totalItemsCount]")
viewModel.loadMoreData(totalItemsCount)
}
@UiThread
override fun onScrolledUp() {
}
@UiThread
override fun onScrolledToEnd() {
}
}
}
override fun onResume() {
super.onResume()
if (::scrollListener.isInitialized) {
binding.mediaList.addOnScrollListener(scrollListener)
}
}
override fun onPause() {
super.onPause()
if (::scrollListener.isInitialized) {
binding.mediaList.removeOnScrollListener(scrollListener)
}
}
private fun goToFileViewer(fileModel: FileModel) {

View file

@ -22,16 +22,21 @@ package org.linphone.ui.main.chat.viewmodel
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.Content
import org.linphone.core.tools.Log
import org.linphone.ui.main.chat.model.FileModel
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
import kotlin.math.min
class ConversationDocumentsListViewModel
@UiThread
constructor() : AbstractConversationViewModel() {
companion object {
private const val TAG = "[Conversation Documents List ViewModel]"
private const val CONTENTS_PER_PAGE = 20
}
val documentsList = MutableLiveData<List<FileModel>>()
@ -42,6 +47,8 @@ class ConversationDocumentsListViewModel
MutableLiveData<Event<FileModel>>()
}
private var totalDocumentsCount: Int = -1
@WorkerThread
override fun afterNotifyingChatRoomFound(sameOne: Boolean) {
loadDocumentsList()
@ -56,16 +63,48 @@ class ConversationDocumentsListViewModel
@WorkerThread
private fun loadDocumentsList() {
operationInProgress.postValue(true)
val list = arrayListOf<FileModel>()
Log.i(
"$TAG Loading document contents for conversation [${LinphoneUtils.getConversationId(
chatRoom
)}]"
)
val documents = chatRoom.documentContents
Log.i("$TAG [${documents.size}] documents have been fetched")
for (documentContent in documents) {
totalDocumentsCount = chatRoom.documentContentsSize
Log.i("$TAG Document contents size is [$totalDocumentsCount]")
val contentsToLoad = min(totalDocumentsCount, CONTENTS_PER_PAGE)
val contents = chatRoom.getDocumentContentsRange(0, contentsToLoad)
Log.i("$TAG [${contents.size}] documents have been fetched")
documentsList.postValue(getFileModelsListFromContents(contents))
operationInProgress.postValue(false)
}
@UiThread
fun loadMoreData(totalItemsCount: Int) {
coreContext.postOnCoreThread {
Log.i("$TAG Loading more data, current total is $totalItemsCount, max size is $totalDocumentsCount")
if (totalItemsCount < totalDocumentsCount) {
var upperBound: Int = totalItemsCount + CONTENTS_PER_PAGE
if (upperBound > totalDocumentsCount) {
upperBound = totalDocumentsCount
}
val contents = chatRoom.getDocumentContentsRange(totalItemsCount, upperBound)
Log.i("$TAG [${contents.size}] contents loaded, adding them to list")
val list = arrayListOf<FileModel>()
list.addAll(documentsList.value.orEmpty())
list.addAll(getFileModelsListFromContents(contents))
documentsList.postValue(list)
}
}
}
@WorkerThread
private fun getFileModelsListFromContents(contents: Array<Content>): ArrayList<FileModel> {
val list = arrayListOf<FileModel>()
for (documentContent in contents) {
val isEncrypted = documentContent.isFileEncrypted
val originalPath = documentContent.filePath.orEmpty()
val path = if (isEncrypted) {
@ -94,14 +133,11 @@ class ConversationDocumentsListViewModel
val model =
FileModel(path, name, size, timestamp, isEncrypted, originalPath, ephemeral) {
openDocumentEvent.postValue(Event(it))
}
openDocumentEvent.postValue(Event(it))
}
list.add(model)
}
}
Log.i("$TAG [${documents.size}] documents have been processed")
documentsList.postValue(list)
operationInProgress.postValue(false)
return list
}
}

View file

@ -22,16 +22,21 @@ package org.linphone.ui.main.chat.viewmodel
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.Content
import org.linphone.core.tools.Log
import org.linphone.ui.main.chat.model.FileModel
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
import kotlin.math.min
class ConversationMediaListViewModel
@UiThread
constructor() : AbstractConversationViewModel() {
companion object {
private const val TAG = "[Conversation Media List ViewModel]"
private const val CONTENTS_PER_PAGE = 50
}
val mediaList = MutableLiveData<List<FileModel>>()
@ -42,6 +47,8 @@ class ConversationMediaListViewModel
MutableLiveData<Event<FileModel>>()
}
private var totalMediaCount: Int = -1
@WorkerThread
override fun afterNotifyingChatRoomFound(sameOne: Boolean) {
loadMediaList()
@ -56,16 +63,48 @@ class ConversationMediaListViewModel
@WorkerThread
private fun loadMediaList() {
operationInProgress.postValue(true)
val list = arrayListOf<FileModel>()
Log.i(
"$TAG Loading media contents for conversation [${LinphoneUtils.getConversationId(
chatRoom
)}]"
)
val media = chatRoom.mediaContents
Log.i("$TAG [${media.size}] media have been fetched")
for (mediaContent in media) {
totalMediaCount = chatRoom.mediaContentsSize
Log.i("$TAG Media contents size is [$totalMediaCount]")
val contentsToLoad = min(totalMediaCount, CONTENTS_PER_PAGE)
val contents = chatRoom.getMediaContentsRange(0, contentsToLoad)
Log.i("$TAG [${contents.size}] media have been fetched")
mediaList.postValue(getFileModelsListFromContents(contents))
operationInProgress.postValue(false)
}
@UiThread
fun loadMoreData(totalItemsCount: Int) {
coreContext.postOnCoreThread {
Log.i("$TAG Loading more data, current total is $totalItemsCount, max size is $totalMediaCount")
if (totalItemsCount < totalMediaCount) {
var upperBound: Int = totalItemsCount + CONTENTS_PER_PAGE
if (upperBound > totalMediaCount) {
upperBound = totalMediaCount
}
val contents = chatRoom.getMediaContentsRange(totalItemsCount, upperBound)
Log.i("$TAG [${contents.size}] contents loaded, adding them to list")
val list = arrayListOf<FileModel>()
list.addAll(mediaList.value.orEmpty())
list.addAll(getFileModelsListFromContents(contents))
mediaList.postValue(list)
}
}
}
@WorkerThread
private fun getFileModelsListFromContents(contents: Array<Content>): ArrayList<FileModel> {
val list = arrayListOf<FileModel>()
for (mediaContent in contents) {
// Do not display voice recordings here, even if they are media file
if (mediaContent.isVoiceRecording) continue
@ -85,14 +124,11 @@ class ConversationMediaListViewModel
if (path.isNotEmpty() && name.isNotEmpty()) {
val model =
FileModel(path, name, size, timestamp, isEncrypted, originalPath, chatRoom.isEphemeralEnabled) {
openMediaEvent.postValue(Event(it))
}
openMediaEvent.postValue(Event(it))
}
list.add(model)
}
}
Log.i("$TAG [${media.size}] media have been processed")
mediaList.postValue(list)
operationInProgress.postValue(false)
return list
}
}