diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt
index 5860c4bcf..9bcaaee70 100644
--- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt
+++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt
@@ -48,10 +48,14 @@ class QrCodeViewModel @UiThread constructor() : ViewModel() {
if (!isValidUrl) {
Log.e("$TAG The content of the QR Code doesn't seem to be a valid web URL")
} else {
- Log.i("$TAG QR code URL set, restarting the Core")
+ Log.i(
+ "$TAG QR code URL set, restarting the Core to apply configuration changes"
+ )
core.provisioningUri = result
coreContext.core.stop()
+ Log.i("$TAG Core has been stopped, restarting it")
coreContext.core.start()
+ Log.i("$TAG Core has been restarted")
}
qrCodeFoundEvent.postValue(Event(isValidUrl))
}
diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsFragment.kt
index e88dd0cb2..39076038f 100644
--- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsFragment.kt
+++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsFragment.kt
@@ -24,6 +24,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
+import android.view.animation.AnimationUtils
import androidx.core.view.doOnPreDraw
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
@@ -51,10 +52,10 @@ class ConversationsFragment : GenericFragment() {
}
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
- /*if (findNavController().currentDestination?.id == R.id.newConversationFragment) {
+ if (findNavController().currentDestination?.id == R.id.startConversationFragment) {
// Holds fragment in place while new contact fragment slides over it
return AnimationUtils.loadAnimation(activity, R.anim.hold)
- }*/
+ }
return super.onCreateAnimation(transit, enter, nextAnim)
}
@@ -93,6 +94,13 @@ class ConversationsFragment : GenericFragment() {
}
}
+ sharedViewModel.showStartConversationEvent.observe(viewLifecycleOwner) {
+ it.consume {
+ Log.i("$TAG Navigating to start conversation fragment")
+ findNavController().navigate(R.id.action_global_startConversationFragment)
+ }
+ }
+
sharedViewModel.navigateToContactsEvent.observe(viewLifecycleOwner) {
it.consume {
if (findNavController().currentDestination?.id == R.id.conversationsFragment) {
diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt
index ace1bfa21..e71ce3bca 100644
--- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt
+++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt
@@ -32,6 +32,7 @@ import org.linphone.ui.main.chat.adapter.ConversationsListAdapter
import org.linphone.ui.main.chat.viewmodel.ConversationsListViewModel
import org.linphone.ui.main.fragment.AbstractTopBarFragment
import org.linphone.ui.main.history.fragment.HistoryMenuDialogFragment
+import org.linphone.utils.Event
import org.linphone.utils.hideKeyboard
import org.linphone.utils.showKeyboard
@@ -112,9 +113,14 @@ class ConversationsListFragment : AbstractTopBarFragment() {
adapter.conversationClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
Log.i("$TAG Show conversation with ID [${model.id}]")
+ // TODO
}
}
+ binding.setOnNewConversationClicked {
+ sharedViewModel.showStartConversationEvent.value = Event(true)
+ }
+
listViewModel.conversations.observe(viewLifecycleOwner) {
val currentCount = adapter.itemCount
adapter.submitList(it)
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
new file mode 100644
index 000000000..2ee23cb34
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/StartConversationFragment.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2010-2023 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * 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
+
+import android.app.Dialog
+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.navigation.navGraphViewModels
+import androidx.recyclerview.widget.LinearLayoutManager
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.R
+import org.linphone.core.tools.Log
+import org.linphone.databinding.StartChatFragmentBinding
+import org.linphone.ui.main.chat.viewmodel.StartConversationViewModel
+import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener
+import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel
+import org.linphone.ui.main.fragment.GenericFragment
+import org.linphone.ui.main.history.adapter.ContactsAndSuggestionsListAdapter
+
+@UiThread
+class StartConversationFragment : GenericFragment() {
+ companion object {
+ private const val TAG = "[Start Conversation Fragment]"
+ }
+
+ private lateinit var binding: StartChatFragmentBinding
+
+ private val viewModel: StartConversationViewModel by navGraphViewModels(
+ R.id.main_nav_graph
+ )
+
+ private lateinit var adapter: ContactsAndSuggestionsListAdapter
+
+ private val listener = object : ContactNumberOrAddressClickListener {
+ @UiThread
+ override fun onClicked(model: ContactNumberOrAddressModel) {
+ val address = model.address
+ if (address != null) {
+ coreContext.postOnCoreThread {
+ // TODO
+ }
+ }
+ }
+
+ @UiThread
+ override fun onLongPress(model: ContactNumberOrAddressModel) {
+ }
+ }
+
+ private var numberOrAddressPickerDialog: Dialog? = null
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = StartChatFragmentBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ postponeEnterTransition()
+
+ binding.lifecycleOwner = viewLifecycleOwner
+ binding.viewModel = viewModel
+
+ binding.setBackClickListener {
+ goBack()
+ }
+
+ adapter = ContactsAndSuggestionsListAdapter(viewLifecycleOwner)
+ binding.contactsList.setHasFixedSize(true)
+ binding.contactsList.adapter = adapter
+
+ adapter.contactClickedEvent.observe(viewLifecycleOwner) {
+ it.consume { model ->
+ // TODO
+ }
+ }
+
+ binding.contactsList.layoutManager = LinearLayoutManager(requireContext())
+
+ viewModel.contactsList.observe(
+ viewLifecycleOwner
+ ) {
+ Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items")
+ val count = adapter.itemCount
+ adapter.submitList(it)
+
+ if (count == 0 && it.isNotEmpty()) {
+ (view.parent as? ViewGroup)?.doOnPreDraw {
+ startPostponedEnterTransition()
+ }
+ }
+ }
+
+ viewModel.searchFilter.observe(viewLifecycleOwner) { filter ->
+ val trimmed = filter.trim()
+ viewModel.applyFilter(trimmed)
+ }
+
+ sharedViewModel.defaultAccountChangedEvent.observe(viewLifecycleOwner) {
+ // Do not consume it!
+ viewModel.updateGroupChatButtonVisibility()
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+
+ numberOrAddressPickerDialog?.dismiss()
+ numberOrAddressPickerDialog = null
+ }
+}
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
new file mode 100644
index 000000000..d4ec27fb8
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/StartConversationViewModel.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright (c) 2010-2023 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * 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
+
+import androidx.annotation.UiThread
+import androidx.annotation.WorkerThread
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import java.util.ArrayList
+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.contacts.model.ContactAvatarModel
+import org.linphone.ui.main.history.model.ContactOrSuggestionModel
+import org.linphone.ui.main.model.isInSecureMode
+import org.linphone.utils.LinphoneUtils
+
+class StartConversationViewModel @UiThread constructor() : ViewModel() {
+ companion object {
+ private const val TAG = "[Start Conversation ViewModel]"
+ }
+
+ val searchFilter = MutableLiveData()
+
+ val contactsList = MutableLiveData>()
+
+ val hideGroupChatButton = MutableLiveData()
+
+ val isGroupChatAvailable = 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.Friends.toInt(),
+ MagicSearch.Aggregation.Friend
+ )
+ }
+ }
+
+ init {
+ 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
+ fun updateGroupChatButtonVisibility() {
+ coreContext.postOnCoreThread { core ->
+ val hideGroupChat = !LinphoneUtils.isGroupChatAvailable(core)
+ 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)
+ model.contactAvatarModel = ContactAvatarModel(friend)
+
+ val currentLetter = friend.name?.get(0).toString()
+ val displayLetter = previousLetter.isEmpty() || currentLetter != previousLetter
+ if (currentLetter != previousLetter) {
+ previousLetter = currentLetter
+ }
+ model.contactAvatarModel.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/viewmodel/SharedMainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt
index 7839f8f03..4eaccb346 100644
--- a/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt
+++ b/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt
@@ -94,4 +94,10 @@ class SharedMainViewModel @UiThread constructor() : ViewModel() {
val resetMissedCallsCountEvent: MutableLiveData> by lazy {
MutableLiveData>()
}
+
+ /* Conversation related */
+
+ val showStartConversationEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
}
diff --git a/app/src/main/res/layout/start_chat_fragment.xml b/app/src/main/res/layout/start_chat_fragment.xml
new file mode 100644
index 000000000..5c5e6ad82
--- /dev/null
+++ b/app/src/main/res/layout/start_chat_fragment.xml
@@ -0,0 +1,203 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/main_nav_graph.xml b/app/src/main/res/navigation/main_nav_graph.xml
index e8cc973a8..1f296d441 100644
--- a/app/src/main/res/navigation/main_nav_graph.xml
+++ b/app/src/main/res/navigation/main_nav_graph.xml
@@ -180,6 +180,7 @@
android:name="accountIdentity"
app:argType="string" />
+
+
+
+
+
\ 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 a3cf026e9..b1dfec863 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -325,6 +325,10 @@
Delete conversation
Leave the group
Yesterday at %s
+ New conversation
+ Search contact
+ Create a group conversation
+ No contact for the moment…
Operation in progress, please wait