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 93ff18acd..e5b37544a 100644 --- a/app/src/main/java/org/linphone/ui/call/CallActivity.kt +++ b/app/src/main/java/org/linphone/ui/call/CallActivity.kt @@ -44,6 +44,7 @@ import org.linphone.R import org.linphone.core.tools.Log import org.linphone.databinding.CallActivityBinding import org.linphone.ui.call.fragment.ActiveCallFragmentDirections +import org.linphone.ui.call.fragment.ActiveConferenceCallFragmentDirections import org.linphone.ui.call.fragment.AudioDevicesMenuDialogFragment import org.linphone.ui.call.fragment.IncomingCallFragmentDirections import org.linphone.ui.call.fragment.OutgoingCallFragmentDirections @@ -187,17 +188,50 @@ class CallActivity : AppCompatActivity() { } callsViewModel.goToActiveCallEvent.observe(this) { - it.consume { + it.consume { singleCall -> val navController = findNavController(R.id.call_nav_container) val action = when (navController.currentDestination?.id) { R.id.outgoingCallFragment -> { - OutgoingCallFragmentDirections.actionOutgoingCallFragmentToActiveCallFragment() + if (singleCall) { + Log.i("$TAG Going from outgoing call fragment to call fragment") + OutgoingCallFragmentDirections.actionOutgoingCallFragmentToActiveCallFragment() + } else { + Log.i( + "$TAG Going from outgoing call fragment to conference call fragment" + ) + OutgoingCallFragmentDirections.actionOutgoingCallFragmentToActiveConferenceCallFragment() + } } R.id.incomingCallFragment -> { - IncomingCallFragmentDirections.actionIncomingCallFragmentToActiveCallFragment() + if (singleCall) { + Log.i("$TAG Going from incoming call fragment to call fragment") + IncomingCallFragmentDirections.actionIncomingCallFragmentToActiveCallFragment() + } else { + Log.i( + "$TAG Going from incoming call fragment to conference call fragment" + ) + IncomingCallFragmentDirections.actionIncomingCallFragmentToActiveConferenceCallFragment() + } + } + R.id.activeConferenceCallFragment -> { + if (singleCall) { + Log.i("$TAG Going from conference call fragment to call fragment") + ActiveConferenceCallFragmentDirections.actionActiveConferenceCallFragmentToActiveCallFragment() + } else { + Log.i( + "$TAG Going from conference call fragment to conference call fragment" + ) + ActiveConferenceCallFragmentDirections.actionGlobalActiveConferenceCallFragment() + } } else -> { - ActiveCallFragmentDirections.actionGlobalActiveCallFragment() + if (singleCall) { + Log.i("$TAG Going from call fragment to call fragment") + ActiveCallFragmentDirections.actionGlobalActiveCallFragment() + } else { + Log.i("$TAG Going from call fragment to conference call fragment") + ActiveCallFragmentDirections.actionActiveCallFragmentToActiveConferenceCallFragment() + } } } navController.navigate(action) 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 fb4104f46..d4f85304a 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 @@ -253,9 +253,12 @@ class ActiveCallFragment : GenericCallFragment() { callViewModel.goToConferenceEvent.observe(viewLifecycleOwner) { it.consume { - Log.i("$TAG Going to conference fragment") - val action = ActiveCallFragmentDirections.actionActiveCallFragmentToActiveConferenceCallFragment() - findNavController().navigate(action) + if (findNavController().currentDestination?.id == R.id.activeCallFragment) { + Log.i("$TAG Going to conference fragment") + val action = + ActiveCallFragmentDirections.actionActiveCallFragmentToActiveConferenceCallFragment() + findNavController().navigate(action) + } } } diff --git a/app/src/main/java/org/linphone/ui/call/model/ConferenceParticipantDeviceModel.kt b/app/src/main/java/org/linphone/ui/call/model/ConferenceParticipantDeviceModel.kt index b4ba8a624..37daf1261 100644 --- a/app/src/main/java/org/linphone/ui/call/model/ConferenceParticipantDeviceModel.kt +++ b/app/src/main/java/org/linphone/ui/call/model/ConferenceParticipantDeviceModel.kt @@ -19,11 +19,15 @@ */ package org.linphone.ui.call.model +import android.view.TextureView +import androidx.annotation.UiThread import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.MediaDirection import org.linphone.core.ParticipantDevice import org.linphone.core.ParticipantDeviceListenerStub +import org.linphone.core.StreamType import org.linphone.core.tools.Log class ConferenceParticipantDeviceModel @WorkerThread constructor( @@ -40,7 +44,14 @@ class ConferenceParticipantDeviceModel @WorkerThread constructor( val isSpeaking = MutableLiveData() + val isVideoAvailable = MutableLiveData() + + val isSendingVideo = MutableLiveData() + + private lateinit var textureView: TextureView + private val deviceListener = object : ParticipantDeviceListenerStub() { + @WorkerThread override fun onStateChanged( participantDevice: ParticipantDevice, state: ParticipantDevice.State? @@ -50,6 +61,7 @@ class ConferenceParticipantDeviceModel @WorkerThread constructor( ) } + @WorkerThread override fun onIsMuted(participantDevice: ParticipantDevice, muted: Boolean) { Log.i( "$TAG Participant device [${participantDevice.address.asStringUriOnly()}] is ${if (participantDevice.isMuted) "muted" else "no longer muted"}" @@ -57,6 +69,7 @@ class ConferenceParticipantDeviceModel @WorkerThread constructor( isMuted.postValue(participantDevice.isMuted) } + @WorkerThread override fun onIsSpeakingChanged( participantDevice: ParticipantDevice, speaking: Boolean @@ -66,6 +79,41 @@ class ConferenceParticipantDeviceModel @WorkerThread constructor( ) isSpeaking.postValue(participantDevice.isSpeaking) } + + @WorkerThread + override fun onStreamAvailabilityChanged( + participantDevice: ParticipantDevice, + available: Boolean, + streamType: StreamType? + ) { + Log.i( + "$TAG Participant device [${participantDevice.address.asStringUriOnly()}] stream [$streamType] availability changed to ${if (available) "available" else "not available"}" + ) + if (streamType == StreamType.Video) { + val available = participantDevice.getStreamAvailability(StreamType.Video) + isVideoAvailable.postValue(available) + if (available) { + updateWindowId(textureView) + } + } + } + + @WorkerThread + override fun onStreamCapabilityChanged( + participantDevice: ParticipantDevice, + direction: MediaDirection?, + streamType: StreamType? + ) { + Log.i( + "$TAG Participant device [${participantDevice.address.asStringUriOnly()}] stream [$streamType] capability changed to [$direction]" + ) + if (streamType == StreamType.Video) { + val videoCapability = participantDevice.getStreamCapability(StreamType.Video) + isSendingVideo.postValue( + videoCapability == MediaDirection.SendRecv || videoCapability == MediaDirection.SendOnly + ) + } + } } init { @@ -76,10 +124,35 @@ class ConferenceParticipantDeviceModel @WorkerThread constructor( Log.i( "$TAG Participant [${device.address.asStringUriOnly()}] is in state [${device.state}]" ) + + isVideoAvailable.postValue(device.getStreamAvailability(StreamType.Video)) + val videoCapability = device.getStreamCapability(StreamType.Video) + isSendingVideo.postValue( + videoCapability == MediaDirection.SendRecv || videoCapability == MediaDirection.SendOnly + ) } @WorkerThread fun destroy() { device.removeListener(deviceListener) } + + @UiThread + fun setTextureView(view: TextureView) { + Log.i( + "$TAG TextureView for participant [${device.address.asStringUriOnly()}] available from UI [$view]" + ) + textureView = view + coreContext.postOnCoreThread { + updateWindowId(textureView) + } + } + + @WorkerThread + private fun updateWindowId(windowId: Any?) { + Log.i( + "$$TAG Setting participant [${device.address.asStringUriOnly()}] window ID [$windowId]" + ) + device.nativeVideoWindowId = windowId + } } 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 981f86db8..a666e2582 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 @@ -114,7 +114,7 @@ class CallsViewModel @UiThread constructor() : ViewModel() { ) when (call.state) { Call.State.Connected -> { - goToActiveCallEvent.postValue(Event(true)) + goToActiveCallEvent.postValue(Event(call.conference == null)) } else -> { } @@ -132,8 +132,15 @@ class CallsViewModel @UiThread constructor() : ViewModel() { Log.i("$TAG Asking activity to show incoming call fragment") showIncomingCallEvent.postValue(Event(true)) } else { - Log.i("$TAG Asking activity to show active call fragment") - goToActiveCallEvent.postValue(Event(true)) + if (newCurrentCall.conference == null) { + Log.i("$TAG Asking activity to show active call fragment") + goToActiveCallEvent.postValue(Event(true)) + } else { + Log.i( + "$TAG Asking activity to show active conference call fragment" + ) + goToActiveCallEvent.postValue(Event(false)) + } } } } @@ -160,7 +167,7 @@ class CallsViewModel @UiThread constructor() : ViewModel() { when (currentCall.state) { Call.State.Connected, Call.State.StreamsRunning, Call.State.Paused, Call.State.Pausing, Call.State.PausedByRemote, Call.State.UpdatedByRemote, Call.State.Updating -> { - goToActiveCallEvent.postValue(Event(true)) + goToActiveCallEvent.postValue(Event(currentCall.conference == null)) } Call.State.OutgoingInit, Call.State.OutgoingRinging, Call.State.OutgoingProgress, Call.State.OutgoingEarlyMedia -> { showOutgoingCallEvent.postValue(Event(true)) 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 2cc491ba2..9325d31db 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 @@ -210,9 +210,11 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() { Log.e( "$TAG Failed to get a valid call to display, go to ended call fragment" ) + updateCallDuration() goToEndedCallEvent.postValue(Event(true)) } } else { + updateCallDuration() Log.i("$TAG Call is ending, go to ended call fragment") // Show that call was ended for a few seconds, then leave // TODO FIXME: do not show it when call is being ended due to user terminating the call diff --git a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingWaitingRoomViewModel.kt b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingWaitingRoomViewModel.kt index 99c20b13b..a3c8331b8 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingWaitingRoomViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingWaitingRoomViewModel.kt @@ -29,6 +29,7 @@ import org.linphone.core.ConferenceInfo import org.linphone.core.Core import org.linphone.core.CoreListenerStub import org.linphone.core.Factory +import org.linphone.core.MediaDirection import org.linphone.core.tools.Log import org.linphone.ui.main.contacts.model.ContactAvatarModel import org.linphone.utils.Event @@ -135,6 +136,10 @@ class MeetingWaitingRoomViewModel @UiThread constructor() : ViewModel() { fun join() { coreContext.postOnCoreThread { core -> if (::conferenceInfo.isInitialized) { + Log.i("$TAG Stopping video preview") + core.nativePreviewWindowId = null + core.isVideoPreviewEnabled = false + val conferenceUri = conferenceInfo.uri if (conferenceUri == null) { Log.e("$TAG Conference Info doesn't have a conference SIP URI to call!") @@ -144,7 +149,8 @@ class MeetingWaitingRoomViewModel @UiThread constructor() : ViewModel() { val params = core.createCallParams(null) params ?: return@postOnCoreThread - params.isVideoEnabled = isVideoEnabled.value == true + params.isVideoEnabled = true + params.videoDirection = if (isVideoEnabled.value == true) MediaDirection.SendRecv else MediaDirection.RecvOnly params.isMicEnabled = isMicrophoneMuted.value == false params.account = core.defaultAccount coreContext.startCall(conferenceUri, params) diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index c9beb9c7d..0bee54b12 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -25,6 +25,7 @@ import android.graphics.PorterDuff import android.text.Editable import android.text.TextWatcher import android.view.LayoutInflater +import android.view.TextureView import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo @@ -62,6 +63,7 @@ import org.linphone.contacts.AvatarGenerator import org.linphone.core.ChatRoom import org.linphone.core.ConsolidatedPresence import org.linphone.core.tools.Log +import org.linphone.ui.call.model.ConferenceParticipantDeviceModel /** * This file contains all the data binding necessary for the app @@ -376,6 +378,15 @@ private suspend fun loadContactPictureWithCoil( } } +@UiThread +@BindingAdapter("participantTextureView") +fun setParticipantTextureView( + textureView: TextureView, + model: ConferenceParticipantDeviceModel +) { + model.setTextureView(textureView) +} + @UiThread @BindingAdapter("onValueChanged") fun AppCompatEditText.editTextSetting(lambda: () -> Unit) { diff --git a/app/src/main/res/layout/call_conference_grid_cell.xml b/app/src/main/res/layout/call_conference_grid_cell.xml index d9107e115..2750625df 100644 --- a/app/src/main/res/layout/call_conference_grid_cell.xml +++ b/app/src/main/res/layout/call_conference_grid_cell.xml @@ -50,9 +50,10 @@ android:id="@+id/participant_video_surface" android:layout_width="0dp" android:layout_height="0dp" - android:layout_margin="5dp" app:alignTopRight="false" app:displayMode="hybrid" + participantTextureView="@{model}" + android:visibility="@{model.isSendingVideo ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -66,7 +67,7 @@ android:layout_marginEnd="10dp" android:padding="2dp" android:src="@drawable/microphone_slash" - android:background="@drawable/circle_white_button_background" + android:background="@drawable/shape_circle_white_background" android:visibility="@{model.isMuted ? View.VISIBLE : View.GONE}" app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent" /> diff --git a/app/src/main/res/navigation/call_nav_graph.xml b/app/src/main/res/navigation/call_nav_graph.xml index 870f803b4..2a1b0a515 100644 --- a/app/src/main/res/navigation/call_nav_graph.xml +++ b/app/src/main/res/navigation/call_nav_graph.xml @@ -16,6 +16,12 @@ app:popUpTo="@id/outgoingCallFragment" app:popUpToInclusive="true" app:launchSingleTop="true"/> + + + tools:layout="@layout/call_active_conference_fragment"> + + + + \ No newline at end of file