Merged blind/attended call transfer feature into one

This commit is contained in:
Sylvain Berfini 2024-10-15 13:04:38 +02:00
parent d446e6d998
commit e3d356765d
19 changed files with 954 additions and 338 deletions

View file

@ -1,257 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.call.fragment
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.core.view.doOnPreDraw
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.contacts.getListOfSipAddressesAndPhoneNumbers
import org.linphone.core.Address
import org.linphone.core.tools.Log
import org.linphone.databinding.StartCallFragmentBinding
import org.linphone.ui.main.adapter.ConversationsContactsAndSuggestionsListAdapter
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel
import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel
import org.linphone.ui.main.history.viewmodel.StartCallViewModel
import org.linphone.ui.main.model.ConversationContactOrSuggestionModel
import org.linphone.utils.DialogUtils
import org.linphone.utils.LinphoneUtils
import org.linphone.utils.RecyclerViewHeaderDecoration
import org.linphone.utils.hideKeyboard
import org.linphone.utils.setKeyboardInsetListener
import org.linphone.utils.showKeyboard
abstract class AbstractNewTransferCallFragment : GenericCallFragment() {
companion object {
private const val TAG = "[New/Transfer Call Fragment]"
}
private lateinit var binding: StartCallFragmentBinding
private val viewModel: StartCallViewModel by navGraphViewModels(
R.id.call_nav_graph
)
private lateinit var adapter: ConversationsContactsAndSuggestionsListAdapter
private val listener = object : ContactNumberOrAddressClickListener {
@UiThread
override fun onClicked(model: ContactNumberOrAddressModel) {
val address = model.address
if (address != null) {
coreContext.postOnCoreThread {
action(address)
}
}
}
@UiThread
override fun onLongPress(model: ContactNumberOrAddressModel) {
}
}
private var numberOrAddressPickerDialog: Dialog? = null
abstract val title: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = ConversationsContactsAndSuggestionsListAdapter()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = StartCallFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
viewModel.title.value = title
binding.viewModel = viewModel
observeToastEvents(viewModel)
binding.setBackClickListener {
findNavController().popBackStack()
}
binding.setHideNumpadClickListener {
viewModel.hideNumpad()
}
binding.contactsAndSuggestionsList.setHasFixedSize(true)
val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter)
binding.contactsAndSuggestionsList.addItemDecoration(headerItemDecoration)
adapter.onClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
startCall(model)
}
}
binding.contactsAndSuggestionsList.layoutManager = LinearLayoutManager(requireContext())
viewModel.modelsList.observe(
viewLifecycleOwner
) {
Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items")
val count = adapter.itemCount
adapter.submitList(it)
// Wait for adapter to have items before setting it in the RecyclerView,
// otherwise scroll position isn't retained
if (binding.contactsAndSuggestionsList.adapter != adapter) {
binding.contactsAndSuggestionsList.adapter = adapter
}
if (count == 0) {
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
}
}
viewModel.searchFilter.observe(viewLifecycleOwner) { filter ->
val trimmed = filter.trim()
viewModel.applyFilter(trimmed)
}
viewModel.removedCharacterAtCurrentPositionEvent.observe(viewLifecycleOwner) {
it.consume {
val selectionStart = binding.searchBar.selectionStart
val selectionEnd = binding.searchBar.selectionEnd
if (selectionStart > 0) {
binding.searchBar.text =
binding.searchBar.text?.delete(
selectionStart - 1,
selectionEnd
)
binding.searchBar.setSelection(selectionStart - 1)
}
}
}
viewModel.appendDigitToSearchBarEvent.observe(viewLifecycleOwner) {
it.consume { digit ->
val newValue = "${binding.searchBar.text}$digit"
binding.searchBar.setText(newValue)
binding.searchBar.setSelection(newValue.length)
}
}
viewModel.requestKeyboardVisibilityChangedEvent.observe(viewLifecycleOwner) {
it.consume { show ->
if (show) {
// To automatically open keyboard
binding.searchBar.showKeyboard()
} else {
binding.searchBar.requestFocus()
binding.searchBar.hideKeyboard()
}
}
}
viewModel.isNumpadVisible.observe(viewLifecycleOwner) { visible ->
val standardBottomSheetBehavior = BottomSheetBehavior.from(binding.numpadLayout.root)
if (visible) {
standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
} else {
standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
}
binding.root.setKeyboardInsetListener { keyboardVisible ->
if (keyboardVisible) {
viewModel.isNumpadVisible.value = false
}
}
}
override fun onPause() {
super.onPause()
numberOrAddressPickerDialog?.dismiss()
numberOrAddressPickerDialog = null
}
@WorkerThread
abstract fun action(address: Address)
private fun startCall(model: ConversationContactOrSuggestionModel) {
coreContext.postOnCoreThread {
val friend = model.friend
if (friend == null) {
action(model.address)
return@postOnCoreThread
}
val singleAvailableAddress = LinphoneUtils.getSingleAvailableAddressForFriend(friend)
if (singleAvailableAddress != null) {
Log.i(
"$TAG Only 1 SIP address or phone number found for contact [${friend.name}], starting call directly"
)
action(singleAvailableAddress)
} else {
val list = friend.getListOfSipAddressesAndPhoneNumbers(listener)
Log.i(
"$TAG [${list.size}] numbers or addresses found for contact [${friend.name}], showing selection dialog"
)
coreContext.postOnMainThread {
val numberOrAddressModel = NumberOrAddressPickerDialogModel(list)
val dialog =
DialogUtils.getNumberOrAddressPickerDialog(
requireActivity(),
numberOrAddressModel
)
numberOrAddressPickerDialog = dialog
numberOrAddressModel.dismissEvent.observe(viewLifecycleOwner) { event ->
event.consume {
dialog.dismiss()
}
}
dialog.show()
}
}
}
}
}

View file

@ -214,6 +214,11 @@ class ActiveCallFragment : GenericCallFragment() {
requireActivity().finish()
}
binding.setTransferCallClickListener {
val action = ActiveCallFragmentDirections.actionActiveCallFragmentToTransferCallFragment()
findNavController().navigate(action)
}
binding.setNewCallClickListener {
val action = ActiveCallFragmentDirections.actionActiveCallFragmentToNewCallFragment()
findNavController().navigate(action)
@ -242,13 +247,6 @@ class ActiveCallFragment : GenericCallFragment() {
updateHingeRelatedConstraints(feature)
}
callViewModel.goToInitiateBlindTransferEvent.observe(viewLifecycleOwner) {
it.consume {
val action = ActiveCallFragmentDirections.actionActiveCallFragmentToTransferCallFragment()
findNavController().navigate(action)
}
}
callViewModel.fullScreenMode.observe(viewLifecycleOwner) { hide ->
Log.i("$TAG Switching full screen mode to ${if (hide) "ON" else "OFF"}")
sharedViewModel.toggleFullScreenEvent.value = Event(hide)

View file

@ -19,25 +19,238 @@
*/
package org.linphone.ui.call.fragment
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.core.view.doOnPreDraw
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.contacts.getListOfSipAddressesAndPhoneNumbers
import org.linphone.core.Address
import org.linphone.core.tools.Log
import org.linphone.databinding.StartCallFragmentBinding
import org.linphone.ui.main.adapter.ConversationsContactsAndSuggestionsListAdapter
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel
import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel
import org.linphone.ui.main.history.viewmodel.StartCallViewModel
import org.linphone.ui.main.model.ConversationContactOrSuggestionModel
import org.linphone.utils.DialogUtils
import org.linphone.utils.LinphoneUtils
import org.linphone.utils.RecyclerViewHeaderDecoration
import org.linphone.utils.hideKeyboard
import org.linphone.utils.setKeyboardInsetListener
import org.linphone.utils.showKeyboard
@UiThread
class NewCallFragment : AbstractNewTransferCallFragment() {
class NewCallFragment : GenericCallFragment() {
companion object {
private const val TAG = "[New Call Fragment]"
}
override val title: String
get() = getString(R.string.call_action_start_new_call)
private lateinit var binding: StartCallFragmentBinding
private val viewModel: StartCallViewModel by navGraphViewModels(
R.id.call_nav_graph
)
private lateinit var adapter: ConversationsContactsAndSuggestionsListAdapter
private val listener = object : ContactNumberOrAddressClickListener {
@UiThread
override fun onClicked(model: ContactNumberOrAddressModel) {
val address = model.address
if (address != null) {
coreContext.postOnCoreThread {
action(address)
}
}
}
@UiThread
override fun onLongPress(model: ContactNumberOrAddressModel) {
}
}
private var numberOrAddressPickerDialog: Dialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = ConversationsContactsAndSuggestionsListAdapter()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = StartCallFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
observeToastEvents(viewModel)
binding.setBackClickListener {
findNavController().popBackStack()
}
binding.setHideNumpadClickListener {
viewModel.hideNumpad()
}
binding.contactsAndSuggestionsList.setHasFixedSize(true)
val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter)
binding.contactsAndSuggestionsList.addItemDecoration(headerItemDecoration)
adapter.onClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
startCall(model)
}
}
binding.contactsAndSuggestionsList.layoutManager = LinearLayoutManager(requireContext())
viewModel.modelsList.observe(
viewLifecycleOwner
) {
Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items")
val count = adapter.itemCount
adapter.submitList(it)
// Wait for adapter to have items before setting it in the RecyclerView,
// otherwise scroll position isn't retained
if (binding.contactsAndSuggestionsList.adapter != adapter) {
binding.contactsAndSuggestionsList.adapter = adapter
}
if (count == 0) {
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
}
}
viewModel.searchFilter.observe(viewLifecycleOwner) { filter ->
val trimmed = filter.trim()
viewModel.applyFilter(trimmed)
}
viewModel.removedCharacterAtCurrentPositionEvent.observe(viewLifecycleOwner) {
it.consume {
val selectionStart = binding.searchBar.selectionStart
val selectionEnd = binding.searchBar.selectionEnd
if (selectionStart > 0) {
binding.searchBar.text =
binding.searchBar.text?.delete(
selectionStart - 1,
selectionEnd
)
binding.searchBar.setSelection(selectionStart - 1)
}
}
}
viewModel.appendDigitToSearchBarEvent.observe(viewLifecycleOwner) {
it.consume { digit ->
val newValue = "${binding.searchBar.text}$digit"
binding.searchBar.setText(newValue)
binding.searchBar.setSelection(newValue.length)
}
}
viewModel.requestKeyboardVisibilityChangedEvent.observe(viewLifecycleOwner) {
it.consume { show ->
if (show) {
// To automatically open keyboard
binding.searchBar.showKeyboard()
} else {
binding.searchBar.requestFocus()
binding.searchBar.hideKeyboard()
}
}
}
viewModel.isNumpadVisible.observe(viewLifecycleOwner) { visible ->
val standardBottomSheetBehavior = BottomSheetBehavior.from(binding.numpadLayout.root)
if (visible) {
standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
} else {
standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
}
binding.root.setKeyboardInsetListener { keyboardVisible ->
if (keyboardVisible) {
viewModel.isNumpadVisible.value = false
}
}
}
override fun onPause() {
super.onPause()
numberOrAddressPickerDialog?.dismiss()
numberOrAddressPickerDialog = null
}
private fun startCall(model: ConversationContactOrSuggestionModel) {
coreContext.postOnCoreThread {
val friend = model.friend
if (friend == null) {
action(model.address)
return@postOnCoreThread
}
val singleAvailableAddress = LinphoneUtils.getSingleAvailableAddressForFriend(friend)
if (singleAvailableAddress != null) {
Log.i(
"$TAG Only 1 SIP address or phone number found for contact [${friend.name}], starting call directly"
)
action(singleAvailableAddress)
} else {
val list = friend.getListOfSipAddressesAndPhoneNumbers(listener)
Log.i(
"$TAG [${list.size}] numbers or addresses found for contact [${friend.name}], showing selection dialog"
)
coreContext.postOnMainThread {
val numberOrAddressModel = NumberOrAddressPickerDialogModel(list)
val dialog =
DialogUtils.getNumberOrAddressPickerDialog(
requireActivity(),
numberOrAddressModel
)
numberOrAddressPickerDialog = dialog
numberOrAddressModel.dismissEvent.observe(viewLifecycleOwner) { event ->
event.consume {
dialog.dismiss()
}
}
dialog.show()
}
}
}
}
@WorkerThread
override fun action(address: Address) {
private fun action(address: Address) {
Log.i("$TAG Calling [${address.asStringUriOnly()}]")
coreContext.startAudioCall(address)

View file

@ -19,48 +19,305 @@
*/
package org.linphone.ui.call.fragment
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.core.view.doOnPreDraw
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import kotlin.getValue
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.Address
import org.linphone.core.tools.Log
import org.linphone.databinding.CallTransferFragmentBinding
import org.linphone.ui.call.adapter.CallsListAdapter
import org.linphone.ui.call.model.CallModel
import org.linphone.ui.call.model.ConfirmCallTransferDialogModel
import org.linphone.ui.call.viewmodel.CallsViewModel
import org.linphone.ui.call.viewmodel.CurrentCallViewModel
import org.linphone.ui.main.adapter.ConversationsContactsAndSuggestionsListAdapter
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel
import org.linphone.ui.main.history.viewmodel.StartCallViewModel
import org.linphone.ui.main.model.ConversationContactOrSuggestionModel
import org.linphone.utils.DialogUtils
import org.linphone.utils.RecyclerViewHeaderDecoration
import org.linphone.utils.hideKeyboard
import org.linphone.utils.setKeyboardInsetListener
import org.linphone.utils.showKeyboard
@UiThread
class TransferCallFragment : AbstractNewTransferCallFragment() {
class TransferCallFragment : GenericCallFragment() {
companion object {
private const val TAG = "[Transfer Call Fragment]"
}
override val title: String
get() = getString(R.string.call_transfer_title)
private lateinit var binding: CallTransferFragmentBinding
private val viewModel: StartCallViewModel by navGraphViewModels(
R.id.call_nav_graph
)
private lateinit var callViewModel: CurrentCallViewModel
private lateinit var callsViewModel: CallsViewModel
private lateinit var callsAdapter: CallsListAdapter
private lateinit var contactsAdapter: ConversationsContactsAndSuggestionsListAdapter
private var numberOrAddressPickerDialog: Dialog? = null
private val listener = object : ContactNumberOrAddressClickListener {
@UiThread
override fun onClicked(model: ContactNumberOrAddressModel) {
val address = model.address
if (address != null) {
coreContext.postOnCoreThread {
// TODO FIXME: transfer call (blind)
}
}
}
@UiThread
override fun onLongPress(model: ContactNumberOrAddressModel) {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
callsAdapter = CallsListAdapter()
contactsAdapter = ConversationsContactsAndSuggestionsListAdapter()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = CallTransferFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
callViewModel = requireActivity().run {
ViewModelProvider(this)[CurrentCallViewModel::class.java]
}
}
@WorkerThread
override fun action(address: Address) {
Log.i("$TAG Transferring current call to [${address.asStringUriOnly()}]")
callViewModel.blindTransferCallTo(address)
callsViewModel = requireActivity().run {
ViewModelProvider(this)[CallsViewModel::class.java]
}
coreContext.postOnMainThread {
try {
findNavController().popBackStack()
} catch (ise: IllegalStateException) {
Log.e("$TAG Can't go back: $ise")
binding.viewModel = viewModel
binding.callsViewModel = callsViewModel
observeToastEvents(viewModel)
binding.setBackClickListener {
findNavController().popBackStack()
}
binding.setHideNumpadClickListener {
viewModel.hideNumpad()
}
binding.callsList.setHasFixedSize(true)
binding.contactsAndSuggestionsList.setHasFixedSize(true)
callsAdapter.callClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
showConfirmAttendedTransferDialog(model)
}
}
val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), contactsAdapter)
binding.contactsAndSuggestionsList.addItemDecoration(headerItemDecoration)
contactsAdapter.onClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
showConfirmBlindTransferDialog(model)
}
}
callsViewModel.callsExceptCurrentOne.observe(viewLifecycleOwner) {
Log.i("$TAG Calls list updated with [${it.size}] items")
callsAdapter.submitList(it)
// Wait for adapter to have items before setting it in the RecyclerView,
// otherwise scroll position isn't retained
if (binding.callsList.adapter != callsAdapter) {
binding.callsList.adapter = callsAdapter
}
}
binding.contactsAndSuggestionsList.layoutManager = LinearLayoutManager(requireContext())
binding.callsList.layoutManager = LinearLayoutManager(requireContext())
viewModel.modelsList.observe(
viewLifecycleOwner
) {
Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items")
val count = contactsAdapter.itemCount
contactsAdapter.submitList(it)
// Wait for adapter to have items before setting it in the RecyclerView,
// otherwise scroll position isn't retained
if (binding.contactsAndSuggestionsList.adapter != contactsAdapter) {
binding.contactsAndSuggestionsList.adapter = contactsAdapter
}
if (count == 0) {
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
}
}
viewModel.searchFilter.observe(viewLifecycleOwner) { filter ->
val trimmed = filter.trim()
viewModel.applyFilter(trimmed)
}
viewModel.removedCharacterAtCurrentPositionEvent.observe(viewLifecycleOwner) {
it.consume {
val selectionStart = binding.searchBar.selectionStart
val selectionEnd = binding.searchBar.selectionEnd
if (selectionStart > 0) {
binding.searchBar.text =
binding.searchBar.text?.delete(
selectionStart - 1,
selectionEnd
)
binding.searchBar.setSelection(selectionStart - 1)
}
}
}
viewModel.appendDigitToSearchBarEvent.observe(viewLifecycleOwner) {
it.consume { digit ->
val newValue = "${binding.searchBar.text}$digit"
binding.searchBar.setText(newValue)
binding.searchBar.setSelection(newValue.length)
}
}
viewModel.requestKeyboardVisibilityChangedEvent.observe(viewLifecycleOwner) {
it.consume { show ->
if (show) {
// To automatically open keyboard
binding.searchBar.showKeyboard()
} else {
binding.searchBar.requestFocus()
binding.searchBar.hideKeyboard()
}
}
}
viewModel.isNumpadVisible.observe(viewLifecycleOwner) { visible ->
val standardBottomSheetBehavior = BottomSheetBehavior.from(binding.numpadLayout.root)
if (visible) {
standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
} else {
standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
}
binding.root.setKeyboardInsetListener { keyboardVisible ->
if (keyboardVisible) {
viewModel.isNumpadVisible.value = false
}
}
}
override fun onPause() {
super.onPause()
numberOrAddressPickerDialog?.dismiss()
numberOrAddressPickerDialog = null
}
override fun onResume() {
super.onResume()
viewModel.title.value = getString(
R.string.call_transfer_current_call_title,
callViewModel.displayedName.value ?: callViewModel.displayedAddress.value
)
}
private fun showConfirmAttendedTransferDialog(callModel: CallModel) {
val model = ConfirmCallTransferDialogModel(
callViewModel.displayedName.value.orEmpty(),
callModel.displayName.value.orEmpty()
)
val dialog = DialogUtils.getConfirmCallTransferCallDialog(
requireActivity(),
model
)
model.cancelEvent.observe(viewLifecycleOwner) {
it.consume {
dialog.dismiss()
}
}
model.confirmEvent.observe(viewLifecycleOwner) {
it.consume {
coreContext.postOnCoreThread {
val call = callModel.call
Log.i(
"$TAG Transferring (attended) call to [${call.remoteAddress.asStringUriOnly()}]"
)
callViewModel.attendedTransferCallTo(call)
}
dialog.dismiss()
findNavController().popBackStack()
}
}
dialog.show()
}
private fun showConfirmBlindTransferDialog(contactModel: ConversationContactOrSuggestionModel) {
val model = ConfirmCallTransferDialogModel(
callViewModel.displayedName.value.orEmpty(),
contactModel.name
)
val dialog = DialogUtils.getConfirmCallTransferCallDialog(
requireActivity(),
model
)
model.cancelEvent.observe(viewLifecycleOwner) {
it.consume {
dialog.dismiss()
}
}
model.confirmEvent.observe(viewLifecycleOwner) {
it.consume {
coreContext.postOnCoreThread {
val address = contactModel.address
Log.i("$TAG Transferring (blind) call to [${address.asStringUriOnly()}]")
callViewModel.blindTransferCallTo(address)
}
dialog.dismiss()
findNavController().popBackStack()
}
}
dialog.show()
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2010-2024 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.call.model
import androidx.annotation.UiThread
import androidx.lifecycle.MutableLiveData
import org.linphone.utils.AppUtils
import org.linphone.utils.Event
class ConfirmCallTransferDialogModel @UiThread constructor(
toTransfer: String,
toReceiveTransfer: String
) {
val cancelEvent = MutableLiveData<Event<Boolean>>()
val confirmEvent = MutableLiveData<Event<Boolean>>()
val message = MutableLiveData<String>()
init {
message.value = AppUtils.getFormattedString(
org.linphone.R.string.call_transfer_confirm_dialog_message,
toTransfer,
toReceiveTransfer
)
}
@UiThread
fun cancel() {
cancelEvent.value = Event(true)
}
@UiThread
fun confirm() {
confirmEvent.value = Event(true)
}
}

View file

@ -41,6 +41,8 @@ class CallsViewModel @UiThread constructor() : GenericViewModel() {
val calls = MutableLiveData<ArrayList<CallModel>>()
val callsExceptCurrentOne = MutableLiveData<ArrayList<CallModel>>()
val callsCount = MutableLiveData<Int>()
val showTopBar = MutableLiveData<Boolean>()
@ -237,6 +239,15 @@ class CallsViewModel @UiThread constructor() : GenericViewModel() {
private fun updateOtherCallsInfo() {
val core = coreContext.core
callsExceptCurrentOne.value.orEmpty().forEach(CallModel::destroy)
val list = arrayListOf<CallModel>()
for (call in core.calls) {
if (call != core.currentCall) {
list.add(CallModel(call))
}
}
callsExceptCurrentOne.postValue(list)
if (core.callsNb > 1) {
showTopBar.postValue(true)
if (core.callsNb == 2) {

View file

@ -138,10 +138,6 @@ class CurrentCallViewModel @UiThread constructor() : GenericViewModel() {
MutableLiveData<Event<Pair<Boolean, String>>>()
}
val goToInitiateBlindTransferEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
val goToEndedCallEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
@ -236,7 +232,7 @@ class CurrentCallViewModel @UiThread constructor() : GenericViewModel() {
MutableLiveData<Event<Boolean>>()
}
private lateinit var currentCall: Call
lateinit var currentCall: Call
private val callListener = object : CallListenerStub() {
@WorkerThread
@ -874,35 +870,6 @@ class CurrentCallViewModel @UiThread constructor() : GenericViewModel() {
showNumpadBottomSheetEvent.value = Event(true)
}
@UiThread
fun transferClicked() {
coreContext.postOnCoreThread { core ->
if (core.callsNb == 1) {
Log.i("$TAG Only one call, initiate blind call transfer")
goToInitiateBlindTransferEvent.postValue(Event(true))
} else {
val callToTransferTo = core.calls.findLast {
it.state == Call.State.Paused && it != currentCall
}
if (callToTransferTo == null) {
Log.e(
"$TAG Couldn't find a call in Paused state to transfer current call to"
)
return@postOnCoreThread
}
Log.i(
"$TAG Doing an attended transfer between currently displayed call [${currentCall.remoteAddress.asStringUriOnly()}] and paused call [${callToTransferTo.remoteAddress.asStringUriOnly()}]"
)
if (callToTransferTo.transferToAnother(currentCall) != 0) {
Log.e("$TAG Failed to make attended transfer!")
} else {
Log.i("$TAG Attended transfer is successful")
}
}
}
}
@UiThread
fun createConversation() {
if (::currentCall.isInitialized) {
@ -930,6 +897,20 @@ class CurrentCallViewModel @UiThread constructor() : GenericViewModel() {
}
}
@WorkerThread
fun attendedTransferCallTo(to: Call) {
if (::currentCall.isInitialized) {
Log.i(
"$TAG Doing an attended transfer between currently displayed call [${currentCall.remoteAddress.asStringUriOnly()}] and paused call [${to.remoteAddress.asStringUriOnly()}]"
)
if (to.transferToAnother(currentCall) == 0) {
Log.i("$TAG Attended transfer is successful")
} else {
Log.e("$TAG Failed to make attended transfer!")
}
}
}
@WorkerThread
fun blindTransferCallTo(to: Address) {
if (::currentCall.isInitialized) {

View file

@ -21,6 +21,7 @@ package org.linphone.ui.main.contacts.model
import androidx.annotation.UiThread
import androidx.lifecycle.MutableLiveData
import org.linphone.utils.AppUtils
import org.linphone.utils.Event
class TrustCallDialogModel @UiThread constructor(contact: String, device: String) {
@ -33,7 +34,11 @@ class TrustCallDialogModel @UiThread constructor(contact: String, device: String
val confirmCallEvent = MutableLiveData<Event<Boolean>>()
init {
message.value = "You're about to call $contact's device $device.\nAre you sure you want to make a call now?"
message.value = AppUtils.getFormattedString(
org.linphone.R.string.contact_dialog_increase_trust_level_message,
contact,
device
)
}
@UiThread

View file

@ -129,8 +129,6 @@ class MainViewModel @UiThread constructor() : ViewModel() {
Log.i("$TAG Last call ended, removing in-call 'alert'")
removeAlert(SINGLE_CALL)
atLeastOneCall.postValue(false)
// TODO: do not do it if nothing has changed!
computeNonDefaultAccountNotificationsCount()
}

View file

@ -70,8 +70,13 @@ class AppUtils {
}
@AnyThread
fun getFormattedString(@StringRes id: Int, args: Any): String {
return coreContext.context.getString(id, args)
fun getFormattedString(@StringRes id: Int, arg: Any): String {
return coreContext.context.getString(id, arg)
}
@AnyThread
fun getFormattedString(@StringRes id: Int, arg1: Any, arg2: Any): String {
return coreContext.context.getString(id, arg1, arg2)
}
@AnyThread

View file

@ -36,6 +36,7 @@ import androidx.lifecycle.LifecycleOwner
import org.linphone.R
import org.linphone.databinding.DialogAssistantAcceptConditionsAndPolicyBinding
import org.linphone.databinding.DialogAssistantCreateAccountConfirmPhoneNumberBinding
import org.linphone.databinding.DialogCallConfirmTransferBinding
import org.linphone.databinding.DialogCancelContactChangesBinding
import org.linphone.databinding.DialogCancelMeetingBinding
import org.linphone.databinding.DialogContactConfirmTrustCallBinding
@ -59,6 +60,7 @@ import org.linphone.databinding.DialogZrtpSasValidationBinding
import org.linphone.databinding.DialogZrtpSecurityAlertBinding
import org.linphone.ui.assistant.model.AcceptConditionsAndPolicyDialogModel
import org.linphone.ui.assistant.model.ConfirmPhoneNumberDialogModel
import org.linphone.ui.call.model.ConfirmCallTransferDialogModel
import org.linphone.ui.call.model.ZrtpAlertDialogModel
import org.linphone.ui.call.model.ZrtpSasConfirmationDialogModel
import org.linphone.ui.main.contacts.model.ContactTrustDialogModel
@ -430,6 +432,22 @@ class DialogUtils {
return getDialog(context, binding)
}
@UiThread
fun getConfirmCallTransferCallDialog(
context: Context,
viewModel: ConfirmCallTransferDialogModel
): Dialog {
val binding: DialogCallConfirmTransferBinding = DataBindingUtil.inflate(
LayoutInflater.from(context),
R.layout.dialog_call_confirm_transfer,
null,
false
)
binding.viewModel = viewModel
return getDialog(context, binding)
}
@UiThread
fun getKickConferenceParticipantConfirmationDialog(
context: Context,

View file

@ -5,6 +5,9 @@
<data>
<import type="android.view.View" />
<variable
name="transferCallClickListener"
type="View.OnClickListener" />
<variable
name="newCallClickListener"
type="View.OnClickListener" />
@ -39,14 +42,14 @@
<ImageView
android:id="@+id/transfer"
android:onClick="@{() -> viewModel.transferClicked()}"
android:onClick="@{transferCallClickListener}"
android:layout_width="0dp"
android:layout_height="@dimen/call_button_size"
android:layout_marginTop="@dimen/call_extra_button_top_margin"
android:background="@drawable/in_call_button_background_red"
android:padding="@dimen/call_button_icon_padding"
android:src="@drawable/phone_transfer"
android:contentDescription="@{callsViewModel.callsCount == 1 ? @string/call_action_blind_transfer : @string/call_action_attended_transfer}"
android:contentDescription="@string/call_action_transfer"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="@id/transfer_label"
app:layout_constraintStart_toStartOf="@id/transfer_label"
@ -207,11 +210,11 @@
<androidx.appcompat.widget.AppCompatTextView
style="@style/in_call_extra_action_label_style"
android:id="@+id/transfer_label"
android:onClick="@{() -> viewModel.transferClicked()}"
android:onClick="@{transferCallClickListener}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingBottom="15dp"
android:text="@{callsViewModel.callsCount == 1 ? @string/call_action_blind_transfer : @string/call_action_attended_transfer, default=@string/call_action_blind_transfer}"
android:text="@string/call_action_transfer"
app:layout_constraintEnd_toStartOf="@id/new_call_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/transfer"/>

View file

@ -5,6 +5,9 @@
<data>
<import type="android.view.View" />
<variable
name="transferCallClickListener"
type="View.OnClickListener" />
<variable
name="newCallClickListener"
type="View.OnClickListener" />
@ -39,14 +42,14 @@
<ImageView
android:id="@+id/transfer"
android:onClick="@{() -> viewModel.transferClicked()}"
android:onClick="@{transferCallClickListener}"
android:layout_width="0dp"
android:layout_height="@dimen/call_button_size"
android:layout_marginTop="@dimen/call_extra_button_top_margin"
android:padding="@dimen/call_button_icon_padding"
android:background="@drawable/in_call_button_background_red"
android:src="@drawable/phone_transfer"
android:contentDescription="@{callsViewModel.callsCount == 1 ? @string/call_action_blind_transfer : @string/call_action_attended_transfer}"
android:contentDescription="@string/call_action_transfer"
app:tint="@color/in_call_button_tint_color"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="@id/transfer_label"
@ -207,10 +210,10 @@
<androidx.appcompat.widget.AppCompatTextView
style="@style/in_call_extra_action_label_style"
android:id="@+id/transfer_label"
android:onClick="@{() -> viewModel.transferClicked()}"
android:onClick="@{transferCallClickListener}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{callsViewModel.callsCount == 1 ? @string/call_action_blind_transfer : @string/call_action_attended_transfer, default=@string/call_action_blind_transfer}"
android:text="@string/call_action_transfer"
app:layout_constraintTop_toBottomOf="@id/transfer"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/new_call_label"/>

View file

@ -8,6 +8,9 @@
<variable
name="backClickListener"
type="View.OnClickListener" />
<variable
name="transferCallClickListener"
type="View.OnClickListener" />
<variable
name="newCallClickListener"
type="View.OnClickListener" />
@ -277,6 +280,7 @@
layout="@layout/call_actions_bottom_sheet"
bind:viewModel="@{viewModel}"
bind:callsViewModel="@{callsViewModel}"
bind:transferCallClickListener="@{transferCallClickListener}"
bind:newCallClickListener="@{newCallClickListener}"
bind:callsListClickListener="@{callsListClickListener}"/>

View file

@ -0,0 +1,214 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:bind="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="backClickListener"
type="View.OnClickListener" />
<variable
name="hideNumpadClickListener"
type="View.OnClickListener" />
<variable
name="askForGroupCallSubjectClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.history.viewmodel.StartCallViewModel" />
<variable
name="callsViewModel"
type="org.linphone.ui.call.viewmodel.CallsViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/color_main2_000">
<ImageView
android:id="@+id/back"
android:layout_width="wrap_content"
android:layout_height="@dimen/top_bar_height"
android:adjustViewBounds="true"
android:onClick="@{backClickListener}"
android:padding="15dp"
android:src="@drawable/caret_left"
android:contentDescription="@string/content_description_go_back_icon"
app:tint="?attr/color_main1_500"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/main_page_title_style"
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:text="@{viewModel.title, default=@string/call_transfer_current_call_title}"
app:layout_constraintBottom_toBottomOf="@id/back"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/back"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/background"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/back" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/section_header_style"
android:id="@+id/calls_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="16dp"
android:text="@string/call_transfer_active_calls_label"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/section_header_style"
android:id="@+id/no_current_call_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/call_transfer_no_active_call_label"
android:gravity="center"
android:visibility="@{callsViewModel.callsExceptCurrentOne.isEmpty ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/calls_label" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/calls_list"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/calls_label"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/calls_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="calls_list, no_current_call_label"
app:barrierDirection="bottom" />
<androidx.appcompat.widget.AppCompatEditText
style="@style/default_text_style"
android:id="@+id/search_bar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="20dp"
android:background="@drawable/edit_text_background"
android:drawableStart="@drawable/magnifying_glass"
android:drawablePadding="10dp"
android:drawableTint="?attr/color_main2_600"
android:hint="@string/history_call_start_search_bar_filter_hint"
android:inputType="textPersonName|textNoSuggestions"
android:paddingStart="15dp"
android:paddingTop="10dp"
android:paddingEnd="45dp"
android:paddingBottom="10dp"
android:text="@={viewModel.searchFilter}"
android:textSize="14sp"
app:layout_constraintHeight_min="48dp"
app:layout_constraintWidth_max="@dimen/text_input_max_width"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/calls_barrier" />
<ImageView
android:id="@+id/numpad"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:layout_marginEnd="15dp"
android:onClick="@{() -> viewModel.switchBetweenKeyboardAndNumpad()}"
android:contentDescription="@string/content_description_show_numpad"
android:src="@{viewModel.isNumpadVisible ? @drawable/keyboard : @drawable/numpad, default=@drawable/numpad}"
android:visibility="@{viewModel.searchFilter.length() == 0 ? View.VISIBLE : View.GONE}"
app:layout_constraintBottom_toBottomOf="@id/search_bar"
app:layout_constraintEnd_toEndOf="@id/search_bar"
app:layout_constraintTop_toTopOf="@id/search_bar"
app:tint="?attr/color_main2_600" />
<ImageView
android:id="@+id/clear_field"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:layout_marginEnd="15dp"
android:onClick="@{() -> viewModel.clearFilter()}"
android:src="@drawable/x"
android:contentDescription="@string/content_description_clear_filter"
android:visibility="@{viewModel.searchFilter.length() > 0 ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintBottom_toBottomOf="@id/search_bar"
app:layout_constraintEnd_toEndOf="@id/search_bar"
app:layout_constraintTop_toTopOf="@id/search_bar"
app:tint="?attr/color_main2_600" />
<ImageView
android:id="@+id/no_contacts_nor_suggestion_image"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="10dp"
android:src="@drawable/illu"
android:contentDescription="@null"
android:visibility="@{viewModel.isEmpty ? View.VISIBLE : View.GONE}"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="200dp"
app:layout_constraintVertical_bias="0.3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/search_bar"
app:layout_constraintBottom_toBottomOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/section_header_style"
android:id="@+id/no_contacts_nor_suggestion_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/history_call_start_no_suggestion_nor_contact"
android:gravity="center"
android:visibility="@{viewModel.isEmpty ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/no_contacts_nor_suggestion_image" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/contacts_and_suggestions_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:visibility="@{viewModel.isEmpty ? View.GONE : View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/search_bar"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include
android:id="@+id/numpad_layout"
bind:handleClickedListener="@{hideNumpadClickListener}"
bind:model="@{viewModel.numpadModel}"
layout="@layout/start_call_numpad_bottom_sheet" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="org.linphone.ui.call.model.ConfirmCallTransferDialogModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:onClick="@{() -> viewModel.cancel()}"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/dialog_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="2dp"
android:src="@drawable/shape_dialog_background"
android:contentDescription="@null"
app:layout_constraintWidth_max="@dimen/dialog_max_width"
app:layout_constraintBottom_toBottomOf="@id/anchor"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/title" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/section_header_style"
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:paddingTop="@dimen/dialog_top_bottom_margin"
android:text="@string/call_transfer_confirm_dialog_tittle"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintBottom_toTopOf="@id/message"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:layout_marginTop="10dp"
android:text="@{viewModel.message, default=@string/call_transfer_confirm_dialog_message}"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@id/cancel"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toBottomOf="@id/title" />
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.cancel()}"
style="@style/secondary_button_label_style"
android:id="@+id/cancel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:text="@string/dialog_cancel"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toBottomOf="@id/message"
app:layout_constraintBottom_toTopOf="@id/confirm"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.confirm()}"
style="@style/primary_button_label_style"
android:id="@+id/confirm"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:text="@string/dialog_confirm"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toBottomOf="@id/cancel"
app:layout_constraintBottom_toTopOf="@id/anchor"/>
<View
android:id="@+id/anchor"
android:layout_width="wrap_content"
android:layout_height="@dimen/dialog_top_bottom_margin"
app:layout_constraintTop_toBottomOf="@id/confirm"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -55,7 +55,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:text="@{viewModel.title, default=@string/history_call_start_title}"
android:text="@string/call_action_start_new_call"
app:layout_constraintBottom_toBottomOf="@id/back"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/back"

View file

@ -572,9 +572,13 @@
<string name="call_incoming">Appel entrant</string>
<string name="call_ended">Appel terminé</string>
<string name="call_incoming_for_account">Appel entrant pour %s</string>
<string name="call_transfer_current_call_title">Transférer %s à…</string>
<string name="call_transfer_active_calls_label">Appels en cours</string>
<string name="call_transfer_no_active_call_label">Pas d\'autre appel</string>
<string name="call_transfer_confirm_dialog_tittle">Confirmer le transfert</string>
<string name="call_transfer_confirm_dialog_message">Vous allez transférer %1$s à %2$s.</string>
<string name="call_action_blind_transfer">Transfert</string>
<string name="call_action_attended_transfer">Transfert</string>
<string name="call_action_transfer">Transfert</string>
<string name="call_action_start_new_call">Nouvel appel</string>
<string name="call_action_go_to_calls_list">Liste des appels</string>
<string name="call_action_show_dialer">Pavé</string>
@ -599,7 +603,6 @@
<string name="call_srtp_point_to_point_encrypted">Appel chiffré de point à point</string>
<string name="call_not_encrypted">Appel non chiffré</string>
<string name="calls_list_title">Liste des appels</string>
<string name="call_transfer_title">Transférer l\'appel vers</string>
<string name="call_remote_is_recording">%s est en train d\'enregistrer</string>
<string name="calls_count_label">%s appels</string>
<string name="calls_paused_count_label">%s appels en pause</string>

View file

@ -610,9 +610,13 @@
<string name="call_incoming">Incoming call</string>
<string name="call_ended">Call ended</string>
<string name="call_incoming_for_account">Incoming call for %s</string>
<string name="call_transfer_current_call_title">Transfer %s to…</string>
<string name="call_transfer_active_calls_label">Current calls</string>
<string name="call_transfer_no_active_call_label">No other call</string>
<string name="call_transfer_confirm_dialog_tittle">Confirm call transfer</string>
<string name="call_transfer_confirm_dialog_message">You\'re about to transfer call %1$s to %2$s.</string>
<string name="call_action_blind_transfer">Transfer</string>
<string name="call_action_attended_transfer">Attended transfer</string>
<string name="call_action_transfer">Transfer</string>
<string name="call_action_start_new_call">New call</string>
<string name="call_action_go_to_calls_list">Calls list</string>
<string name="call_action_show_dialer">Dialer</string>
@ -637,7 +641,6 @@
<string name="call_srtp_point_to_point_encrypted">Point-to-point encrypted by SRTP</string>
<string name="call_not_encrypted">Call is not encrypted</string>
<string name="calls_list_title">Calls list</string>
<string name="call_transfer_title">Transfer call to</string>
<string name="call_remote_is_recording">%s is recording</string>
<string name="calls_count_label">%s calls</string>
<string name="calls_paused_count_label">%s paused calls</string>