From d9d7508292147b291ffdd952aa21f6ae5a346705 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 24 Oct 2023 10:22:50 +0200 Subject: [PATCH] Improved genericity related to contact / suggestion picker --- .../chat/fragment/ConversationInfoFragment.kt | 2 +- .../fragment/StartConversationFragment.kt | 42 +---- .../viewmodel/StartConversationViewModel.kt | 141 -------------- .../AddParticipantsFragment.kt} | 46 ++++- .../fragment/GenericAddressPickerFragment.kt | 69 ++++++- .../history/fragment/StartCallFragment.kt | 35 +--- .../history/viewmodel/StartCallViewModel.kt | 167 +---------------- .../AddParticipantsViewModel.kt} | 18 +- .../viewmodel/AddressSelectionViewModel.kt | 176 ++++++++++++++++++ ... => generic_add_participants_fragment.xml} | 60 +++--- .../main/res/layout/start_call_fragment.xml | 3 - .../main/res/layout/start_chat_fragment.xml | 24 +-- .../main/res/navigation/chat_nav_graph.xml | 12 +- app/src/main/res/values/strings.xml | 5 +- 14 files changed, 349 insertions(+), 451 deletions(-) rename app/src/main/java/org/linphone/ui/main/{chat/fragment/AddParticipantToConversationFragment.kt => fragment/AddParticipantsFragment.kt} (54%) rename app/src/main/java/org/linphone/ui/main/{chat/viewmodel/ConversationAddParticipantViewModel.kt => viewmodel/AddParticipantsViewModel.kt} (64%) rename app/src/main/res/layout/{chat_add_participant_fragment.xml => generic_add_participants_fragment.xml} (77%) diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt index 66d5b8caa..e952c2be3 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt @@ -128,7 +128,7 @@ class ConversationInfoFragment : GenericFragment() { } binding.setAddParticipantsClickListener { - val action = ConversationInfoFragmentDirections.actionConversationInfoFragmentToAddParticipantToConversationFragment() + val action = ConversationInfoFragmentDirections.actionConversationInfoFragmentToAddParticipantsFragment() findNavController().navigate(action) } } diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/StartConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/StartConversationFragment.kt index 5ef1bccac..23f64dea7 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/StartConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/StartConversationFragment.kt @@ -27,8 +27,6 @@ import androidx.annotation.UiThread import androidx.annotation.WorkerThread import androidx.core.view.doOnPreDraw import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.LinearLayoutManager -import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R import org.linphone.core.Address import org.linphone.core.Friend @@ -37,8 +35,6 @@ import org.linphone.databinding.StartChatFragmentBinding import org.linphone.ui.main.MainActivity import org.linphone.ui.main.chat.viewmodel.StartConversationViewModel import org.linphone.ui.main.fragment.GenericAddressPickerFragment -import org.linphone.ui.main.history.adapter.ContactsAndSuggestionsListAdapter -import org.linphone.ui.main.model.SelectedAddressModel import org.linphone.utils.Event @UiThread @@ -49,9 +45,7 @@ class StartConversationFragment : GenericAddressPickerFragment() { private lateinit var binding: StartChatFragmentBinding - private lateinit var viewModel: StartConversationViewModel - - private lateinit var adapter: ContactsAndSuggestionsListAdapter + override lateinit var viewModel: StartConversationViewModel override fun onCreateView( inflater: LayoutInflater, @@ -63,31 +57,22 @@ class StartConversationFragment : GenericAddressPickerFragment() { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + viewModel = ViewModelProvider(this)[StartConversationViewModel::class.java] + super.onViewCreated(view, savedInstanceState) postponeEnterTransition() binding.lifecycleOwner = viewLifecycleOwner - viewModel = ViewModelProvider(this)[StartConversationViewModel::class.java] binding.viewModel = viewModel binding.setBackClickListener { goBack() } - adapter = ContactsAndSuggestionsListAdapter(viewLifecycleOwner) - binding.contactsList.setHasFixedSize(true) - binding.contactsList.adapter = adapter + setupRecyclerView(binding.contactsList) - adapter.contactClickedEvent.observe(viewLifecycleOwner) { - it.consume { model -> - handleClickOnContactModel(model) - } - } - - binding.contactsList.layoutManager = LinearLayoutManager(requireContext()) - - viewModel.contactsList.observe( + viewModel.contactsAndSuggestionsList.observe( viewLifecycleOwner ) { Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items") @@ -118,11 +103,6 @@ class StartConversationFragment : GenericAddressPickerFragment() { } } - viewModel.searchFilter.observe(viewLifecycleOwner) { filter -> - val trimmed = filter.trim() - viewModel.applyFilter(trimmed) - } - sharedViewModel.defaultAccountChangedEvent.observe(viewLifecycleOwner) { // Do not consume it! viewModel.updateGroupChatButtonVisibility() @@ -130,15 +110,7 @@ class StartConversationFragment : GenericAddressPickerFragment() { } @WorkerThread - override fun onAddressSelected(address: Address, friend: Friend) { - if (viewModel.multipleSelectionMode.value == true) { - val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(address) - val model = SelectedAddressModel(address, avatarModel) { - viewModel.removeAddressModelFromSelection(it) - } - viewModel.addAddressModelToSelection(model) - } else { - viewModel.createOneToOneChatRoomWith(address) - } + override fun onSingleAddressSelected(address: Address, friend: Friend) { + viewModel.createOneToOneChatRoomWith(address) } } diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/StartConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/StartConversationViewModel.kt index b3f2cd9cd..c16c9954e 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/StartConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/StartConversationViewModel.kt @@ -23,20 +23,14 @@ import androidx.annotation.UiThread import androidx.annotation.WorkerThread import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData -import kotlin.collections.ArrayList import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R -import org.linphone.contacts.ContactsManager.ContactsListener import org.linphone.core.Address import org.linphone.core.ChatRoom import org.linphone.core.ChatRoomListenerStub import org.linphone.core.ChatRoomParams -import org.linphone.core.MagicSearch -import org.linphone.core.MagicSearchListenerStub -import org.linphone.core.SearchResult import org.linphone.core.tools.Log -import org.linphone.ui.main.history.model.ContactOrSuggestionModel import org.linphone.ui.main.model.isInSecureMode import org.linphone.ui.main.viewmodel.AddressSelectionViewModel import org.linphone.utils.AppUtils @@ -48,10 +42,6 @@ class StartConversationViewModel @UiThread constructor() : AddressSelectionViewM private const val TAG = "[Start Conversation ViewModel]" } - val searchFilter = MutableLiveData() - - val contactsList = MutableLiveData>() - val hideGroupChatButton = MutableLiveData() val subject = MutableLiveData() @@ -96,33 +86,6 @@ class StartConversationViewModel @UiThread constructor() : AddressSelectionViewM } } - private var currentFilter = "" - private var previousFilter = "NotSet" - private var limitSearchToLinphoneAccounts = true - - private lateinit var magicSearch: MagicSearch - - private val magicSearchListener = object : MagicSearchListenerStub() { - @WorkerThread - override fun onSearchResultsReceived(magicSearch: MagicSearch) { - Log.i("$TAG Magic search contacts available") - processMagicSearchResults(magicSearch.lastSearch) - } - } - - private val contactsListener = object : ContactsListener { - @WorkerThread - override fun onContactsLoaded() { - Log.i("$TAG Contacts have been (re)loaded, updating list") - applyFilter( - currentFilter, - if (limitSearchToLinphoneAccounts) corePreferences.defaultDomain else "", - MagicSearch.Source.Friends.toInt(), - MagicSearch.Aggregation.Friend - ) - } - } - init { groupChatRoomCreateButtonEnabled.postValue(false) groupChatRoomCreateButtonEnabled.addSource(selection) { @@ -137,32 +100,6 @@ class StartConversationViewModel @UiThread constructor() : AddressSelectionViewM } updateGroupChatButtonVisibility() - - coreContext.postOnCoreThread { core -> - val defaultAccount = core.defaultAccount - limitSearchToLinphoneAccounts = defaultAccount?.isInSecureMode() ?: false - - coreContext.contactsManager.addListener(contactsListener) - magicSearch = core.createMagicSearch() - magicSearch.limitedSearch = false - magicSearch.addListener(magicSearchListener) - } - - applyFilter(currentFilter) - } - - @UiThread - override fun onCleared() { - coreContext.postOnCoreThread { - magicSearch.removeListener(magicSearchListener) - coreContext.contactsManager.removeListener(contactsListener) - } - super.onCleared() - } - - @UiThread - fun clearFilter() { - searchFilter.value = "" } @UiThread @@ -341,82 +278,4 @@ class StartConversationViewModel @UiThread constructor() : AddressSelectionViewM hideGroupChatButton.postValue(hideGroupChat) } } - - @WorkerThread - fun processMagicSearchResults(results: Array) { - Log.i("$TAG Processing [${results.size}] results") - - val contactsList = arrayListOf() - var previousLetter = "" - - for (result in results) { - val address = result.address - if (address != null) { - val friend = coreContext.contactsManager.findContactByAddress(address) - if (friend != null) { - val model = ContactOrSuggestionModel(address, friend) - val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress( - address - ) - model.avatarModel.postValue(avatarModel) - - val currentLetter = friend.name?.get(0).toString() - val displayLetter = previousLetter.isEmpty() || currentLetter != previousLetter - if (currentLetter != previousLetter) { - previousLetter = currentLetter - } - avatarModel.firstContactStartingByThatLetter.postValue( - displayLetter - ) - - contactsList.add(model) - } - } - } - - val list = arrayListOf() - list.addAll(contactsList) - this.contactsList.postValue(list) - Log.i("$TAG Processed [${results.size}] results, extracted [${list.size}] suggestions") - } - - @UiThread - fun applyFilter(filter: String) { - coreContext.postOnCoreThread { - applyFilter( - filter, - if (limitSearchToLinphoneAccounts) corePreferences.defaultDomain else "", - MagicSearch.Source.Friends.toInt(), - MagicSearch.Aggregation.Friend - ) - } - } - - @WorkerThread - private fun applyFilter( - filter: String, - domain: String, - sources: Int, - aggregation: MagicSearch.Aggregation - ) { - if (previousFilter.isNotEmpty() && ( - previousFilter.length > filter.length || - (previousFilter.length == filter.length && previousFilter != filter) - ) - ) { - magicSearch.resetSearchCache() - } - currentFilter = filter - previousFilter = filter - - Log.i( - "$TAG Asking Magic search for contacts matching filter [$filter], domain [$domain] and in sources [$sources]" - ) - magicSearch.getContactsListAsync( - filter, - domain, - sources, - aggregation - ) - } } diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/AddParticipantToConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/fragment/AddParticipantsFragment.kt similarity index 54% rename from app/src/main/java/org/linphone/ui/main/chat/fragment/AddParticipantToConversationFragment.kt rename to app/src/main/java/org/linphone/ui/main/fragment/AddParticipantsFragment.kt index 56925cf66..30068f4d2 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/AddParticipantToConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/fragment/AddParticipantsFragment.kt @@ -17,32 +17,38 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.ui.main.chat.fragment +package org.linphone.ui.main.fragment import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.UiThread +import androidx.core.view.doOnPreDraw import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController -import org.linphone.databinding.ChatAddParticipantFragmentBinding -import org.linphone.ui.main.chat.viewmodel.ConversationAddParticipantViewModel -import org.linphone.ui.main.fragment.GenericFragment +import org.linphone.core.Address +import org.linphone.core.Friend +import org.linphone.core.tools.Log +import org.linphone.databinding.GenericAddParticipantsFragmentBinding +import org.linphone.ui.main.viewmodel.AddParticipantsViewModel @UiThread -class AddParticipantToConversationFragment : GenericFragment() { +class AddParticipantsFragment : GenericAddressPickerFragment() { + companion object { + private const val TAG = "[Add Participants Fragment]" + } - private lateinit var binding: ChatAddParticipantFragmentBinding + private lateinit var binding: GenericAddParticipantsFragmentBinding - private lateinit var viewModel: ConversationAddParticipantViewModel + override lateinit var viewModel: AddParticipantsViewModel override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = ChatAddParticipantFragmentBinding.inflate(layoutInflater) + binding = GenericAddParticipantsFragmentBinding.inflate(layoutInflater) return binding.root } @@ -51,19 +57,41 @@ class AddParticipantToConversationFragment : GenericFragment() { return true } + override fun onSingleAddressSelected(address: Address, friend: Friend) { + Log.e("$TAG This shouldn't happen as we should always be in multiple selection mode here!") + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { // This fragment is displayed in a SlidingPane "child" area isSlidingPaneChild = true + viewModel = ViewModelProvider(this)[AddParticipantsViewModel::class.java] + super.onViewCreated(view, savedInstanceState) + postponeEnterTransition() binding.lifecycleOwner = viewLifecycleOwner - viewModel = ViewModelProvider(this)[ConversationAddParticipantViewModel::class.java] binding.viewModel = viewModel binding.setBackClickListener { goBack() } + + setupRecyclerView(binding.contactsList) + + viewModel.contactsAndSuggestionsList.observe( + viewLifecycleOwner + ) { + Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items") + val count = adapter.itemCount + adapter.submitList(it) + + if (count == 0) { + (view.parent as? ViewGroup)?.doOnPreDraw { + startPostponedEnterTransition() + } + } + } } } diff --git a/app/src/main/java/org/linphone/ui/main/fragment/GenericAddressPickerFragment.kt b/app/src/main/java/org/linphone/ui/main/fragment/GenericAddressPickerFragment.kt index f52177536..dd98dd443 100644 --- a/app/src/main/java/org/linphone/ui/main/fragment/GenericAddressPickerFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/fragment/GenericAddressPickerFragment.kt @@ -20,8 +20,12 @@ package org.linphone.ui.main.fragment import android.app.Dialog +import android.os.Bundle +import android.view.View import androidx.annotation.UiThread import androidx.annotation.WorkerThread +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.contacts.getListOfSipAddressesAndPhoneNumbers import org.linphone.core.Address @@ -30,9 +34,13 @@ import org.linphone.core.tools.Log import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel +import org.linphone.ui.main.history.adapter.ContactsAndSuggestionsListAdapter import org.linphone.ui.main.history.model.ContactOrSuggestionModel +import org.linphone.ui.main.model.SelectedAddressModel import org.linphone.ui.main.model.isInSecureMode +import org.linphone.ui.main.viewmodel.AddressSelectionViewModel import org.linphone.utils.DialogUtils +import org.linphone.utils.RecyclerViewHeaderDecoration @UiThread abstract class GenericAddressPickerFragment : GenericFragment() { @@ -42,15 +50,9 @@ abstract class GenericAddressPickerFragment : GenericFragment() { private var numberOrAddressPickerDialog: Dialog? = null - @WorkerThread - abstract fun onAddressSelected(address: Address, friend: Friend) + protected lateinit var adapter: ContactsAndSuggestionsListAdapter - override fun onPause() { - super.onPause() - - numberOrAddressPickerDialog?.dismiss() - numberOrAddressPickerDialog = null - } + protected abstract val viewModel: AddressSelectionViewModel private val listener = object : ContactNumberOrAddressClickListener { @UiThread @@ -74,6 +76,57 @@ abstract class GenericAddressPickerFragment : GenericFragment() { } } + @WorkerThread + abstract fun onSingleAddressSelected(address: Address, friend: Friend) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + adapter = ContactsAndSuggestionsListAdapter(viewLifecycleOwner) + + adapter.contactClickedEvent.observe(viewLifecycleOwner) { + it.consume { model -> + handleClickOnContactModel(model) + } + } + + viewModel.searchFilter.observe(viewLifecycleOwner) { filter -> + val trimmed = filter.trim() + viewModel.applyFilter(trimmed) + } + } + + override fun onPause() { + super.onPause() + + numberOrAddressPickerDialog?.dismiss() + numberOrAddressPickerDialog = null + } + + @UiThread + protected fun setupRecyclerView(recyclerView: RecyclerView) { + recyclerView.setHasFixedSize(true) + recyclerView.adapter = adapter + + val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter, true) + recyclerView.addItemDecoration(headerItemDecoration) + + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + } + + @WorkerThread + fun onAddressSelected(address: Address, friend: Friend) { + if (viewModel.multipleSelectionMode.value == true) { + val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(address) + val model = SelectedAddressModel(address, avatarModel) { + viewModel.removeAddressModelFromSelection(it) + } + viewModel.addAddressModelToSelection(model) + } else { + onSingleAddressSelected(address, friend) + } + } + protected fun handleClickOnContactModel(model: ContactOrSuggestionModel) { coreContext.postOnCoreThread { core -> val friend = model.friend diff --git a/app/src/main/java/org/linphone/ui/main/history/fragment/StartCallFragment.kt b/app/src/main/java/org/linphone/ui/main/history/fragment/StartCallFragment.kt index 4faa32601..a896322f9 100644 --- a/app/src/main/java/org/linphone/ui/main/history/fragment/StartCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/history/fragment/StartCallFragment.kt @@ -26,8 +26,7 @@ import android.view.ViewGroup import androidx.annotation.UiThread import androidx.annotation.WorkerThread import androidx.core.view.doOnPreDraw -import androidx.navigation.navGraphViewModels -import androidx.recyclerview.widget.LinearLayoutManager +import androidx.lifecycle.ViewModelProvider import com.google.android.material.bottomsheet.BottomSheetBehavior import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R @@ -36,9 +35,7 @@ import org.linphone.core.Friend import org.linphone.core.tools.Log import org.linphone.databinding.StartCallFragmentBinding import org.linphone.ui.main.fragment.GenericAddressPickerFragment -import org.linphone.ui.main.history.adapter.ContactsAndSuggestionsListAdapter import org.linphone.ui.main.history.viewmodel.StartCallViewModel -import org.linphone.utils.RecyclerViewHeaderDecoration import org.linphone.utils.addCharacterAtPosition import org.linphone.utils.hideKeyboard import org.linphone.utils.removeCharacterAtPosition @@ -53,11 +50,7 @@ class StartCallFragment : GenericAddressPickerFragment() { private lateinit var binding: StartCallFragmentBinding - private val viewModel: StartCallViewModel by navGraphViewModels( - R.id.main_nav_graph - ) - - private lateinit var adapter: ContactsAndSuggestionsListAdapter + override lateinit var viewModel: StartCallViewModel override fun onCreateView( inflater: LayoutInflater, @@ -69,6 +62,8 @@ class StartCallFragment : GenericAddressPickerFragment() { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + viewModel = ViewModelProvider(this)[StartCallViewModel::class.java] + super.onViewCreated(view, savedInstanceState) postponeEnterTransition() @@ -85,20 +80,7 @@ class StartCallFragment : GenericAddressPickerFragment() { viewModel.hideNumpad() } - adapter = ContactsAndSuggestionsListAdapter(viewLifecycleOwner) - binding.contactsAndSuggestionsList.setHasFixedSize(true) - binding.contactsAndSuggestionsList.adapter = adapter - - val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter, true) - binding.contactsAndSuggestionsList.addItemDecoration(headerItemDecoration) - - adapter.contactClickedEvent.observe(viewLifecycleOwner) { - it.consume { model -> - handleClickOnContactModel(model) - } - } - - binding.contactsAndSuggestionsList.layoutManager = LinearLayoutManager(requireContext()) + setupRecyclerView(binding.contactsAndSuggestionsList) viewModel.contactsAndSuggestionsList.observe( viewLifecycleOwner @@ -114,11 +96,6 @@ class StartCallFragment : GenericAddressPickerFragment() { } } - viewModel.searchFilter.observe(viewLifecycleOwner) { filter -> - val trimmed = filter.trim() - viewModel.applyFilter(trimmed) - } - viewModel.removedCharacterAtCurrentPositionEvent.observe(viewLifecycleOwner) { it.consume { binding.searchBar.removeCharacterAtPosition() @@ -165,7 +142,7 @@ class StartCallFragment : GenericAddressPickerFragment() { } @WorkerThread - override fun onAddressSelected(address: Address, friend: Friend) { + override fun onSingleAddressSelected(address: Address, friend: Friend) { coreContext.startCall(address) } diff --git a/app/src/main/java/org/linphone/ui/main/history/viewmodel/StartCallViewModel.kt b/app/src/main/java/org/linphone/ui/main/history/viewmodel/StartCallViewModel.kt index 15c422b74..403377990 100644 --- a/app/src/main/java/org/linphone/ui/main/history/viewmodel/StartCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/history/viewmodel/StartCallViewModel.kt @@ -20,37 +20,25 @@ package org.linphone.ui.main.history.viewmodel import androidx.annotation.UiThread -import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import java.util.ArrayList import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.contacts.ContactsManager.ContactsListener -import org.linphone.core.MagicSearch -import org.linphone.core.MagicSearchListenerStub -import org.linphone.core.SearchResult import org.linphone.core.tools.Log -import org.linphone.ui.main.history.model.ContactOrSuggestionModel import org.linphone.ui.main.history.model.NumpadModel -import org.linphone.ui.main.model.isInSecureMode +import org.linphone.ui.main.viewmodel.AddressSelectionViewModel import org.linphone.utils.Event import org.linphone.utils.LinphoneUtils -class StartCallViewModel @UiThread constructor() : ViewModel() { +class StartCallViewModel @UiThread constructor() : AddressSelectionViewModel() { companion object { private const val TAG = "[Start Call ViewModel]" } val title = MutableLiveData() - val searchFilter = MutableLiveData() - - val contactsAndSuggestionsList = MutableLiveData>() - val numpadModel: NumpadModel val hideGroupCallButton = MutableLiveData() @@ -71,33 +59,6 @@ class StartCallViewModel @UiThread constructor() : ViewModel() { MutableLiveData>() } - private var currentFilter = "" - private var previousFilter = "NotSet" - private var limitSearchToLinphoneAccounts = true - - private lateinit var magicSearch: MagicSearch - - private val magicSearchListener = object : MagicSearchListenerStub() { - @WorkerThread - override fun onSearchResultsReceived(magicSearch: MagicSearch) { - Log.i("$TAG Magic search contacts available") - processMagicSearchResults(magicSearch.lastSearch) - } - } - - private val contactsListener = object : ContactsListener { - @WorkerThread - override fun onContactsLoaded() { - Log.i("$TAG Contacts have been (re)loaded, updating list") - applyFilter( - currentFilter, - if (limitSearchToLinphoneAccounts) corePreferences.defaultDomain else "", - MagicSearch.Source.All.toInt(), - MagicSearch.Aggregation.Friend - ) - } - } - init { isNumpadVisible.value = false numpadModel = NumpadModel( @@ -129,32 +90,6 @@ class StartCallViewModel @UiThread constructor() : ViewModel() { ) updateGroupCallButtonVisibility() - - coreContext.postOnCoreThread { core -> - val defaultAccount = core.defaultAccount - limitSearchToLinphoneAccounts = defaultAccount?.isInSecureMode() ?: false - - coreContext.contactsManager.addListener(contactsListener) - magicSearch = core.createMagicSearch() - magicSearch.limitedSearch = false - magicSearch.addListener(magicSearchListener) - } - - applyFilter(currentFilter) - } - - @UiThread - override fun onCleared() { - coreContext.postOnCoreThread { - magicSearch.removeListener(magicSearchListener) - coreContext.contactsManager.removeListener(contactsListener) - } - super.onCleared() - } - - @UiThread - fun clearFilter() { - searchFilter.value = "" } @UiThread @@ -181,102 +116,4 @@ class StartCallViewModel @UiThread constructor() : ViewModel() { fun hideNumpad() { isNumpadVisible.value = false } - - @WorkerThread - fun processMagicSearchResults(results: Array) { - Log.i("$TAG Processing [${results.size}] results") - - val contactsList = arrayListOf() - val suggestionsList = arrayListOf() - var previousLetter = "" - - for (result in results) { - val address = result.address - if (address != null) { - val friend = coreContext.contactsManager.findContactByAddress(address) - if (friend != null) { - val model = ContactOrSuggestionModel(address, friend) - val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress( - address - ) - model.avatarModel.postValue(avatarModel) - - val currentLetter = friend.name?.get(0).toString() - val displayLetter = previousLetter.isEmpty() || currentLetter != previousLetter - if (currentLetter != previousLetter) { - previousLetter = currentLetter - } - avatarModel.firstContactStartingByThatLetter.postValue( - displayLetter - ) - - contactsList.add(model) - } else { - // If user-input generated result (always last) already exists, don't show it again - if (result.sourceFlags == MagicSearch.Source.Request.toInt()) { - val found = suggestionsList.find { - it.address.weakEqual(address) - } - if (found != null) { - Log.i( - "$TAG Result generated from user input is a duplicate of an existing solution, preventing double" - ) - continue - } - } - - val model = ContactOrSuggestionModel(address) { - coreContext.startCall(address) - } - suggestionsList.add(model) - } - } - } - - val list = arrayListOf() - list.addAll(contactsList) - list.addAll(suggestionsList) - contactsAndSuggestionsList.postValue(list) - Log.i("$TAG Processed [${results.size}] results, extracted [${list.size}] suggestions") - } - - @UiThread - fun applyFilter(filter: String) { - coreContext.postOnCoreThread { - applyFilter( - filter, - if (limitSearchToLinphoneAccounts) corePreferences.defaultDomain else "", - MagicSearch.Source.All.toInt(), - MagicSearch.Aggregation.Friend - ) - } - } - - @WorkerThread - private fun applyFilter( - filter: String, - domain: String, - sources: Int, - aggregation: MagicSearch.Aggregation - ) { - if (previousFilter.isNotEmpty() && ( - previousFilter.length > filter.length || - (previousFilter.length == filter.length && previousFilter != filter) - ) - ) { - magicSearch.resetSearchCache() - } - currentFilter = filter - previousFilter = filter - - Log.i( - "$TAG Asking Magic search for contacts matching filter [$filter], domain [$domain] and in sources [$sources]" - ) - magicSearch.getContactsListAsync( - filter, - domain, - sources, - aggregation - ) - } } diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationAddParticipantViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/AddParticipantsViewModel.kt similarity index 64% rename from app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationAddParticipantViewModel.kt rename to app/src/main/java/org/linphone/ui/main/viewmodel/AddParticipantsViewModel.kt index 940c7cd05..89f829064 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationAddParticipantViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/AddParticipantsViewModel.kt @@ -17,20 +17,16 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.ui.main.chat.viewmodel +package org.linphone.ui.main.viewmodel import androidx.annotation.UiThread -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.linphone.ui.main.history.model.ContactOrSuggestionModel -class ConversationAddParticipantViewModel @UiThread constructor() : ViewModel() { - val searchFilter = MutableLiveData() +class AddParticipantsViewModel @UiThread constructor() : AddressSelectionViewModel() { + companion object { + private const val TAG = "[Add Participants ViewModel]" + } - val contactsList = MutableLiveData>() - - @UiThread - fun clearFilter() { - searchFilter.value = "" + init { + switchToMultipleSelectionMode() } } diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt index 50beebc2b..0b711bd82 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt @@ -23,9 +23,17 @@ import androidx.annotation.UiThread import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R +import org.linphone.contacts.ContactsManager +import org.linphone.core.MagicSearch +import org.linphone.core.MagicSearchListenerStub +import org.linphone.core.SearchResult import org.linphone.mediastream.Log +import org.linphone.ui.main.history.model.ContactOrSuggestionModel import org.linphone.ui.main.model.SelectedAddressModel +import org.linphone.ui.main.model.isInSecureMode import org.linphone.utils.AppUtils abstract class AddressSelectionViewModel @UiThread constructor() : ViewModel() { @@ -39,14 +47,82 @@ abstract class AddressSelectionViewModel @UiThread constructor() : ViewModel() { val selectionCount = MutableLiveData() + protected var magicSearchSourceFlags = MagicSearch.Source.All.toInt() + + private var currentFilter = "" + private var previousFilter = "NotSet" + + val searchFilter = MutableLiveData() + + val contactsAndSuggestionsList = MutableLiveData>() + + private var limitSearchToLinphoneAccounts = true + + private lateinit var magicSearch: MagicSearch + + private val magicSearchListener = object : MagicSearchListenerStub() { + @WorkerThread + override fun onSearchResultsReceived(magicSearch: MagicSearch) { + Log.i("$TAG Magic search contacts available") + processMagicSearchResults(magicSearch.lastSearch) + } + } + + private val contactsListener = object : ContactsManager.ContactsListener { + @WorkerThread + override fun onContactsLoaded() { + Log.i("$TAG Contacts have been (re)loaded, updating list") + applyFilter( + currentFilter, + if (limitSearchToLinphoneAccounts) corePreferences.defaultDomain else "", + magicSearchSourceFlags, + MagicSearch.Aggregation.Friend + ) + } + } + init { multipleSelectionMode.value = false + + coreContext.postOnCoreThread { core -> + val defaultAccount = core.defaultAccount + limitSearchToLinphoneAccounts = defaultAccount?.isInSecureMode() ?: false + + coreContext.contactsManager.addListener(contactsListener) + magicSearch = core.createMagicSearch() + magicSearch.limitedSearch = false + magicSearch.addListener(magicSearchListener) + } + + applyFilter(currentFilter) + } + + @UiThread + override fun onCleared() { + coreContext.postOnCoreThread { + magicSearch.removeListener(magicSearchListener) + coreContext.contactsManager.removeListener(contactsListener) + } + super.onCleared() + } + + @UiThread + fun clearFilter() { + searchFilter.value = "" } @UiThread fun switchToMultipleSelectionMode() { Log.i("$$TAG Multiple selection mode ON") multipleSelectionMode.value = true + + selectionCount.postValue( + AppUtils.getStringWithPlural( + R.plurals.selection_count_label, + 0, + "0" + ) + ) } @WorkerThread @@ -101,4 +177,104 @@ abstract class AddressSelectionViewModel @UiThread constructor() : ViewModel() { Log.w("$TAG Address isn't in selection, doing nothing") } } + + @WorkerThread + fun processMagicSearchResults(results: Array) { + Log.i("$TAG Processing [${results.size}] results") + + val contactsList = arrayListOf() + val suggestionsList = arrayListOf() + var previousLetter = "" + + for (result in results) { + val address = result.address + if (address != null) { + val friend = coreContext.contactsManager.findContactByAddress(address) + if (friend != null) { + val model = ContactOrSuggestionModel(address, friend) + val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress( + address + ) + model.avatarModel.postValue(avatarModel) + + val currentLetter = friend.name?.get(0).toString() + val displayLetter = previousLetter.isEmpty() || currentLetter != previousLetter + if (currentLetter != previousLetter) { + previousLetter = currentLetter + } + avatarModel.firstContactStartingByThatLetter.postValue( + displayLetter + ) + + contactsList.add(model) + } else { + // If user-input generated result (always last) already exists, don't show it again + if (result.sourceFlags == MagicSearch.Source.Request.toInt()) { + val found = suggestionsList.find { + it.address.weakEqual(address) + } + if (found != null) { + Log.i( + "$TAG Result generated from user input is a duplicate of an existing solution, preventing double" + ) + continue + } + } + + val model = ContactOrSuggestionModel(address) { + coreContext.startCall(address) + } + suggestionsList.add(model) + } + } + } + + val list = arrayListOf() + list.addAll(contactsList) + list.addAll(suggestionsList) + contactsAndSuggestionsList.postValue(list) + Log.i( + "$TAG Processed [${results.size}] results, extracted [${suggestionsList.size}] suggestions" + ) + } + + @UiThread + fun applyFilter(filter: String) { + coreContext.postOnCoreThread { + applyFilter( + filter, + if (limitSearchToLinphoneAccounts) corePreferences.defaultDomain else "", + magicSearchSourceFlags, + MagicSearch.Aggregation.Friend + ) + } + } + + @WorkerThread + private fun applyFilter( + filter: String, + domain: String, + sources: Int, + aggregation: MagicSearch.Aggregation + ) { + if (previousFilter.isNotEmpty() && ( + previousFilter.length > filter.length || + (previousFilter.length == filter.length && previousFilter != filter) + ) + ) { + magicSearch.resetSearchCache() + } + currentFilter = filter + previousFilter = filter + + Log.i( + "$TAG Asking Magic search for contacts matching filter [$filter], domain [$domain] and in sources [$sources]" + ) + magicSearch.getContactsListAsync( + filter, + domain, + sources, + aggregation + ) + } } diff --git a/app/src/main/res/layout/chat_add_participant_fragment.xml b/app/src/main/res/layout/generic_add_participants_fragment.xml similarity index 77% rename from app/src/main/res/layout/chat_add_participant_fragment.xml rename to app/src/main/res/layout/generic_add_participants_fragment.xml index a28be9745..919acb449 100644 --- a/app/src/main/res/layout/chat_add_participant_fragment.xml +++ b/app/src/main/res/layout/generic_add_participants_fragment.xml @@ -10,7 +10,7 @@ type="View.OnClickListener" /> + type="org.linphone.ui.main.viewmodel.AddParticipantsViewModel" /> @@ -56,6 +56,36 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/title" /> + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/multiple_selection"/> - - diff --git a/app/src/main/res/layout/start_call_fragment.xml b/app/src/main/res/layout/start_call_fragment.xml index 382b330f7..cf5d10015 100644 --- a/app/src/main/res/layout/start_call_fragment.xml +++ b/app/src/main/res/layout/start_call_fragment.xml @@ -11,9 +11,6 @@ - diff --git a/app/src/main/res/layout/start_chat_fragment.xml b/app/src/main/res/layout/start_chat_fragment.xml index dc506e923..d8d4f32c8 100644 --- a/app/src/main/res/layout/start_chat_fragment.xml +++ b/app/src/main/res/layout/start_chat_fragment.xml @@ -248,7 +248,7 @@ android:layout_height="0dp" android:layout_margin="10dp" android:src="@drawable/illu" - android:visibility="@{viewModel.contactsList.size() == 0 ? View.VISIBLE : View.GONE}" + android:visibility="@{viewModel.contactsAndSuggestionsList.size() == 0 ? View.VISIBLE : View.GONE}" app:layout_constraintDimensionRatio="1:1" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHeight_max="200dp" @@ -262,36 +262,22 @@ android:id="@+id/no_contact_label" android:layout_width="0dp" android:layout_height="wrap_content" - android:text="@string/new_conversation_no_contact" + android:text="@{viewModel.searchFilter.length() > 0 ? @string/new_conversation_no_matching_contact : @string/new_conversation_no_contact, default=@string/new_conversation_no_contact}" android:gravity="center" - android:visibility="@{viewModel.contactsList.size() == 0 ? View.VISIBLE : View.GONE}" + android:visibility="@{viewModel.contactsAndSuggestionsList.size() == 0 ? View.VISIBLE : View.GONE}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/no_contact_image" /> - - diff --git a/app/src/main/res/navigation/chat_nav_graph.xml b/app/src/main/res/navigation/chat_nav_graph.xml index ae1b2c3be..286fda8b1 100644 --- a/app/src/main/res/navigation/chat_nav_graph.xml +++ b/app/src/main/res/navigation/chat_nav_graph.xml @@ -48,8 +48,8 @@ android:name="remoteSipUri" app:argType="string" /> + android:id="@+id/addParticipantsFragment" + android:name="org.linphone.ui.main.fragment.AddParticipantsFragment" + android:label="AddParticipantsFragment" + tools:layout="@layout/generic_add_participants_fragment" /> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ae53ffc45..d3b45b311 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -345,14 +345,15 @@ New conversation Search contact Create a group conversation - No contact for the moment… + No contact and no suggestion for the moment… + No matching result… Name of the group Say something… %s is composing… %s are composing… - Add participant + Add participants Group members Add participants