diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index e660c91c9..779694aff 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -208,6 +208,14 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C return } + val currentCall = core.currentCall + if (currentCall != null) { + Log.w( + "$TAG Found current call [${currentCall.remoteAddress.asStringUriOnly()}], pausing it first" + ) + currentCall.pause() + } + val params = callParams ?: core.createCallParams(null) if (params == null) { val call = core.inviteAddress(address) diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractTopBarViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractTopBarViewModel.kt index 5ec42bc6a..0a8c9d61a 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractTopBarViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractTopBarViewModel.kt @@ -24,13 +24,11 @@ import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R import org.linphone.core.Call import org.linphone.core.Core import org.linphone.core.CoreListenerStub import org.linphone.core.tools.Log import org.linphone.ui.main.model.AccountModel -import org.linphone.utils.AppUtils import org.linphone.utils.Event import org.linphone.utils.LinphoneUtils @@ -169,7 +167,7 @@ open class AbstractTopBarViewModel @UiThread constructor() : ViewModel() { callDisplayName.postValue( contact?.name ?: LinphoneUtils.getDisplayName(currentCall.remoteAddress) ) - callStatus.postValue(callStateToString(currentCall.state)) + callStatus.postValue(LinphoneUtils.callStateToString(currentCall.state)) } else { val firstCall = core.calls.firstOrNull() if (firstCall != null) { @@ -179,38 +177,10 @@ open class AbstractTopBarViewModel @UiThread constructor() : ViewModel() { callDisplayName.postValue( contact?.name ?: LinphoneUtils.getDisplayName(firstCall.remoteAddress) ) - callStatus.postValue(callStateToString(firstCall.state)) + callStatus.postValue(LinphoneUtils.callStateToString(firstCall.state)) } } Log.i("$TAG At least a call, asking fragment to change status bar color") changeSystemTopBarColorToInCallEvent.postValue(Event(true)) } - - @WorkerThread - private fun callStateToString(state: Call.State): String { - return when (state) { - Call.State.IncomingEarlyMedia, Call.State.IncomingReceived -> { - AppUtils.getString(R.string.voip_call_state_incoming_received) - } - Call.State.OutgoingInit, Call.State.OutgoingProgress -> { - AppUtils.getString(R.string.voip_call_state_outgoing_progress) - } - Call.State.OutgoingRinging, Call.State.OutgoingEarlyMedia -> { - AppUtils.getString(R.string.voip_call_state_outgoing_ringing) - } - Call.State.Connected, Call.State.StreamsRunning, Call.State.Updating, Call.State.UpdatedByRemote -> { - AppUtils.getString(R.string.voip_call_state_connected) - } - Call.State.Pausing, Call.State.Paused, Call.State.PausedByRemote -> { - AppUtils.getString(R.string.voip_call_state_paused) - } - Call.State.End, Call.State.Released, Call.State.Error -> { - AppUtils.getString(R.string.voip_call_state_ended) - } - else -> { - // TODO: handle other states - "" - } - } - } } diff --git a/app/src/main/java/org/linphone/ui/voip/fragment/ActiveCallFragment.kt b/app/src/main/java/org/linphone/ui/voip/fragment/ActiveCallFragment.kt index cb285f645..ddd21718b 100644 --- a/app/src/main/java/org/linphone/ui/voip/fragment/ActiveCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/voip/fragment/ActiveCallFragment.kt @@ -118,6 +118,11 @@ class ActiveCallFragment : GenericCallFragment() { findNavController().navigate(action) } + binding.setCallsListClickListener { + val action = ActiveCallFragmentDirections.actionActiveCallFragmentToCallsListFragment() + findNavController().navigate(action) + } + sharedViewModel = requireActivity().run { ViewModelProvider(this)[SharedCallViewModel::class.java] } diff --git a/app/src/main/java/org/linphone/ui/voip/fragment/CallsListFragment.kt b/app/src/main/java/org/linphone/ui/voip/fragment/CallsListFragment.kt new file mode 100644 index 000000000..68065bb50 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/voip/fragment/CallsListFragment.kt @@ -0,0 +1,62 @@ +/* + * 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.voip.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import androidx.navigation.navGraphViewModels +import org.linphone.R +import org.linphone.databinding.VoipCallsListFragmentBinding +import org.linphone.ui.voip.viewmodel.CallsViewModel + +class CallsListFragment : GenericCallFragment() { + companion object { + private const val TAG = "[Calls List Fragment]" + } + + private lateinit var binding: VoipCallsListFragmentBinding + + private val viewModel: CallsViewModel by navGraphViewModels( + R.id.voip_nav_graph + ) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = VoipCallsListFragmentBinding.inflate(layoutInflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.lifecycleOwner = viewLifecycleOwner + binding.viewModel = viewModel + + binding.setBackClickListener { + findNavController().popBackStack() + } + } +} diff --git a/app/src/main/java/org/linphone/ui/voip/model/CallModel.kt b/app/src/main/java/org/linphone/ui/voip/model/CallModel.kt new file mode 100644 index 000000000..902ae262a --- /dev/null +++ b/app/src/main/java/org/linphone/ui/voip/model/CallModel.kt @@ -0,0 +1,64 @@ +/* + * 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.voip.model + +import androidx.annotation.WorkerThread +import androidx.lifecycle.MutableLiveData +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Call +import org.linphone.core.CallListenerStub +import org.linphone.ui.main.contacts.model.ContactAvatarModel +import org.linphone.utils.LinphoneUtils + +class CallModel @WorkerThread constructor(val call: Call) { + val displayName = MutableLiveData() + + val state = MutableLiveData() + + val isPaused = MutableLiveData() + + val friend = coreContext.contactsManager.findContactByAddress(call.remoteAddress) + + val contact = MutableLiveData() + + private val callListener = object : CallListenerStub() { + override fun onStateChanged(call: Call, state: Call.State, message: String) { + this@CallModel.state.postValue(LinphoneUtils.callStateToString(state)) + isPaused.postValue(LinphoneUtils.isCallPaused(state)) + } + } + + init { + call.addListener(callListener) + + displayName.postValue(friend?.name ?: LinphoneUtils.getDisplayName(call.remoteAddress)) + if (friend != null) { + contact.postValue(ContactAvatarModel(friend)) + } + + state.postValue(LinphoneUtils.callStateToString(call.state)) + isPaused.postValue(LinphoneUtils.isCallPaused(call.state)) + } + + @WorkerThread + fun destroy() { + call.removeListener(callListener) + } +} diff --git a/app/src/main/java/org/linphone/ui/voip/viewmodel/CallsViewModel.kt b/app/src/main/java/org/linphone/ui/voip/viewmodel/CallsViewModel.kt index a5063d875..11334ad7f 100644 --- a/app/src/main/java/org/linphone/ui/voip/viewmodel/CallsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/voip/viewmodel/CallsViewModel.kt @@ -30,6 +30,7 @@ import org.linphone.core.Call import org.linphone.core.Core import org.linphone.core.CoreListenerStub import org.linphone.core.tools.Log +import org.linphone.ui.voip.model.CallModel import org.linphone.utils.Event class CallsViewModel @UiThread constructor() : ViewModel() { @@ -41,6 +42,8 @@ class CallsViewModel @UiThread constructor() : ViewModel() { private const val ALERT_NETWORK_TYPE_CELLULAR = "mobile" } + val calls = MutableLiveData>() + val goToActiveCallEvent = MutableLiveData>() val showIncomingCallEvent = MutableLiveData>() @@ -94,6 +97,34 @@ class CallsViewModel @UiThread constructor() : ViewModel() { state: Call.State, message: String ) { + // Update calls list if needed + val found = calls.value.orEmpty().find { + it.call == call + } + if (found == null) { + if (state != Call.State.End && state != Call.State.Released && state != Call.State.Error) { + Log.i( + "$TAG Found a call [${call.remoteAddress.asStringUriOnly()}] not yet in calls list, let's add it" + ) + val list = arrayListOf() + list.addAll(calls.value.orEmpty()) + val model = CallModel(call) + list.add(model) + calls.postValue(list) + } + } else { + if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) { + Log.i( + "$TAG Call [${call.remoteAddress.asStringUriOnly()}] shouldn't be in calls list anymore, let's remove it" + ) + val list = arrayListOf() + list.addAll(calls.value.orEmpty()) + list.remove(found) + calls.postValue(list) + found.destroy() + } + } + if (call == core.currentCall || core.currentCall == null) { Log.i( "$TAG Current call [${call.remoteAddress.asStringUriOnly()}] state changed [$state]" @@ -139,6 +170,13 @@ class CallsViewModel @UiThread constructor() : ViewModel() { core.addListener(coreListener) if (core.callsNb > 0) { + val list = arrayListOf() + for (call in core.calls) { + val model = CallModel(call) + list.add(model) + } + calls.postValue(list) + val currentCall = core.currentCall ?: core.calls.first() when (currentCall.state) { @@ -165,6 +203,7 @@ class CallsViewModel @UiThread constructor() : ViewModel() { super.onCleared() coreContext.postOnCoreThread { core -> + calls.value.orEmpty().forEach(CallModel::destroy) core.removeListener(coreListener) } } diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index 26f15998a..558158c13 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -118,6 +118,14 @@ class LinphoneUtils { } } + @AnyThread + fun isCallPaused(callState: Call.State): Boolean { + return when (callState) { + Call.State.Pausing, Call.State.Paused, Call.State.PausedByRemote, Call.State.Resuming -> true + else -> false + } + } + @AnyThread @IntegerRes fun getIconResId(callStatus: Status, callDir: Dir): Int { @@ -189,5 +197,33 @@ class LinphoneUtils { remoteSipUri.clean() return "${localSipUri.asStringUriOnly()}~${remoteSipUri.asStringUriOnly()}" } + + @WorkerThread + fun callStateToString(state: Call.State): String { + return when (state) { + Call.State.IncomingEarlyMedia, Call.State.IncomingReceived -> { + AppUtils.getString(R.string.voip_call_state_incoming_received) + } + Call.State.OutgoingInit, Call.State.OutgoingProgress -> { + AppUtils.getString(R.string.voip_call_state_outgoing_progress) + } + Call.State.OutgoingRinging, Call.State.OutgoingEarlyMedia -> { + AppUtils.getString(R.string.voip_call_state_outgoing_ringing) + } + Call.State.Connected, Call.State.StreamsRunning, Call.State.Updating, Call.State.UpdatedByRemote -> { + AppUtils.getString(R.string.voip_call_state_connected) + } + Call.State.Pausing, Call.State.Paused, Call.State.PausedByRemote -> { + AppUtils.getString(R.string.voip_call_state_paused) + } + Call.State.End, Call.State.Released, Call.State.Error -> { + AppUtils.getString(R.string.voip_call_state_ended) + } + else -> { + // TODO: handle other states + "" + } + } + } } } diff --git a/app/src/main/res/drawable/shape_round_in_call_pressed_button_background.xml b/app/src/main/res/drawable/shape_round_in_call_pressed_button_background.xml index f64d098d4..2ce614341 100644 --- a/app/src/main/res/drawable/shape_round_in_call_pressed_button_background.xml +++ b/app/src/main/res/drawable/shape_round_in_call_pressed_button_background.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/account_profile_fragment.xml b/app/src/main/res/layout/account_profile_fragment.xml index 00424bd23..9ef9324a0 100644 --- a/app/src/main/res/layout/account_profile_fragment.xml +++ b/app/src/main/res/layout/account_profile_fragment.xml @@ -38,7 +38,7 @@ android:adjustViewBounds="true" android:padding="15dp" android:src="@drawable/caret_left" - app:tint="@color/gray_main2_500" + app:tint="@color/orange_main_500" app:layout_constraintBottom_toBottomOf="@id/title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/title" /> @@ -83,7 +83,7 @@ android:id="@+id/avatar" android:layout_width="@dimen/avatar_big_size" android:layout_height="@dimen/avatar_big_size" - android:layout_marginTop="21dp" + android:layout_marginTop="20dp" android:adjustViewBounds="true" android:background="@drawable/shape_circle_light_blue_background" accountAvatar="@{viewModel.accountModel}" diff --git a/app/src/main/res/layout/account_profile_secure_mode_fragment.xml b/app/src/main/res/layout/account_profile_secure_mode_fragment.xml index a2bef6980..2b53789d9 100644 --- a/app/src/main/res/layout/account_profile_secure_mode_fragment.xml +++ b/app/src/main/res/layout/account_profile_secure_mode_fragment.xml @@ -32,7 +32,7 @@ android:adjustViewBounds="true" android:padding="15dp" android:src="@drawable/caret_left" - app:tint="@color/gray_main2_500" + app:tint="@color/orange_main_500" app:layout_constraintBottom_toBottomOf="@id/title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/title" /> diff --git a/app/src/main/res/layout/call_fragment.xml b/app/src/main/res/layout/call_fragment.xml index c778e6e4b..65b538da7 100644 --- a/app/src/main/res/layout/call_fragment.xml +++ b/app/src/main/res/layout/call_fragment.xml @@ -31,7 +31,7 @@ android:onClick="@{backClickListener}" android:visibility="@{viewModel.showBackButton ? View.VISIBLE : View.GONE}" android:src="@drawable/caret_left" - app:tint="@color/gray_main2_500" + app:tint="@color/orange_main_500" app:layout_constraintBottom_toBottomOf="@id/title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/title"/> diff --git a/app/src/main/res/layout/call_start_fragment.xml b/app/src/main/res/layout/call_start_fragment.xml index e43ffac66..162783060 100644 --- a/app/src/main/res/layout/call_start_fragment.xml +++ b/app/src/main/res/layout/call_start_fragment.xml @@ -36,7 +36,7 @@ android:onClick="@{backClickListener}" android:padding="15dp" android:src="@drawable/caret_left" - app:tint="@color/gray_main2_500" + app:tint="@color/orange_main_500" app:layout_constraintBottom_toBottomOf="@id/title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/title" /> diff --git a/app/src/main/res/layout/contact_fragment.xml b/app/src/main/res/layout/contact_fragment.xml index 3a437950e..28308e001 100644 --- a/app/src/main/res/layout/contact_fragment.xml +++ b/app/src/main/res/layout/contact_fragment.xml @@ -34,7 +34,7 @@ android:onClick="@{backClickListener}" android:visibility="@{viewModel.showBackButton ? View.VISIBLE : View.GONE}" android:src="@drawable/caret_left" - app:tint="@color/gray_main2_500" + app:tint="@color/orange_main_500" app:layout_constraintBottom_toBottomOf="@id/invisible_title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/invisible_title"/> diff --git a/app/src/main/res/layout/contact_new_or_edit_fragment.xml b/app/src/main/res/layout/contact_new_or_edit_fragment.xml index ee21db3dd..b15911b45 100644 --- a/app/src/main/res/layout/contact_new_or_edit_fragment.xml +++ b/app/src/main/res/layout/contact_new_or_edit_fragment.xml @@ -29,7 +29,7 @@ android:adjustViewBounds="true" android:padding="15dp" android:src="@drawable/caret_left" - app:tint="@color/gray_main2_500" + app:tint="@color/orange_main_500" app:layout_constraintBottom_toBottomOf="@id/title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/title" /> diff --git a/app/src/main/res/layout/help_debug_fragment.xml b/app/src/main/res/layout/help_debug_fragment.xml index 85356bc60..7ef11b539 100644 --- a/app/src/main/res/layout/help_debug_fragment.xml +++ b/app/src/main/res/layout/help_debug_fragment.xml @@ -26,7 +26,7 @@ android:adjustViewBounds="true" android:padding="15dp" android:src="@drawable/caret_left" - app:tint="@color/gray_main2_500" + app:tint="@color/orange_main_500" app:layout_constraintBottom_toBottomOf="@id/title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/title" /> diff --git a/app/src/main/res/layout/help_fragment.xml b/app/src/main/res/layout/help_fragment.xml index 98e7f86ba..2a9cd12e4 100644 --- a/app/src/main/res/layout/help_fragment.xml +++ b/app/src/main/res/layout/help_fragment.xml @@ -43,7 +43,7 @@ android:adjustViewBounds="true" android:padding="15dp" android:src="@drawable/caret_left" - app:tint="@color/gray_main2_500" + app:tint="@color/orange_main_500" app:layout_constraintBottom_toBottomOf="@id/title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/title" /> diff --git a/app/src/main/res/layout/recordings_fragment.xml b/app/src/main/res/layout/recordings_fragment.xml index bc2cca3f1..2092a8944 100644 --- a/app/src/main/res/layout/recordings_fragment.xml +++ b/app/src/main/res/layout/recordings_fragment.xml @@ -23,7 +23,7 @@ android:adjustViewBounds="true" android:padding="15dp" android:src="@drawable/caret_left" - app:tint="@color/gray_main2_500" + app:tint="@color/orange_main_500" app:layout_constraintBottom_toBottomOf="@id/title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/title" /> diff --git a/app/src/main/res/layout/settings_fragment.xml b/app/src/main/res/layout/settings_fragment.xml index 45f59c6d1..505a5c1ac 100644 --- a/app/src/main/res/layout/settings_fragment.xml +++ b/app/src/main/res/layout/settings_fragment.xml @@ -25,7 +25,7 @@ android:adjustViewBounds="true" android:padding="15dp" android:src="@drawable/caret_left" - app:tint="@color/gray_main2_500" + app:tint="@color/orange_main_500" app:layout_constraintBottom_toBottomOf="@id/title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/title" /> diff --git a/app/src/main/res/layout/voip_call_list_cell.xml b/app/src/main/res/layout/voip_call_list_cell.xml new file mode 100644 index 000000000..66c2d9d11 --- /dev/null +++ b/app/src/main/res/layout/voip_call_list_cell.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_calls_list_fragment.xml b/app/src/main/res/layout/voip_calls_list_fragment.xml new file mode 100644 index 000000000..9787c5f65 --- /dev/null +++ b/app/src/main/res/layout/voip_calls_list_fragment.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/voip_nav_graph.xml b/app/src/main/res/navigation/voip_nav_graph.xml index f196f70da..4cfe435c2 100644 --- a/app/src/main/res/navigation/voip_nav_graph.xml +++ b/app/src/main/res/navigation/voip_nav_graph.xml @@ -54,6 +54,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 f6c7e411d..e363f057f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -290,6 +290,8 @@ Paused Ended + Calls list + Skip Forgotten password?