mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-17 11:28:06 +00:00
Started blind call transfer
This commit is contained in:
parent
a60c66ad33
commit
027e5dd61b
14 changed files with 502 additions and 245 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>>()
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue