Started blind call transfer

This commit is contained in:
Sylvain Berfini 2023-09-25 14:24:57 +02:00
parent a60c66ad33
commit 027e5dd61b
14 changed files with 502 additions and 245 deletions

View file

@ -168,6 +168,11 @@ class ContactsManager @UiThread constructor(context: Context) {
}
}
@WorkerThread
fun findDisplayName(address: Address): String {
return findContactByAddress(address)?.name ?: LinphoneUtils.getDisplayName(address)
}
@WorkerThread
fun onCoreStarted(core: Core) {
core.addListener(coreListener)

View file

@ -30,8 +30,10 @@ import androidx.annotation.AnyThread
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.emoji2.text.EmojiCompat
import androidx.lifecycle.MutableLiveData
import java.util.*
import org.linphone.BuildConfig
import org.linphone.LinphoneApplication
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.contacts.ContactsManager
import org.linphone.core.tools.Log
@ -39,6 +41,7 @@ import org.linphone.notifications.NotificationsManager
import org.linphone.telecom.TelecomManager
import org.linphone.ui.call.CallActivity
import org.linphone.utils.ActivityMonitor
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
class CoreContext @UiThread constructor(val context: Context) : HandlerThread("Core Thread") {
@ -68,6 +71,10 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
private val mainThread = Handler(Looper.getMainLooper())
val greenToastToShowEvent: MutableLiveData<Event<Pair<String, Int>>> by lazy {
MutableLiveData<Event<Pair<String, Int>>>()
}
@SuppressLint("HandlerLeak")
private lateinit var coreThread: Handler
@ -77,6 +84,7 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
Log.i("$TAG Global state changed [$state]")
}
@WorkerThread
override fun onConfiguringStatus(
core: Core,
status: Config.ConfiguringState?,
@ -103,6 +111,25 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
}
}
}
@WorkerThread
override fun onTransferStateChanged(core: Core, transfered: Call, state: Call.State) {
Log.i(
"$TAG Transferred call [${transfered.remoteAddress.asStringUriOnly()}] state changed [$state]"
)
if (state == Call.State.Connected) {
// TODO FIXME: Remote is call being transferred, not transferee !
val displayName = contactsManager.findDisplayName(transfered.remoteAddress)
val message = context.getString(
org.linphone.R.string.toast_call_transfer_successful,
displayName
)
val icon = org.linphone.R.drawable.transfer
greenToastToShowEvent.postValue(Event(Pair(message, icon)))
}
}
}
init {

View file

@ -29,6 +29,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.children
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -58,6 +59,8 @@ import org.linphone.utils.slideInToastFromTopForDuration
class CallActivity : AppCompatActivity() {
companion object {
private const val TAG = "[Call Activity]"
private const val PERSISTENT_TOAST_TAG = "PERSISTENT"
}
private lateinit var binding: CallActivityBinding
@ -108,15 +111,16 @@ class CallActivity : AppCompatActivity() {
callViewModel.showLowWifiSignalEvent.observe(this) {
it.consume { show ->
if (show) {
showRedToast(
showPersistentRedToast(
getString(R.string.toast_alert_low_wifi_signal),
R.drawable.wifi_low
)
} else {
hideRedToast()
removePersistentRedToasts()
showGreenToast(
getString(R.string.toast_alert_low_wifi_signal_cleared),
R.drawable.wifi_high
R.drawable.wifi_high,
2000
)
}
}
@ -125,20 +129,39 @@ class CallActivity : AppCompatActivity() {
callViewModel.showLowCellularSignalEvent.observe(this) {
it.consume { show ->
if (show) {
showRedToast(
showPersistentRedToast(
getString(R.string.toast_alert_low_cellular_signal),
R.drawable.cell_signal_low
)
} else {
hideRedToast()
removePersistentRedToasts()
showGreenToast(
getString(R.string.toast_alert_low_cellular_signal_cleared),
R.drawable.cell_signal_full
R.drawable.cell_signal_full,
2000
)
}
}
}
callViewModel.transferInProgressEvent.observe(this) {
it.consume { remote ->
showGreenToast(
getString(R.string.toast_call_transfer_in_progress, remote),
R.drawable.transfer
)
}
}
callViewModel.transferFailedEvent.observe(this) {
it.consume { remote ->
showRedToast(
getString(R.string.toast_call_transfer_failed, remote),
R.drawable.warning_circle
)
}
}
callsViewModel.showIncomingCallEvent.observe(this) {
it.consume {
val action = IncomingCallFragmentDirections.actionGlobalIncomingCallFragment()
@ -245,29 +268,44 @@ class CallActivity : AppCompatActivity() {
)
}
private fun showRedToast(message: String, @DrawableRes icon: Int) {
private fun showRedToast(message: String, @DrawableRes icon: Int, duration: Long = 4000) {
val redToast = AppUtils.getRedToast(this, binding.toastsArea, message, icon)
binding.toastsArea.addView(redToast.root)
redToast.root.slideInToastFromTopForDuration(
binding.toastsArea as ViewGroup,
lifecycleScope,
duration
)
}
private fun showPersistentRedToast(message: String, @DrawableRes icon: Int) {
val redToast = AppUtils.getRedToast(this, binding.toastsArea, message, icon)
redToast.root.tag = PERSISTENT_TOAST_TAG
binding.toastsArea.addView(redToast.root)
redToast.root.slideInToastFromTop(
binding.toastsArea as ViewGroup,
true
)
}
private fun hideRedToast() {
// TODO: improve
binding.toastsArea.removeAllViews()
private fun removePersistentRedToasts() {
for (child in binding.toastsArea.children) {
if (child.tag == PERSISTENT_TOAST_TAG) {
binding.toastsArea.removeView(child)
}
}
}
private fun showGreenToast(message: String, @DrawableRes icon: Int) {
private fun showGreenToast(message: String, @DrawableRes icon: Int, duration: Long = 4000) {
val greenToast = AppUtils.getGreenToast(this, binding.toastsArea, message, icon)
binding.toastsArea.addView(greenToast.root)
greenToast.root.slideInToastFromTopForDuration(
binding.toastsArea as ViewGroup,
lifecycleScope,
2000
duration
)
}

View file

@ -0,0 +1,266 @@
/*
* 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.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.adapter.ContactsAndSuggestionsListAdapter
import org.linphone.ui.main.history.model.ContactOrSuggestionModel
import org.linphone.ui.main.history.viewmodel.StartCallViewModel
import org.linphone.ui.main.model.isInSecureMode
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
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: ContactsAndSuggestionsListAdapter
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 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)
postponeEnterTransition()
binding.lifecycleOwner = viewLifecycleOwner
viewModel.title.value = title
binding.viewModel = viewModel
binding.hideGroupChatButton = true
binding.setBackClickListener {
findNavController().popBackStack()
}
binding.setHideNumpadClickListener {
viewModel.hideNumpad()
}
adapter = ContactsAndSuggestionsListAdapter(viewLifecycleOwner)
binding.contactsAndSuggestionsList.setHasFixedSize(true)
binding.contactsAndSuggestionsList.adapter = adapter
val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter, true)
binding.contactsAndSuggestionsList.addItemDecoration(headerItemDecoration)
adapter.contactClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
startCall(model)
}
}
binding.contactsAndSuggestionsList.layoutManager = LinearLayoutManager(requireContext())
viewModel.contactsAndSuggestionsList.observe(
viewLifecycleOwner
) {
Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items")
val count = adapter.itemCount
adapter.submitList(it)
if (count == 0 && it.isNotEmpty()) {
(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(requireActivity().window)
} 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: ContactOrSuggestionModel) {
coreContext.postOnCoreThread { core ->
val friend = model.friend
if (friend == null) {
action(model.address)
return@postOnCoreThread
}
val addressesCount = friend.addresses.size
val numbersCount = friend.phoneNumbers.size
// Do not consider phone numbers if default account is in secure mode
val enablePhoneNumbers = core.defaultAccount?.isInSecureMode() != true
if (addressesCount == 1 && (numbersCount == 0 || !enablePhoneNumbers)) {
Log.i(
"$TAG Only 1 SIP address found for contact [${friend.name}], starting call directly"
)
val address = friend.addresses.first()
action(address)
} else if (addressesCount == 0 && numbersCount == 1 && enablePhoneNumbers) {
val number = friend.phoneNumbers.first()
val address = core.interpretUrl(number, true)
if (address != null) {
Log.i(
"$TAG Only 1 phone number found for contact [${friend.name}], starting call directly"
)
action(address)
} else {
Log.e("$TAG Failed to interpret phone number [$number] as SIP address")
}
} 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

@ -135,6 +135,11 @@ class ActiveCallFragment : GenericCallFragment() {
findNavController().navigate(action)
}
binding.setTransferClickListener {
val action = ActiveCallFragmentDirections.actionActiveCallFragmentToTransferCallFragment()
findNavController().navigate(action)
}
binding.setCallsListClickListener {
val action = ActiveCallFragmentDirections.actionActiveCallFragmentToCallsListFragment()
findNavController().navigate(action)

View file

@ -19,250 +19,30 @@
*/
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.core.view.doOnPreDraw
import androidx.annotation.WorkerThread
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.contacts.model.ContactNumberOrAddressClickListener
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel
import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel
import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.ui.main.history.adapter.ContactsAndSuggestionsListAdapter
import org.linphone.ui.main.history.model.ContactOrSuggestionModel
import org.linphone.ui.main.history.viewmodel.StartCallViewModel
import org.linphone.ui.main.model.isInSecureMode
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 NewCallFragment : GenericFragment() {
class NewCallFragment : AbstractNewTransferCallFragment() {
companion object {
private const val TAG = "[New Call Fragment]"
}
private lateinit var binding: StartCallFragmentBinding
override val title: String
get() = getString(R.string.call_action_start_new_call)
private val viewModel: StartCallViewModel by navGraphViewModels(
R.id.call_nav_graph
)
@WorkerThread
override fun action(address: Address) {
Log.i("$TAG Calling [${address.asStringUriOnly()}]")
coreContext.startCall(address)
private lateinit var adapter: ContactsAndSuggestionsListAdapter
private val listener = object : ContactNumberOrAddressClickListener {
@UiThread
override fun onClicked(model: ContactNumberOrAddressModel) {
val address = model.address
if (address != null) {
coreContext.postOnCoreThread {
coreContext.startCall(address)
}
findNavController().popBackStack()
}
}
@UiThread
override fun onLongPress(model: ContactNumberOrAddressModel) {
}
}
private var numberOrAddressPickerDialog: Dialog? = null
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)
postponeEnterTransition()
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
binding.hideGroupChatButton = true
binding.setBackClickListener {
goBack()
}
binding.setHideNumpadClickListener {
viewModel.hideNumpad()
}
adapter = ContactsAndSuggestionsListAdapter(viewLifecycleOwner)
binding.contactsAndSuggestionsList.setHasFixedSize(true)
binding.contactsAndSuggestionsList.adapter = adapter
val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter, true)
binding.contactsAndSuggestionsList.addItemDecoration(headerItemDecoration)
adapter.contactClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
startCall(model)
}
}
binding.contactsAndSuggestionsList.layoutManager = LinearLayoutManager(requireContext())
viewModel.contactsAndSuggestionsList.observe(
viewLifecycleOwner
) {
Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items")
val count = adapter.itemCount
adapter.submitList(it)
if (count == 0 && it.isNotEmpty()) {
(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(requireActivity().window)
} 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: ContactOrSuggestionModel) {
coreContext.postOnCoreThread { core ->
val friend = model.friend
if (friend == null) {
coreContext.startCall(model.address)
coreContext.postOnMainThread {
findNavController().popBackStack()
}
return@postOnCoreThread
}
val addressesCount = friend.addresses.size
val numbersCount = friend.phoneNumbers.size
// Do not consider phone numbers if default account is in secure mode
val enablePhoneNumbers = core.defaultAccount?.isInSecureMode() != true
if (addressesCount == 1 && (numbersCount == 0 || !enablePhoneNumbers)) {
Log.i(
"$TAG Only 1 SIP address found for contact [${friend.name}], starting call directly"
)
val address = friend.addresses.first()
coreContext.startCall(address)
coreContext.postOnMainThread {
findNavController().popBackStack()
}
} else if (addressesCount == 0 && numbersCount == 1 && enablePhoneNumbers) {
val number = friend.phoneNumbers.first()
val address = core.interpretUrl(number, true)
if (address != null) {
Log.i(
"$TAG Only 1 phone number found for contact [${friend.name}], starting call directly"
)
coreContext.startCall(address)
coreContext.postOnMainThread {
findNavController().popBackStack()
}
} else {
Log.e("$TAG Failed to interpret phone number [$number] as SIP address")
}
} 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()
}
}
coreContext.postOnMainThread {
findNavController().popBackStack()
}
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.call.fragment
import android.os.Bundle
import android.view.View
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.Address
import org.linphone.core.tools.Log
import org.linphone.ui.call.viewmodel.CurrentCallViewModel
@UiThread
class TransferCallFragment : AbstractNewTransferCallFragment() {
companion object {
private const val TAG = "[Transfer Call Fragment]"
}
override val title: String
get() = getString(R.string.call_transfer_title)
private lateinit var callViewModel: CurrentCallViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
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)
coreContext.postOnMainThread {
findNavController().popBackStack()
}
}
}

View file

@ -30,6 +30,7 @@ import java.util.Locale
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.core.Address
import org.linphone.core.Alert
import org.linphone.core.AlertListenerStub
import org.linphone.core.AudioDevice
@ -115,6 +116,14 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
MutableLiveData<Event<Boolean>>()
}
val transferInProgressEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
val transferFailedEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
val numpadModel: NumpadModel
val appendDigitToSearchBarEvent: MutableLiveData<Event<String>> by lazy {
@ -247,6 +256,22 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
}
}
}
@WorkerThread
override fun onTransferStateChanged(core: Core, transfered: Call, state: Call.State) {
Log.i(
"$TAG Transferred call [${transfered.remoteAddress.asStringUriOnly()}] state changed [$state]"
)
// TODO FIXME: Remote is call being transferred, not transferee !
if (state == Call.State.OutgoingProgress) {
val displayName = coreContext.contactsManager.findDisplayName(transfered.remoteAddress)
transferInProgressEvent.postValue(Event(displayName))
} else if (LinphoneUtils.isCallEnding(state)) {
val displayName = coreContext.contactsManager.findDisplayName(transfered.remoteAddress)
transferFailedEvent.postValue(Event(displayName))
}
}
}
private val alertListener = object : AlertListenerStub() {
@ -489,6 +514,22 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
showNumpadBottomSheetEvent.value = Event(true)
}
@WorkerThread
fun blindTransferCallTo(to: Address) {
if (::currentCall.isInitialized) {
Log.i(
"$TAG Call [${currentCall.remoteAddress.asStringUriOnly()}] is being blindly transferred to [${to.asStringUriOnly()}]"
)
if (currentCall.transferTo(to) == 0) {
Log.i("$TAG Blind call transfer is successful")
} else {
Log.e("$TAG Failed to make blind call transfer!")
val displayName = coreContext.contactsManager.findDisplayName(to)
transferFailedEvent.postValue(Event(displayName))
}
}
}
@WorkerThread
private fun showZrtpSasDialog(authToken: String) {
val toRead: String

View file

@ -106,6 +106,14 @@ class MainActivity : AppCompatActivity() {
POST_NOTIFICATIONS_PERMISSION_REQUEST
)
}
coreContext.greenToastToShowEvent.observe(this) {
it.consume { pair ->
val message = pair.first
val icon = pair.second
showGreenToast(message, icon)
}
}
}
override fun onRequestPermissionsResult(

View file

@ -96,6 +96,8 @@ class StartCallFragment : GenericFragment() {
postponeEnterTransition()
binding.lifecycleOwner = viewLifecycleOwner
viewModel.title.value = getString(R.string.history_call_start_title)
binding.viewModel = viewModel
binding.setBackClickListener {

View file

@ -45,6 +45,8 @@ class StartCallViewModel @UiThread constructor() : ViewModel() {
private const val TAG = "[Start Call ViewModel]"
}
val title = MutableLiveData<String>()
val searchFilter = MutableLiveData<String>()
val contactsAndSuggestionsList = MutableLiveData<ArrayList<ContactOrSuggestionModel>>()

View file

@ -14,6 +14,9 @@
<variable
name="hideGroupChatButton"
type="Boolean" />
<variable
name="title"
type="String" />
<variable
name="viewModel"
type="org.linphone.ui.main.history.viewmodel.StartCallViewModel" />
@ -48,7 +51,7 @@
android:layout_height="@dimen/top_bar_height"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:text="@string/history_call_start_title"
android:text="@{viewModel.title, default=@string/history_call_start_title}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/back"
app:layout_constraintTop_toTopOf="parent" />

View file

@ -54,6 +54,12 @@
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out"
app:launchSingleTop="true" />
<action
android:id="@+id/action_activeCallFragment_to_transferCallFragment"
app:destination="@id/transferCallFragment"
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out"
app:launchSingleTop="true" />
<action
android:id="@+id/action_activeCallFragment_to_callsListFragment"
app:destination="@id/callsListFragment"
@ -72,7 +78,15 @@
android:id="@+id/newCallFragment"
android:name="org.linphone.ui.call.fragment.NewCallFragment"
android:label="NewCallFragment"
tools:layout="@layout/start_call_fragment" />
tools:layout="@layout/start_call_fragment" >
</fragment>
<fragment
android:id="@+id/transferCallFragment"
android:name="org.linphone.ui.call.fragment.TransferCallFragment"
android:label="TransferCallFragment"
tools:layout="@layout/start_call_fragment" >
</fragment>
<fragment
android:id="@+id/callsListFragment"

View file

@ -290,6 +290,10 @@
<string name="call_state_paused">Paused</string>
<string name="call_state_ended">Ended</string>
<string name="calls_list_title">Calls list</string>
<string name="call_transfer_title">Transfer call to</string>
<string name="toast_call_transfer_in_progress">Call is being transferred to %s</string>
<string name="toast_call_transfer_successful">Call has been transferred to %s</string>
<string name="toast_call_transfer_failed">Call transfer to %s failed!</string>
<!-- Keep <u></u> in following strings translations! -->
<string name="welcome_carousel_skip"><u>Skip</u></string>