From 6dea7c3fecfdfad40b1697c211b15a44bb5fc47d Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 26 Oct 2023 14:31:38 +0200 Subject: [PATCH] Create if necessary then go to 1-1 chat room when using message button from contact/history details --- .../chat/fragment/ConversationFragment.kt | 1 + .../chat/viewmodel/ConversationViewModel.kt | 4 +- .../viewmodel/StartConversationViewModel.kt | 18 +- .../main/contacts/fragment/ContactFragment.kt | 8 + .../contacts/viewmodel/ContactViewModel.kt | 148 ++- .../fragment/HistoryContactFragment.kt | 8 + .../viewmodel/ContactHistoryViewModel.kt | 155 ++- app/src/main/res/layout/contact_fragment.xml | 1020 +++++++++-------- .../res/layout/history_contact_fragment.xml | 427 +++---- 9 files changed, 1067 insertions(+), 722 deletions(-) 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 1a81bea00..464df834e 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 @@ -154,6 +154,7 @@ class ConversationFragment : GenericFragment() { (view.parent as? ViewGroup)?.doOnPreDraw { Log.e("$TAG Failed to find chat room, going back") goBack() + // TODO: show toast } } } diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt index 877d99c5a..868ab1532 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt @@ -168,7 +168,9 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { super.onCleared() coreContext.postOnCoreThread { - chatRoom.removeListener(chatRoomListener) + if (::chatRoom.isInitialized) { + chatRoom.removeListener(chatRoomListener) + } events.value.orEmpty().forEach(EventLogModel::destroy) } } 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 c16c9954e..96f4204d6 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 @@ -191,17 +191,27 @@ class StartConversationViewModel @UiThread constructor() : AddressSelectionViewM val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams() params.isGroupEnabled = false params.subject = AppUtils.getString(R.string.conversation_one_to_one_hidden_subject) + params.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default 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 + if (LinphoneUtils.isEndToEndEncryptedChatAvailable(core)) { + Log.i( + "$TAG Account is in interop mode but LIME is available, creating a E2E chat room" + ) + params.backend = ChatRoom.Backend.FlexisipChat + params.isEncryptionEnabled = true + } else { + Log.i( + "$TAG Account is in interop mode but LIME isn't available, 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()}]" diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt index 76fdfaa25..3008cc1ab 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt @@ -178,6 +178,14 @@ class ContactFragment : GenericFragment() { } } + viewModel.goToConversationEvent.observe(viewLifecycleOwner) { + it.consume { pair -> + Log.i("$TAG Going to conversation [${pair.first}][${pair.second}]") + sharedViewModel.showConversationEvent.value = Event(pair) + sharedViewModel.navigateToConversationsEvent.value = Event(true) + } + } + viewModel.vCardTerminatedEvent.observe(viewLifecycleOwner) { it.consume { pair -> val contactName = pair.first diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt index 0263a52a9..1fabd8719 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt @@ -29,8 +29,13 @@ import java.util.Locale import kotlinx.coroutines.launch import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.R import org.linphone.contacts.ContactsManager import org.linphone.contacts.getListOfSipAddressesAndPhoneNumbers +import org.linphone.core.Address +import org.linphone.core.ChatRoom +import org.linphone.core.ChatRoomListenerStub +import org.linphone.core.ChatRoomParams import org.linphone.core.Friend import org.linphone.core.tools.Log import org.linphone.ui.main.contacts.model.ContactAvatarModel @@ -38,8 +43,10 @@ import org.linphone.ui.main.contacts.model.ContactDeviceModel import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel import org.linphone.ui.main.model.isInSecureMode +import org.linphone.utils.AppUtils import org.linphone.utils.Event import org.linphone.utils.FileUtils +import org.linphone.utils.LinphoneUtils class ContactViewModel @UiThread constructor() : ViewModel() { companion object { @@ -70,6 +77,12 @@ class ContactViewModel @UiThread constructor() : ViewModel() { val videoCallDisabled = MutableLiveData() + val operationInProgress = MutableLiveData() + + val chatRoomCreationErrorEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + val showLongPressMenuForNumberOrAddressEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -86,6 +99,10 @@ class ContactViewModel @UiThread constructor() : ViewModel() { MutableLiveData>() } + val goToConversationEvent: MutableLiveData>> by lazy { + MutableLiveData>>() + } + val vCardTerminatedEvent: MutableLiveData>> by lazy { MutableLiveData>>() } @@ -107,6 +124,7 @@ class ContactViewModel @UiThread constructor() : ViewModel() { override fun onClicked(model: ContactNumberOrAddressModel) { val address = model.address if (model.isEnabled && address != null) { + // TODO FIXME: handle chat action & video call as well coreContext.postOnCoreThread { Log.i("$TAG Calling SIP address [${address.asStringUriOnly()}]") coreContext.startCall(address) @@ -139,6 +157,34 @@ class ContactViewModel @UiThread constructor() : ViewModel() { } } + private val chatRoomListener = object : ChatRoomListenerStub() { + @WorkerThread + override fun onStateChanged(chatRoom: ChatRoom, newState: ChatRoom.State?) { + val state = chatRoom.state + val id = LinphoneUtils.getChatRoomId(chatRoom) + Log.i("$TAG Chat room [$id] (${chatRoom.subject}) state changed: [$state]") + + if (state == ChatRoom.State.Created) { + Log.i("$TAG Chat room [$id] successfully created") + chatRoom.removeListener(this) + operationInProgress.postValue(false) + goToConversationEvent.postValue( + Event( + Pair( + chatRoom.localAddress.asStringUriOnly(), + chatRoom.peerAddress.asStringUriOnly() + ) + ) + ) + } else if (state == ChatRoom.State.CreationFailed) { + 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 lateinit var friend: Friend private var refKey: String = "" @@ -387,7 +433,7 @@ class ContactViewModel @UiThread constructor() : ViewModel() { } @UiThread - fun sendMessage() { + fun goToConversation() { coreContext.postOnCoreThread { core -> val addressesCount = friend.addresses.size val numbersCount = friend.phoneNumbers.size @@ -399,7 +445,7 @@ class ContactViewModel @UiThread constructor() : ViewModel() { Log.i( "$TAG Only 1 SIP address found for contact [${friend.name}], sending message directly" ) - // TODO: send message feature + goToConversation(friend.addresses.first()) } else if (addressesCount == 0 && numbersCount == 1 && enablePhoneNumbers) { val number = friend.phoneNumbers.first() val address = core.interpretUrl(number, true) @@ -407,7 +453,7 @@ class ContactViewModel @UiThread constructor() : ViewModel() { Log.i( "$TAG Only 1 phone number found for contact [${friend.name}], sending message directly" ) - // TODO: send message feature + goToConversation(address) } else { Log.e("$TAG Failed to interpret phone number [$number] as SIP address") } @@ -420,4 +466,100 @@ class ContactViewModel @UiThread constructor() : ViewModel() { } } } + + @WorkerThread + private fun goToConversation(remote: Address) { + val core = coreContext.core + val account = core.defaultAccount + val localSipUri = account?.params?.identityAddress?.asStringUriOnly() + if (!localSipUri.isNullOrEmpty()) { + val remoteSipUri = remote.asStringUriOnly() + Log.i("$TAG Looking for existing chat room between [$localSipUri] and [$remoteSipUri]") + + val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams() + params.isGroupEnabled = false + params.subject = AppUtils.getString(R.string.conversation_one_to_one_hidden_subject) + params.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default + + 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 + } else if (!account.isInSecureMode()) { + if (LinphoneUtils.isEndToEndEncryptedChatAvailable(core)) { + Log.i( + "$TAG Account is in interop mode but LIME is available, creating a E2E chat room" + ) + params.backend = ChatRoom.Backend.FlexisipChat + params.isEncryptionEnabled = true + } else { + Log.i( + "$TAG Account is in interop mode but LIME isn't available, 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()}]" + ) + return + } + + val participants = arrayOf(remote) + val localAddress = account.params.identityAddress + val existingChatRoom = core.searchChatRoom(params, localAddress, null, participants) + if (existingChatRoom != null) { + Log.i( + "$TAG Found existing chat room [${LinphoneUtils.getChatRoomId(existingChatRoom)}], going to it" + ) + goToConversationEvent.postValue( + Event(Pair(localSipUri, existingChatRoom.peerAddress.asStringUriOnly())) + ) + } else { + Log.i( + "$TAG No existing chat room between [$localSipUri] and [$remoteSipUri] was found, let's create it" + ) + operationInProgress.postValue(true) + 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) + goToConversationEvent.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) + goToConversationEvent.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 + } + } + } + } } diff --git a/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryContactFragment.kt b/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryContactFragment.kt index fe1b8752a..ee5588c7f 100644 --- a/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryContactFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryContactFragment.kt @@ -128,6 +128,14 @@ class HistoryContactFragment : GenericFragment() { goBack() // TODO FIXME : issue with tablet when pane can't be closed } } + + viewModel.goToConversationEvent.observe(viewLifecycleOwner) { + it.consume { pair -> + Log.i("$TAG Going to conversation [${pair.first}][${pair.second}]") + sharedViewModel.showConversationEvent.value = Event(pair) + sharedViewModel.navigateToConversationsEvent.value = Event(true) + } + } } private fun copyNumberOrAddressToClipboard(value: String) { diff --git a/app/src/main/java/org/linphone/ui/main/history/viewmodel/ContactHistoryViewModel.kt b/app/src/main/java/org/linphone/ui/main/history/viewmodel/ContactHistoryViewModel.kt index 97cc4ec15..7445a2863 100644 --- a/app/src/main/java/org/linphone/ui/main/history/viewmodel/ContactHistoryViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/history/viewmodel/ContactHistoryViewModel.kt @@ -1,18 +1,30 @@ package org.linphone.ui.main.history.viewmodel 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.core.Address import org.linphone.core.Call +import org.linphone.core.ChatRoom +import org.linphone.core.ChatRoomListenerStub +import org.linphone.core.ChatRoomParams +import org.linphone.core.tools.Log import org.linphone.ui.main.history.model.CallLogHistoryModel import org.linphone.ui.main.history.model.CallLogModel +import org.linphone.ui.main.model.isInSecureMode +import org.linphone.utils.AppUtils import org.linphone.utils.Event import org.linphone.utils.LinphoneUtils class ContactHistoryViewModel @UiThread constructor() : ViewModel() { + companion object { + private const val TAG = "[Contact History ViewModel]" + } + val showBackButton = MutableLiveData() val callLogModel = MutableLiveData() @@ -23,12 +35,50 @@ class ContactHistoryViewModel @UiThread constructor() : ViewModel() { val videoCallDisabled = MutableLiveData() + val operationInProgress = MutableLiveData() + + val chatRoomCreationErrorEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val goToConversationEvent: MutableLiveData>> by lazy { + MutableLiveData>>() + } + val historyDeletedEvent: MutableLiveData> by lazy { MutableLiveData>() } private lateinit var address: Address + private val chatRoomListener = object : ChatRoomListenerStub() { + @WorkerThread + override fun onStateChanged(chatRoom: ChatRoom, newState: ChatRoom.State?) { + val state = chatRoom.state + val id = LinphoneUtils.getChatRoomId(chatRoom) + Log.i("$TAG Chat room [$id] (${chatRoom.subject}) state changed: [$state]") + + if (state == ChatRoom.State.Created) { + Log.i("$TAG Chat room [$id] successfully created") + chatRoom.removeListener(this) + operationInProgress.postValue(false) + goToConversationEvent.postValue( + Event( + Pair( + chatRoom.localAddress.asStringUriOnly(), + chatRoom.peerAddress.asStringUriOnly() + ) + ) + ) + } else if (state == ChatRoom.State.CreationFailed) { + Log.e("$TAG Chat room [$id] creation has failed!") + chatRoom.removeListener(this) + operationInProgress.postValue(false) + chatRoomCreationErrorEvent.postValue(Event("Error!")) // TODO FIXME: use translated string + } + } + } + init { coreContext.postOnCoreThread { core -> chatDisabled.postValue(corePreferences.disableChat) @@ -92,7 +142,108 @@ class ContactHistoryViewModel @UiThread constructor() : ViewModel() { } @UiThread - fun sendMessage() { - // TODO: chat feature + fun goToConversation() { + coreContext.postOnCoreThread { core -> + val account = core.defaultAccount + val localSipUri = account?.params?.identityAddress?.asStringUriOnly() + if (!localSipUri.isNullOrEmpty()) { + val remote = address + val remoteSipUri = remote.asStringUriOnly() + Log.i( + "$TAG Looking for existing chat room between [$localSipUri] and [$remoteSipUri]" + ) + + val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams() + params.isGroupEnabled = false + params.subject = AppUtils.getString(R.string.conversation_one_to_one_hidden_subject) + params.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default + + 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 + } else if (!account.isInSecureMode()) { + if (LinphoneUtils.isEndToEndEncryptedChatAvailable(core)) { + Log.i( + "$TAG Account is in interop mode but LIME is available, creating a E2E chat room" + ) + params.backend = ChatRoom.Backend.FlexisipChat + params.isEncryptionEnabled = true + } else { + Log.i( + "$TAG Account is in interop mode but LIME isn't available, 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()}]" + ) + return@postOnCoreThread + } + + val participants = arrayOf(remote) + val localAddress = account.params.identityAddress + val existingChatRoom = core.searchChatRoom(params, localAddress, null, participants) + if (existingChatRoom != null) { + Log.i( + "$TAG Found existing chat room [${LinphoneUtils.getChatRoomId( + existingChatRoom + )}], going to it" + ) + goToConversationEvent.postValue( + Event(Pair(localSipUri, existingChatRoom.peerAddress.asStringUriOnly())) + ) + } else { + Log.i( + "$TAG No existing chat room between [$localSipUri] and [$remoteSipUri] was found, let's create it" + ) + operationInProgress.postValue(true) + 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) + goToConversationEvent.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) + goToConversationEvent.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 + } + } + } + } } } diff --git a/app/src/main/res/layout/contact_fragment.xml b/app/src/main/res/layout/contact_fragment.xml index 6f7f45721..8b4c75623 100644 --- a/app/src/main/res/layout/contact_fragment.xml +++ b/app/src/main/res/layout/contact_fragment.xml @@ -1,6 +1,7 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:bind="http://schemas.android.com/tools"> @@ -20,555 +21,566 @@ type="org.linphone.ui.main.contacts.viewmodel.ContactViewModel" /> - + android:layout_height="match_parent"> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/history_contact_fragment.xml b/app/src/main/res/layout/history_contact_fragment.xml index 6077fd2ef..9c64f9e93 100644 --- a/app/src/main/res/layout/history_contact_fragment.xml +++ b/app/src/main/res/layout/history_contact_fragment.xml @@ -1,6 +1,7 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:bind="http://schemas.android.com/tools"> @@ -17,231 +18,241 @@ type="org.linphone.ui.main.history.viewmodel.ContactHistoryViewModel" /> - + android:layout_height="match_parent"> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + \ No newline at end of file