From 91f13fe4073d2e0d12020c8a2527012df2878e8a Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 13 Feb 2026 11:42:27 +0100 Subject: [PATCH] Fixed UI when merging calls into local conference --- .../java/org/linphone/core/CoreContext.kt | 22 ++++--- .../java/org/linphone/ui/call/CallActivity.kt | 13 ++++ .../fragment/ActiveConferenceCallFragment.kt | 11 ---- .../viewmodel/ConferenceViewModel.kt | 15 +++-- .../ui/call/fragment/ActiveCallFragment.kt | 11 ---- .../ui/call/viewmodel/CallsViewModel.kt | 62 ++++++++++++------- .../ui/call/viewmodel/CurrentCallViewModel.kt | 43 ++++++++----- .../call_activity_other_calls_top_bar.xml | 8 +-- 8 files changed, 107 insertions(+), 78 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index fc7b47aab..420f5d0cf 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -1040,15 +1040,21 @@ class CoreContext @WorkerThread fun terminateCall(call: Call) { - if (call.dir == Call.Dir.Incoming && LinphoneUtils.isCallIncoming(call.state)) { - val reason = if (call.core.callsNb > 1) Reason.Busy else Reason.Declined - Log.i( - "$TAG Declining call [${call.remoteAddress.asStringUriOnly()}] with reason [$reason]" - ) - call.decline(reason) + val conference = call.conference + if (conference != null) { + Log.i("$TAG Terminating conference [${call.remoteAddress.asStringUriOnly()}]") + conference.terminate() } else { - Log.i("$TAG Terminating call [${call.remoteAddress.asStringUriOnly()}]") - call.terminate() + if (call.dir == Call.Dir.Incoming && LinphoneUtils.isCallIncoming(call.state)) { + val reason = if (call.core.callsNb > 1) Reason.Busy else Reason.Declined + Log.i( + "$TAG Declining call [${call.remoteAddress.asStringUriOnly()}] with reason [$reason]" + ) + call.decline(reason) + } else { + Log.i("$TAG Terminating call [${call.remoteAddress.asStringUriOnly()}]") + call.terminate() + } } } diff --git a/app/src/main/java/org/linphone/ui/call/CallActivity.kt b/app/src/main/java/org/linphone/ui/call/CallActivity.kt index 3dc732406..56ff88922 100644 --- a/app/src/main/java/org/linphone/ui/call/CallActivity.kt +++ b/app/src/main/java/org/linphone/ui/call/CallActivity.kt @@ -43,6 +43,7 @@ import androidx.databinding.DataBindingUtil import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.navigation.findNavController +import androidx.navigation.fragment.findNavController import androidx.window.layout.FoldingFeature import androidx.window.layout.WindowInfoTracker import androidx.window.layout.WindowLayoutInfo @@ -264,6 +265,18 @@ class CallActivity : GenericActivity() { coreContext.enableProximitySensor(enabled) } + callViewModel.goToCallEvent.observe(this) { + it.consume { + navigateToActiveCall(true) + } + } + + callViewModel.goToConferenceEvent.observe(this) { + it.consume { + navigateToActiveCall(false) + } + } + callsViewModel.showIncomingCallEvent.observe(this) { it.consume { val action = IncomingCallFragmentDirections.actionGlobalIncomingCallFragment() diff --git a/app/src/main/java/org/linphone/ui/call/conference/fragment/ActiveConferenceCallFragment.kt b/app/src/main/java/org/linphone/ui/call/conference/fragment/ActiveConferenceCallFragment.kt index 3b2973924..58e173935 100644 --- a/app/src/main/java/org/linphone/ui/call/conference/fragment/ActiveConferenceCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/conference/fragment/ActiveConferenceCallFragment.kt @@ -237,17 +237,6 @@ class ActiveConferenceCallFragment : GenericCallFragment() { } } - callViewModel.goToCallEvent.observe(viewLifecycleOwner) { - it.consume { - if (findNavController().currentDestination?.id == R.id.activeConferenceCallFragment) { - Log.i("$TAG Going to active call fragment") - val action = - ActiveConferenceCallFragmentDirections.actionActiveConferenceCallFragmentToActiveCallFragment() - findNavController().navigate(action) - } - } - } - binding.setBackClickListener { (requireActivity() as CallActivity).goToMainActivity() } diff --git a/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt b/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt index 16cbe2ff6..5c1272cce 100644 --- a/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt @@ -98,6 +98,8 @@ class ConferenceViewModel MutableLiveData() } + var conferenceConfigured = false + private lateinit var conference: Conference private val conferenceListener = object : ConferenceListenerStub() { @@ -289,6 +291,7 @@ class ConferenceViewModel } init { + conferenceConfigured = false isPaused.value = false isConversationAvailable.value = false isMeParticipantSendingVideo.value = false @@ -297,6 +300,7 @@ class ConferenceViewModel @WorkerThread fun destroy() { + conferenceConfigured = false isCurrentCallInConference.postValue(false) if (::conference.isInitialized) { conference.removeListener(conferenceListener) @@ -314,6 +318,7 @@ class ConferenceViewModel isCurrentCallInConference.postValue(true) conference = conf conference.addListener(conferenceListener) + conferenceConfigured = true val isIn = conference.isIn val state = conf.state @@ -351,15 +356,15 @@ class ConferenceViewModel Log.w( "$TAG Conference has a participant sharing its screen, changing layout from mosaic to active speaker" ) - setNewLayout(ACTIVE_SPEAKER_LAYOUT) + setNewLayout(ACTIVE_SPEAKER_LAYOUT, call) } else if (currentLayout == AUDIO_ONLY_LAYOUT) { val defaultLayout = call.core.defaultConferenceLayout.toInt() if (defaultLayout == Conference.Layout.ActiveSpeaker.toInt()) { Log.w("$TAG Joined conference in audio only layout, switching to active speaker layout") - setNewLayout(ACTIVE_SPEAKER_LAYOUT) + setNewLayout(ACTIVE_SPEAKER_LAYOUT, call) } else { Log.w("$TAG Joined conference in audio only layout, switching to grid layout") - setNewLayout(GRID_LAYOUT) + setNewLayout(GRID_LAYOUT, call) } } } @@ -461,9 +466,9 @@ class ConferenceViewModel } @WorkerThread - fun setNewLayout(newLayout: Int) { + fun setNewLayout(newLayout: Int, currentCall: Call? = null) { if (::conference.isInitialized) { - val call = conference.call + val call = conference.call ?: currentCall if (call != null) { val params = call.core.createCallParams(call) if (params != null) { diff --git a/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt index ead2822ca..dc1933d07 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt @@ -331,17 +331,6 @@ class ActiveCallFragment : GenericCallFragment() { } } - callViewModel.goToConferenceEvent.observe(viewLifecycleOwner) { - it.consume { - if (findNavController().currentDestination?.id == R.id.activeCallFragment) { - Log.i("$TAG Going to conference fragment") - val action = - ActiveCallFragmentDirections.actionActiveCallFragmentToActiveConferenceCallFragment() - findNavController().navigate(action) - } - } - } - callViewModel.isReceivingVideo.observe(viewLifecycleOwner) { receiving -> if (!receiving && callViewModel.fullScreenMode.value == true) { Log.i("$TAG We are no longer receiving video, leaving full screen mode") diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt index cde233826..bd58e5cdb 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt @@ -47,6 +47,8 @@ class CallsViewModel val callsCount = MutableLiveData() + val allCallsIntoConference = MutableLiveData() + val showTopBar = MutableLiveData() val goToActiveCallEvent = MutableLiveData>() @@ -234,6 +236,7 @@ class CallsViewModel showRedToast(R.string.conference_failed_to_merge_calls_into_conference_toast, R.drawable.warning_circle) } else { conference.addParticipants(core.calls) + allCallsIntoConference.postValue(true) } } } @@ -251,9 +254,16 @@ class CallsViewModel } callsExceptCurrentOne.postValue(list) - if (core.callsNb > 1) { - showTopBar.postValue(true) - if (core.callsNb == 2) { + val callsCount = core.callsNb + if (callsCount > 1) { + val callsNotInConference = core.calls.filter { + it.conference == null + } + val callsNotInConferenceCount = callsNotInConference.count() + Log.i("$TAG Found [$callsNotInConferenceCount] calls not in conference over [$callsCount] calls") + allCallsIntoConference.postValue(callsNotInConferenceCount == 0) + + if (callsNotInConferenceCount == 1) { val found = core.calls.find { it.state == Call.State.Paused } @@ -273,33 +283,37 @@ class CallsViewModel } callsTopBarStatus.postValue(LinphoneUtils.callStateToString(found.state)) } else { - Log.e("$TAG Failed to find a paused call") + Log.w("$TAG Failed to find a paused call") } - } else { + } else if (callsNotInConferenceCount > 1) { callsTopBarLabel.postValue( AppUtils.getFormattedString(R.string.calls_paused_count_label, core.callsNb - 1) ) callsTopBarStatus.postValue("") // TODO: improve ? + } else { + configureTopBarForSingleCallOrConference() } - } else { - if (core.callsNb == 1) { - callsTopBarIcon.postValue(R.drawable.phone) - - val call = core.calls.first() - val conference = call.conference - if (conference != null) { - callsTopBarLabel.postValue(conference.subject) - } else { - val remoteAddress = call.callLog.remoteAddress - val contact = coreContext.contactsManager.findContactByAddress( - remoteAddress - ) - callsTopBarLabel.postValue( - contact?.name ?: LinphoneUtils.getDisplayName(remoteAddress) - ) - } - callsTopBarStatus.postValue(LinphoneUtils.callStateToString(call.state)) - } + } else if (core.callsNb == 1) { + configureTopBarForSingleCallOrConference() } } + + private fun configureTopBarForSingleCallOrConference() { + callsTopBarIcon.postValue(R.drawable.phone) + + val call = coreContext.core.calls.first() + val conference = call.conference + if (conference != null) { + callsTopBarLabel.postValue(conference.subject) + } else { + val remoteAddress = call.callLog.remoteAddress + val contact = coreContext.contactsManager.findContactByAddress( + remoteAddress + ) + callsTopBarLabel.postValue( + contact?.name ?: LinphoneUtils.getDisplayName(remoteAddress) + ) + } + callsTopBarStatus.postValue(LinphoneUtils.callStateToString(call.state)) + } } diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index 35af7fb3e..b92378136 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -392,6 +392,13 @@ class CurrentCallViewModel } else -> {} } + + if (call.conference != null && !conferenceModel.conferenceConfigured) { + Log.i("$TAG Found conference on call but not conference model, initializing it now") + conferenceModel.configureFromCall(call) + updateMicrophoneMutedIcon() + goToConferenceEvent.postValue(Event(true)) + } } } @@ -695,21 +702,7 @@ class CurrentCallViewModel @UiThread fun refreshMicrophoneState() { coreContext.postOnCoreThread { - if (::currentCall.isInitialized) { - val micMuted = if (currentCall.conference != null) { - currentCall.conference?.microphoneMuted == true - } else { - currentCall.microphoneMuted - } - if (micMuted != isMicrophoneMuted.value) { - if (micMuted) { - Log.w("$TAG Microphone is muted, updating button state accordingly") - } else { - Log.i("$TAG Microphone is not muted, updating button state accordingly") - } - isMicrophoneMuted.postValue(micMuted) - } - } + updateMicrophoneMutedIcon() } } @@ -1097,6 +1090,7 @@ class CurrentCallViewModel conferenceModel.configureFromCall(call) goToConferenceEvent.postValue(Event(true)) } else { + Log.i("$TAG No conference attached to this call, going to call fragment") conferenceModel.destroy() goToCallEvent.postValue(Event(true)) } @@ -1262,6 +1256,25 @@ class CurrentCallViewModel } } + @WorkerThread + private fun updateMicrophoneMutedIcon() { + if (::currentCall.isInitialized) { + val micMuted = if (currentCall.conference != null) { + currentCall.conference?.microphoneMuted == true + } else { + currentCall.microphoneMuted + } + if (micMuted != isMicrophoneMuted.value) { + if (micMuted) { + Log.w("$TAG Microphone is muted, updating button state accordingly") + } else { + Log.i("$TAG Microphone is not muted, updating button state accordingly") + } + isMicrophoneMuted.postValue(micMuted) + } + } + } + @WorkerThread private fun updateOutputAudioDevice(audioDevice: AudioDevice?) { Log.i("$TAG Output audio device updated to [${audioDevice?.deviceName} (${audioDevice?.type})]") diff --git a/app/src/main/res/layout/call_activity_other_calls_top_bar.xml b/app/src/main/res/layout/call_activity_other_calls_top_bar.xml index c4cded125..af0093fe9 100644 --- a/app/src/main/res/layout/call_activity_other_calls_top_bar.xml +++ b/app/src/main/res/layout/call_activity_other_calls_top_bar.xml @@ -13,7 +13,7 @@