mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-17 03:18:06 +00:00
Merged blind/attended call transfer feature into one
This commit is contained in:
parent
d446e6d998
commit
e3d356765d
19 changed files with 954 additions and 338 deletions
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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}"/>
|
||||
|
||||
|
|
|
|||
214
app/src/main/res/layout/call_transfer_fragment.xml
Normal file
214
app/src/main/res/layout/call_transfer_fragment.xml
Normal 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>
|
||||
103
app/src/main/res/layout/dialog_call_confirm_transfer.xml
Normal file
103
app/src/main/res/layout/dialog_call_confirm_transfer.xml
Normal 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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue