diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index f01a2ef03..710a46dc9 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -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) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index ba206ab77..790606bca 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -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>> by lazy { + MutableLiveData>>() + } + @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 { diff --git a/app/src/main/java/org/linphone/ui/call/CallActivity.kt b/app/src/main/java/org/linphone/ui/call/CallActivity.kt index e92375bbe..daf3ac75c 100644 --- a/app/src/main/java/org/linphone/ui/call/CallActivity.kt +++ b/app/src/main/java/org/linphone/ui/call/CallActivity.kt @@ -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 ) } diff --git a/app/src/main/java/org/linphone/ui/call/fragment/AbstractNewTransferCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/AbstractNewTransferCallFragment.kt new file mode 100644 index 000000000..e8c181ae8 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/call/fragment/AbstractNewTransferCallFragment.kt @@ -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 . + */ +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() + } + } + } + } +} diff --git a/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt index e4fbf026b..1840ff8a5 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt @@ -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) diff --git a/app/src/main/java/org/linphone/ui/call/fragment/NewCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/NewCallFragment.kt index 5a19a6265..13b344315 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/NewCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/NewCallFragment.kt @@ -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() } } } diff --git a/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt new file mode 100644 index 000000000..eddebca1a --- /dev/null +++ b/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.ui.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() + } + } +} diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index bfbe965af..40b1e145b 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -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>() } + val transferInProgressEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val transferFailedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + val numpadModel: NumpadModel val appendDigitToSearchBarEvent: MutableLiveData> 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 diff --git a/app/src/main/java/org/linphone/ui/main/MainActivity.kt b/app/src/main/java/org/linphone/ui/main/MainActivity.kt index dd62cb59e..68b44ef25 100644 --- a/app/src/main/java/org/linphone/ui/main/MainActivity.kt +++ b/app/src/main/java/org/linphone/ui/main/MainActivity.kt @@ -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( diff --git a/app/src/main/java/org/linphone/ui/main/history/fragment/StartCallFragment.kt b/app/src/main/java/org/linphone/ui/main/history/fragment/StartCallFragment.kt index 6fcae2b11..616a95813 100644 --- a/app/src/main/java/org/linphone/ui/main/history/fragment/StartCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/history/fragment/StartCallFragment.kt @@ -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 { diff --git a/app/src/main/java/org/linphone/ui/main/history/viewmodel/StartCallViewModel.kt b/app/src/main/java/org/linphone/ui/main/history/viewmodel/StartCallViewModel.kt index 2512e735b..6a15bcd43 100644 --- a/app/src/main/java/org/linphone/ui/main/history/viewmodel/StartCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/history/viewmodel/StartCallViewModel.kt @@ -45,6 +45,8 @@ class StartCallViewModel @UiThread constructor() : ViewModel() { private const val TAG = "[Start Call ViewModel]" } + val title = MutableLiveData() + val searchFilter = MutableLiveData() val contactsAndSuggestionsList = MutableLiveData>() diff --git a/app/src/main/res/layout/start_call_fragment.xml b/app/src/main/res/layout/start_call_fragment.xml index 2998aafef..bb566c51b 100644 --- a/app/src/main/res/layout/start_call_fragment.xml +++ b/app/src/main/res/layout/start_call_fragment.xml @@ -14,6 +14,9 @@ + @@ -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" /> diff --git a/app/src/main/res/navigation/call_nav_graph.xml b/app/src/main/res/navigation/call_nav_graph.xml index 3a30e026b..c59eb68bd 100644 --- a/app/src/main/res/navigation/call_nav_graph.xml +++ b/app/src/main/res/navigation/call_nav_graph.xml @@ -54,6 +54,12 @@ app:enterAnim="@anim/slide_in" app:popExitAnim="@anim/slide_out" app:launchSingleTop="true" /> + + tools:layout="@layout/start_call_fragment" > + + + + Paused Ended Calls list + Transfer call to + Call is being transferred to %s + Call has been transferred to %s + Call transfer to %s failed! Skip