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 2ee23cb34..0c9342bc3 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 @@ -30,13 +30,20 @@ import androidx.navigation.navGraphViewModels import androidx.recyclerview.widget.LinearLayoutManager import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R +import org.linphone.contacts.getListOfSipAddressesAndPhoneNumbers import org.linphone.core.tools.Log import org.linphone.databinding.StartChatFragmentBinding +import org.linphone.ui.main.MainActivity 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.contacts.model.NumberOrAddressPickerDialogModel import org.linphone.ui.main.fragment.GenericFragment import org.linphone.ui.main.history.adapter.ContactsAndSuggestionsListAdapter +import org.linphone.ui.main.history.model.ContactOrSuggestionModel +import org.linphone.ui.main.model.isInSecureMode +import org.linphone.utils.DialogUtils +import org.linphone.utils.Event @UiThread class StartConversationFragment : GenericFragment() { @@ -56,9 +63,12 @@ class StartConversationFragment : GenericFragment() { @UiThread override fun onClicked(model: ContactNumberOrAddressModel) { val address = model.address - if (address != null) { - coreContext.postOnCoreThread { - // TODO + coreContext.postOnCoreThread { + if (address != null) { + Log.i( + "$TAG Creating a 1-1 conversation with [${model.address.asStringUriOnly()}]" + ) + viewModel.createOneToOneChatRoomWith(model.address) } } } @@ -96,7 +106,7 @@ class StartConversationFragment : GenericFragment() { adapter.contactClickedEvent.observe(viewLifecycleOwner) { it.consume { model -> - // TODO + createChatRoom(model) } } @@ -116,6 +126,23 @@ class StartConversationFragment : GenericFragment() { } } + viewModel.chatRoomCreatedEvent.observe(viewLifecycleOwner) { + it.consume { pair -> + Log.i( + "$TAG Chat room [${pair.second}] for local address [${pair.first}] has been created, navigating to it" + ) + sharedViewModel.showConversationEvent.value = Event(pair) + goBack() + } + } + + viewModel.chatRoomCreationErrorEvent.observe(viewLifecycleOwner) { + it.consume { error -> + Log.i("$TAG Chat room creation error, showing red toast") + (requireActivity() as MainActivity).showRedToast(error, R.drawable.warning_circle) + } + } + viewModel.searchFilter.observe(viewLifecycleOwner) { filter -> val trimmed = filter.trim() viewModel.applyFilter(trimmed) @@ -133,4 +160,63 @@ class StartConversationFragment : GenericFragment() { numberOrAddressPickerDialog?.dismiss() numberOrAddressPickerDialog = null } + + private fun createChatRoom(model: ContactOrSuggestionModel) { + coreContext.postOnCoreThread { core -> + val friend = model.friend + if (friend == null) { + Log.i("$TAG Friend is null, creating conversation with [${model.address}]") + viewModel.createOneToOneChatRoomWith(model.address) + return@postOnCoreThread + } + + val addressesCount = friend.addresses.size + val numbersCount = friend.phoneNumbers.size + + // Do not consider phone numbers if default account is in secure mode + val enablePhoneNumbers = core.defaultAccount?.isInSecureMode() != true + + if (addressesCount == 1 && (numbersCount == 0 || !enablePhoneNumbers)) { + Log.i( + "$TAG Only 1 SIP address found for contact [${friend.name}], creating conversation directly" + ) + val address = friend.addresses.first() + viewModel.createOneToOneChatRoomWith(address) + } else if (addressesCount == 0 && numbersCount == 1 && enablePhoneNumbers) { + val number = friend.phoneNumbers.first() + val address = core.interpretUrl(number, true) + if (address != null) { + Log.i( + "$TAG Only 1 phone number found for contact [${friend.name}], creating conversation directly" + ) + viewModel.createOneToOneChatRoomWith(address) + } else { + Log.e("$TAG Failed to interpret phone number [$number] as SIP address") + } + } else { + val list = friend.getListOfSipAddressesAndPhoneNumbers(listener) + Log.i( + "$TAG [${list.size}] numbers or addresses found for contact [${friend.name}], showing selection dialog" + ) + + coreContext.postOnMainThread { + val numberOrAddressModel = NumberOrAddressPickerDialogModel(list) + val dialog = + DialogUtils.getNumberOrAddressPickerDialog( + requireActivity(), + numberOrAddressModel + ) + numberOrAddressPickerDialog = dialog + + numberOrAddressModel.dismissEvent.observe(viewLifecycleOwner) { event -> + event.consume { + dialog.dismiss() + } + } + + dialog.show() + } + } + } + } } 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 24be5cb37..7459ff0cd 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 @@ -26,7 +26,12 @@ import androidx.lifecycle.ViewModel import java.util.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 @@ -34,6 +39,8 @@ 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.AppUtils +import org.linphone.utils.Event import org.linphone.utils.LinphoneUtils class StartConversationViewModel @UiThread constructor() : ViewModel() { @@ -47,7 +54,44 @@ class StartConversationViewModel @UiThread constructor() : ViewModel() { val hideGroupChatButton = MutableLiveData() - val isGroupChatAvailable = MutableLiveData() + val operationInProgress = MutableLiveData() + + val chatRoomCreationErrorEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val chatRoomCreatedEvent: MutableLiveData>> by lazy { + MutableLiveData>>() + } + + private val chatRoomListener = object : ChatRoomListenerStub() { + @WorkerThread + override fun onStateChanged(chatRoom: ChatRoom, newState: ChatRoom.State?) { + val state = chatRoom.state + Log.i("$TAG Chat room state changed: [$state]") + + if (state == ChatRoom.State.Created) { + val id = LinphoneUtils.getChatRoomId(chatRoom) + Log.i("$TAG Chat room [$id] successfully created") + chatRoom.removeListener(this) + operationInProgress.postValue(false) + chatRoomCreatedEvent.postValue( + Event( + Pair( + chatRoom.localAddress.asStringUriOnly(), + chatRoom.peerAddress.asStringUriOnly() + ) + ) + ) + } else if (state == ChatRoom.State.CreationFailed) { + val id = LinphoneUtils.getChatRoomId(chatRoom) + Log.e("$TAG Chat room [$id] creation has failed!") + chatRoom.removeListener(this) + operationInProgress.postValue(false) + chatRoomCreationErrorEvent.postValue(Event("Error!")) // TODO FIXME: use translated string + } + } + } private var currentFilter = "" private var previousFilter = "NotSet" @@ -106,6 +150,102 @@ class StartConversationViewModel @UiThread constructor() : ViewModel() { searchFilter.value = "" } + @WorkerThread + fun createOneToOneChatRoomWith(remote: Address) { + val core = coreContext.core + val account = core.defaultAccount + if (account == null) { + Log.e( + "$TAG No default account found, can't create conversation with [${remote.asStringUriOnly()}]!" + ) + return + } + + operationInProgress.postValue(true) + + val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams() + params.isGroupEnabled = false + params.subject = AppUtils.getString(R.string.conversation_one_to_one_hidden_subject) + + val sameDomain = remote.domain == corePreferences.defaultDomain && remote.domain == account.params.domain + if (account.isInSecureMode() && sameDomain) { + Log.i("$TAG Account is in secure mode & domain matches, creating a E2E chat room") + params.backend = ChatRoom.Backend.FlexisipChat + params.isEncryptionEnabled = true + params.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default + } else if (!account.isInSecureMode()) { + Log.i("$TAG Account is in interop mode, creating a SIP simple chat room") + params.backend = ChatRoom.Backend.Basic + params.isEncryptionEnabled = false + } else { + Log.e( + "$TAG Account is in secure mode, can't chat with SIP address of different domain [${remote.asStringUriOnly()}]" + ) + operationInProgress.postValue(false) + chatRoomCreationErrorEvent.postValue(Event("Error!")) // TODO FIXME: use translated string + return + } + + val participants = arrayOf(remote) + val localAddress = account.params.identityAddress + val existingChatRoom = core.searchChatRoom(params, localAddress, null, participants) + if (existingChatRoom == null) { + Log.i( + "$TAG No existing 1-1 chat room between local account [${localAddress?.asStringUriOnly()}] and remote [${remote.asStringUriOnly()}] was found for given parameters, let's create it" + ) + val chatRoom = core.createChatRoom(params, localAddress, participants) + if (chatRoom != null) { + if (params.backend == ChatRoom.Backend.FlexisipChat) { + if (chatRoom.state == ChatRoom.State.Created) { + val id = LinphoneUtils.getChatRoomId(chatRoom) + Log.i("$TAG 1-1 chat room [$id] has been created") + operationInProgress.postValue(false) + chatRoomCreatedEvent.postValue( + Event( + Pair( + chatRoom.localAddress.asStringUriOnly(), + chatRoom.peerAddress.asStringUriOnly() + ) + ) + ) + } else { + Log.i("$TAG Chat room isn't in Created state yet, wait for it") + chatRoom.addListener(chatRoomListener) + } + } else { + val id = LinphoneUtils.getChatRoomId(chatRoom) + Log.i("$TAG Chat room successfully created [$id]") + operationInProgress.postValue(false) + chatRoomCreatedEvent.postValue( + Event( + Pair( + chatRoom.localAddress.asStringUriOnly(), + chatRoom.peerAddress.asStringUriOnly() + ) + ) + ) + } + } else { + Log.e("$TAG Failed to create 1-1 chat room with [${remote.asStringUriOnly()}]!") + operationInProgress.postValue(false) + chatRoomCreationErrorEvent.postValue(Event("Error!")) // TODO FIXME: use translated string + } + } else { + Log.w( + "$TAG A 1-1 chat room between local account [${localAddress?.asStringUriOnly()}] and remote [${remote.asStringUriOnly()}] for given parameters already exists!" + ) + operationInProgress.postValue(false) + chatRoomCreatedEvent.postValue( + Event( + Pair( + existingChatRoom.localAddress.asStringUriOnly(), + existingChatRoom.peerAddress.asStringUriOnly() + ) + ) + ) + } + } + @UiThread fun updateGroupChatButtonVisibility() { coreContext.postOnCoreThread { core -> 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 609b2cca2..54c36aac8 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 @@ -200,6 +200,7 @@ class StartCallFragment : GenericFragment() { coreContext.postOnCoreThread { core -> val friend = model.friend if (friend == null) { + Log.i("$TAG Friend is null, starting call with [${model.address}]") coreContext.startCall(model.address) return@postOnCoreThread } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6a3283a38..1bba1ef72 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -34,6 +34,8 @@ https://weblate.linphone.org/ https://wiki.linphone.org/xwiki/wiki/public/view/Linphone/Third%20party%20components%20/#Hlinphone-android + Dummy subject + SIP address Display name Domain