diff --git a/CHANGELOG.md b/CHANGELOG.md index bab4b102c..6accff7ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/app/src/main/java/org/linphone/ui/main/chat/ConversationScrollListener.kt b/app/src/main/java/org/linphone/ui/main/chat/RecyclerViewScrollListener.kt similarity index 65% rename from app/src/main/java/org/linphone/ui/main/chat/ConversationScrollListener.kt rename to app/src/main/java/org/linphone/ui/main/chat/RecyclerViewScrollListener.kt index edf0b6097..bc7a8f883 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/ConversationScrollListener.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/RecyclerViewScrollListener.kt @@ -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 isn’t 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) + } + } } } diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationDocumentsListFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationDocumentsListFragment.kt index d908c1f14..028383a8e 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationDocumentsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationDocumentsListFragment.kt @@ -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) { diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 4f68d71cd..8e06bd668 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -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) { diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt index 0f7c6a307..4a88e1930 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt @@ -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) { diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationDocumentsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationDocumentsListViewModel.kt index 82ceaab0b..7d85805ee 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationDocumentsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationDocumentsListViewModel.kt @@ -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>() @@ -42,6 +47,8 @@ class ConversationDocumentsListViewModel MutableLiveData>() } + 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() 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() + list.addAll(documentsList.value.orEmpty()) + list.addAll(getFileModelsListFromContents(contents)) + documentsList.postValue(list) + } + } + } + + @WorkerThread + private fun getFileModelsListFromContents(contents: Array): ArrayList { + val list = arrayListOf() + 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 } } diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationMediaListViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationMediaListViewModel.kt index bad0ea672..f9b211f0e 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationMediaListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationMediaListViewModel.kt @@ -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>() @@ -42,6 +47,8 @@ class ConversationMediaListViewModel MutableLiveData>() } + 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() 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() + list.addAll(mediaList.value.orEmpty()) + list.addAll(getFileModelsListFromContents(contents)) + mediaList.postValue(list) + } + } + } + + @WorkerThread + private fun getFileModelsListFromContents(contents: Array): ArrayList { + val list = arrayListOf() + 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 } }