diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 7fd5072db..50757afa2 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -119,7 +119,20 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C ) { Log.i("$TAG Call [${call.remoteAddress.asStringUriOnly()}] state changed [$state]") when (state) { - Call.State.OutgoingProgress, Call.State.Connected -> { + Call.State.OutgoingProgress -> { + val conferenceInfo = core.findConferenceInformationFromUri(call.remoteAddress) + // Do not show outgoing call view for conference calls, wait for connected state + if (conferenceInfo == null) { + postOnMainThread { + showCallActivity() + } + } else { + Log.i( + "$TAG Call peer address matches known conference, delaying in-call UI until Connected state" + ) + } + } + Call.State.Connected -> { postOnMainThread { showCallActivity() } 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 3f373fa93..93ff18acd 100644 --- a/app/src/main/java/org/linphone/ui/call/CallActivity.kt +++ b/app/src/main/java/org/linphone/ui/call/CallActivity.kt @@ -34,7 +34,6 @@ 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 @@ -166,7 +165,7 @@ class CallActivity : AppCompatActivity() { } } - callViewModel.goTEndedCallEvent.observe(this) { + callViewModel.goToEndedCallEvent.observe(this) { it.consume { val action = ActiveCallFragmentDirections.actionGlobalEndedCallFragment() findNavController(R.id.call_nav_container).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 928a31d69..8e3e61fdd 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 @@ -251,6 +251,14 @@ class ActiveCallFragment : GenericCallFragment() { } } + callViewModel.goToConferenceEvent.observe(viewLifecycleOwner) { + it.consume { + Log.i("$TAG Going to conference fragment") + val action = ActiveCallFragmentDirections.actionActiveCallFragmentToActiveConferenceCallFragment() + findNavController().navigate(action) + } + } + actionsBottomSheetBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { override fun onStateChanged(bottomSheet: View, newState: Int) { when (newState) { diff --git a/app/src/main/java/org/linphone/ui/call/fragment/ActiveConferenceCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/ActiveConferenceCallFragment.kt new file mode 100644 index 000000000..1dc205f85 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/call/fragment/ActiveConferenceCallFragment.kt @@ -0,0 +1,74 @@ +/* + * 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.call.fragment + +import android.os.Bundle +import android.os.SystemClock +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.ViewModelProvider +import org.linphone.databinding.CallActiveConferenceFragmentBinding +import org.linphone.ui.call.viewmodel.CallsViewModel +import org.linphone.ui.call.viewmodel.CurrentCallViewModel + +class ActiveConferenceCallFragment : GenericCallFragment() { + companion object { + private const val TAG = "[Active Conference Call Fragment]" + } + + private lateinit var binding: CallActiveConferenceFragmentBinding + + private lateinit var callViewModel: CurrentCallViewModel + + private lateinit var callsViewModel: CallsViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = CallActiveConferenceFragmentBinding.inflate(layoutInflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + callViewModel = requireActivity().run { + ViewModelProvider(this)[CurrentCallViewModel::class.java] + } + + callsViewModel = requireActivity().run { + ViewModelProvider(this)[CallsViewModel::class.java] + } + + binding.lifecycleOwner = viewLifecycleOwner + binding.viewModel = callViewModel + binding.conferenceViewModel = callViewModel.conferenceModel + binding.callsViewModel = callsViewModel + binding.numpadModel = callViewModel.numpadModel + + callViewModel.callDuration.observe(viewLifecycleOwner) { duration -> + binding.chronometer.base = SystemClock.elapsedRealtime() - (1000 * duration) + binding.chronometer.start() + } + } +} diff --git a/app/src/main/java/org/linphone/ui/call/model/ConferenceModel.kt b/app/src/main/java/org/linphone/ui/call/model/ConferenceModel.kt new file mode 100644 index 000000000..145af8603 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/call/model/ConferenceModel.kt @@ -0,0 +1,47 @@ +/* + * 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.call.model + +import androidx.annotation.WorkerThread +import androidx.lifecycle.MutableLiveData +import org.linphone.core.Call +import org.linphone.core.Conference +import org.linphone.core.tools.Log + +class ConferenceModel { + companion object { + private const val TAG = "[Conference ViewModel]" + } + + val subject = MutableLiveData() + + private lateinit var conference: Conference + + @WorkerThread + fun configureFromCall(call: Call) { + val conf = call.conference ?: return + conference = conf + + Log.i( + "$TAG Configuring conference with subject [${conference.subject}] from call [${call.callLog.callId}]" + ) + subject.postValue(conference.subject) + } +} 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 dfd4a8601..46429b508 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 @@ -43,6 +43,7 @@ import org.linphone.core.MediaDirection import org.linphone.core.MediaEncryption import org.linphone.core.tools.Log import org.linphone.ui.call.model.AudioDeviceModel +import org.linphone.ui.call.model.ConferenceModel import org.linphone.ui.main.contacts.model.ContactAvatarModel import org.linphone.ui.main.history.model.NumpadModel import org.linphone.utils.AppUtils @@ -110,7 +111,7 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() { MutableLiveData>() } - val goTEndedCallEvent: MutableLiveData> by lazy { + val goToEndedCallEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -129,6 +130,14 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() { MutableLiveData>>() } + // Conference + + val conferenceModel = ConferenceModel() + + val goToConferenceEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + // Extras actions val isActionsMenuExpanded = MutableLiveData() @@ -201,13 +210,13 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() { Log.e( "$TAG Failed to get a valid call to display, go to ended call fragment" ) - goTEndedCallEvent.postValue(Event(true)) + goToEndedCallEvent.postValue(Event(true)) } } else { 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 - goTEndedCallEvent.postValue(Event(true)) + goToEndedCallEvent.postValue(Event(true)) } } else { val videoEnabled = call.currentParams.isVideoEnabled @@ -224,6 +233,12 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() { Log.w("$TAG Video is not longer enabled, leaving full screen mode") fullScreenMode.postValue(false) } + + if (call.state == Call.State.Connected && call.conference != null) { + Log.i("$TAG Call is in Connected state and conference isn't null") + conferenceModel.configureFromCall(call) + goToConferenceEvent.postValue(Event(true)) + } } isPaused.postValue(isCallPaused()) @@ -719,6 +734,11 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() { currentCall = call call.addListener(callListener) + if (call.conference != null) { + conferenceModel.configureFromCall(call) + goToConferenceEvent.postValue(Event(true)) + } + if (call.dir == Call.Dir.Incoming) { if (call.core.accountList.size > 1) { val displayName = LinphoneUtils.getDisplayName(call.toAddress) diff --git a/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingWaitingRoomFragment.kt b/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingWaitingRoomFragment.kt index bf3911337..402cd0608 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingWaitingRoomFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingWaitingRoomFragment.kt @@ -109,6 +109,13 @@ class MeetingWaitingRoomFragment : GenericFragment() { } } + viewModel.conferenceCreatedEvent.observe(viewLifecycleOwner) { + it.consume { + Log.i("$TAG Conference was joined, leaving waiting room") + goBack() + } + } + if (!isCameraPermissionGranted()) { viewModel.isVideoAvailable.value = false Log.w("$TAG Camera permission wasn't granted yet, asking for it now") 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 d9b06697b..55552974f 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 @@ -24,7 +24,10 @@ import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Conference import org.linphone.core.ConferenceInfo +import org.linphone.core.Core +import org.linphone.core.CoreListenerStub import org.linphone.core.Factory import org.linphone.core.tools.Log import org.linphone.ui.main.contacts.model.ContactAvatarModel @@ -53,11 +56,38 @@ class MeetingWaitingRoomViewModel @UiThread constructor() : ViewModel() { val conferenceInfoFoundEvent = MutableLiveData>() + val conferenceCreatedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + private lateinit var conferenceInfo: ConferenceInfo + private val coreListener = object : CoreListenerStub() { + override fun onConferenceStateChanged( + core: Core, + conference: Conference, + state: Conference.State? + ) { + Log.i("$TAG Conference state changed: [$state]") + if (conference.state == Conference.State.Created) { + conferenceCreatedEvent.postValue(Event(true)) + } + } + } + + init { + coreContext.postOnCoreThread { core -> + core.addListener(coreListener) + } + } + @UiThread override fun onCleared() { super.onCleared() + + coreContext.postOnCoreThread { core -> + core.removeListener(coreListener) + } } @UiThread diff --git a/app/src/main/res/layout/call_active_conference_fragment.xml b/app/src/main/res/layout/call_active_conference_fragment.xml new file mode 100644 index 000000000..d1c9f8fc1 --- /dev/null +++ b/app/src/main/res/layout/call_active_conference_fragment.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/call_nav_graph.xml b/app/src/main/res/navigation/call_nav_graph.xml index 3478a670f..870f803b4 100644 --- a/app/src/main/res/navigation/call_nav_graph.xml +++ b/app/src/main/res/navigation/call_nav_graph.xml @@ -66,6 +66,12 @@ app:enterAnim="@anim/slide_in" app:popExitAnim="@anim/slide_out" app:launchSingleTop="true" /> + + + \ 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 c243b4e2d..f61588164 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -427,6 +427,8 @@ %s calls %s paused calls + Waiting for other participants… + Account connection error