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 deleted file mode 100644 index 5fb74a251..000000000 --- a/app/src/main/java/org/linphone/ui/call/fragment/AbstractNewTransferCallFragment.kt +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright (c) 2010-2023 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.ui.call.fragment - -import android.app.Dialog -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.annotation.UiThread -import androidx.annotation.WorkerThread -import androidx.core.view.doOnPreDraw -import androidx.navigation.fragment.findNavController -import androidx.navigation.navGraphViewModels -import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.bottomsheet.BottomSheetBehavior -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.contacts.getListOfSipAddressesAndPhoneNumbers -import org.linphone.core.Address -import org.linphone.core.tools.Log -import org.linphone.databinding.StartCallFragmentBinding -import org.linphone.ui.main.adapter.ConversationsContactsAndSuggestionsListAdapter -import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener -import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel -import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel -import org.linphone.ui.main.history.viewmodel.StartCallViewModel -import org.linphone.ui.main.model.ConversationContactOrSuggestionModel -import org.linphone.utils.DialogUtils -import org.linphone.utils.LinphoneUtils -import org.linphone.utils.RecyclerViewHeaderDecoration -import org.linphone.utils.hideKeyboard -import org.linphone.utils.setKeyboardInsetListener -import org.linphone.utils.showKeyboard - -abstract class AbstractNewTransferCallFragment : GenericCallFragment() { - companion object { - private const val TAG = "[New/Transfer Call Fragment]" - } - - private lateinit var binding: StartCallFragmentBinding - - private val viewModel: StartCallViewModel by navGraphViewModels( - R.id.call_nav_graph - ) - - private lateinit var adapter: ConversationsContactsAndSuggestionsListAdapter - - private val listener = object : ContactNumberOrAddressClickListener { - @UiThread - override fun onClicked(model: ContactNumberOrAddressModel) { - val address = model.address - if (address != null) { - coreContext.postOnCoreThread { - action(address) - } - } - } - - @UiThread - override fun onLongPress(model: ContactNumberOrAddressModel) { - } - } - - private var numberOrAddressPickerDialog: Dialog? = null - - abstract val title: String - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - adapter = ConversationsContactsAndSuggestionsListAdapter() - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = StartCallFragmentBinding.inflate(layoutInflater) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - viewModel.title.value = title - binding.viewModel = viewModel - observeToastEvents(viewModel) - - binding.setBackClickListener { - findNavController().popBackStack() - } - - binding.setHideNumpadClickListener { - viewModel.hideNumpad() - } - - binding.contactsAndSuggestionsList.setHasFixedSize(true) - - val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter) - binding.contactsAndSuggestionsList.addItemDecoration(headerItemDecoration) - - adapter.onClickedEvent.observe(viewLifecycleOwner) { - it.consume { model -> - startCall(model) - } - } - - binding.contactsAndSuggestionsList.layoutManager = LinearLayoutManager(requireContext()) - - viewModel.modelsList.observe( - viewLifecycleOwner - ) { - Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items") - val count = adapter.itemCount - adapter.submitList(it) - - // Wait for adapter to have items before setting it in the RecyclerView, - // otherwise scroll position isn't retained - if (binding.contactsAndSuggestionsList.adapter != adapter) { - binding.contactsAndSuggestionsList.adapter = adapter - } - - if (count == 0) { - (view.parent as? ViewGroup)?.doOnPreDraw { - startPostponedEnterTransition() - } - } - } - - viewModel.searchFilter.observe(viewLifecycleOwner) { filter -> - val trimmed = filter.trim() - viewModel.applyFilter(trimmed) - } - - viewModel.removedCharacterAtCurrentPositionEvent.observe(viewLifecycleOwner) { - it.consume { - val selectionStart = binding.searchBar.selectionStart - val selectionEnd = binding.searchBar.selectionEnd - if (selectionStart > 0) { - binding.searchBar.text = - binding.searchBar.text?.delete( - selectionStart - 1, - selectionEnd - ) - binding.searchBar.setSelection(selectionStart - 1) - } - } - } - - viewModel.appendDigitToSearchBarEvent.observe(viewLifecycleOwner) { - it.consume { digit -> - val newValue = "${binding.searchBar.text}$digit" - binding.searchBar.setText(newValue) - binding.searchBar.setSelection(newValue.length) - } - } - - viewModel.requestKeyboardVisibilityChangedEvent.observe(viewLifecycleOwner) { - it.consume { show -> - if (show) { - // To automatically open keyboard - binding.searchBar.showKeyboard() - } else { - binding.searchBar.requestFocus() - binding.searchBar.hideKeyboard() - } - } - } - - viewModel.isNumpadVisible.observe(viewLifecycleOwner) { visible -> - val standardBottomSheetBehavior = BottomSheetBehavior.from(binding.numpadLayout.root) - if (visible) { - standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED - } else { - standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED - } - } - - binding.root.setKeyboardInsetListener { keyboardVisible -> - if (keyboardVisible) { - viewModel.isNumpadVisible.value = false - } - } - } - - override fun onPause() { - super.onPause() - - numberOrAddressPickerDialog?.dismiss() - numberOrAddressPickerDialog = null - } - - @WorkerThread - abstract fun action(address: Address) - - private fun startCall(model: ConversationContactOrSuggestionModel) { - coreContext.postOnCoreThread { - val friend = model.friend - if (friend == null) { - action(model.address) - return@postOnCoreThread - } - - val singleAvailableAddress = LinphoneUtils.getSingleAvailableAddressForFriend(friend) - if (singleAvailableAddress != null) { - Log.i( - "$TAG Only 1 SIP address or phone number found for contact [${friend.name}], starting call directly" - ) - action(singleAvailableAddress) - } else { - val list = friend.getListOfSipAddressesAndPhoneNumbers(listener) - Log.i( - "$TAG [${list.size}] numbers or addresses found for contact [${friend.name}], showing selection dialog" - ) - - coreContext.postOnMainThread { - val numberOrAddressModel = NumberOrAddressPickerDialogModel(list) - val dialog = - DialogUtils.getNumberOrAddressPickerDialog( - requireActivity(), - numberOrAddressModel - ) - numberOrAddressPickerDialog = dialog - - numberOrAddressModel.dismissEvent.observe(viewLifecycleOwner) { event -> - event.consume { - dialog.dismiss() - } - } - - dialog.show() - } - } - } - } -} 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 cbb091a06..5c77bf5e8 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 @@ -214,6 +214,11 @@ class ActiveCallFragment : GenericCallFragment() { requireActivity().finish() } + binding.setTransferCallClickListener { + val action = ActiveCallFragmentDirections.actionActiveCallFragmentToTransferCallFragment() + findNavController().navigate(action) + } + binding.setNewCallClickListener { val action = ActiveCallFragmentDirections.actionActiveCallFragmentToNewCallFragment() findNavController().navigate(action) @@ -242,13 +247,6 @@ class ActiveCallFragment : GenericCallFragment() { updateHingeRelatedConstraints(feature) } - callViewModel.goToInitiateBlindTransferEvent.observe(viewLifecycleOwner) { - it.consume { - val action = ActiveCallFragmentDirections.actionActiveCallFragmentToTransferCallFragment() - findNavController().navigate(action) - } - } - callViewModel.fullScreenMode.observe(viewLifecycleOwner) { hide -> Log.i("$TAG Switching full screen mode to ${if (hide) "ON" else "OFF"}") sharedViewModel.toggleFullScreenEvent.value = Event(hide) 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 3057c346d..b44437249 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,25 +19,238 @@ */ package org.linphone.ui.call.fragment +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import androidx.annotation.UiThread import androidx.annotation.WorkerThread +import androidx.core.view.doOnPreDraw import androidx.navigation.fragment.findNavController +import androidx.navigation.navGraphViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.bottomsheet.BottomSheetBehavior import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R +import org.linphone.contacts.getListOfSipAddressesAndPhoneNumbers import org.linphone.core.Address import org.linphone.core.tools.Log +import org.linphone.databinding.StartCallFragmentBinding +import org.linphone.ui.main.adapter.ConversationsContactsAndSuggestionsListAdapter +import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener +import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel +import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel +import org.linphone.ui.main.history.viewmodel.StartCallViewModel +import org.linphone.ui.main.model.ConversationContactOrSuggestionModel +import org.linphone.utils.DialogUtils +import org.linphone.utils.LinphoneUtils +import org.linphone.utils.RecyclerViewHeaderDecoration +import org.linphone.utils.hideKeyboard +import org.linphone.utils.setKeyboardInsetListener +import org.linphone.utils.showKeyboard -@UiThread -class NewCallFragment : AbstractNewTransferCallFragment() { +class NewCallFragment : GenericCallFragment() { companion object { private const val TAG = "[New Call Fragment]" } - override val title: String - get() = getString(R.string.call_action_start_new_call) + private lateinit var binding: StartCallFragmentBinding + + private val viewModel: StartCallViewModel by navGraphViewModels( + R.id.call_nav_graph + ) + + private lateinit var adapter: ConversationsContactsAndSuggestionsListAdapter + + private val listener = object : ContactNumberOrAddressClickListener { + @UiThread + override fun onClicked(model: ContactNumberOrAddressModel) { + val address = model.address + if (address != null) { + coreContext.postOnCoreThread { + action(address) + } + } + } + + @UiThread + override fun onLongPress(model: ContactNumberOrAddressModel) { + } + } + + private var numberOrAddressPickerDialog: Dialog? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + adapter = ConversationsContactsAndSuggestionsListAdapter() + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = StartCallFragmentBinding.inflate(layoutInflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.lifecycleOwner = viewLifecycleOwner + + binding.viewModel = viewModel + observeToastEvents(viewModel) + + binding.setBackClickListener { + findNavController().popBackStack() + } + + binding.setHideNumpadClickListener { + viewModel.hideNumpad() + } + + binding.contactsAndSuggestionsList.setHasFixedSize(true) + + val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter) + binding.contactsAndSuggestionsList.addItemDecoration(headerItemDecoration) + + adapter.onClickedEvent.observe(viewLifecycleOwner) { + it.consume { model -> + startCall(model) + } + } + + binding.contactsAndSuggestionsList.layoutManager = LinearLayoutManager(requireContext()) + + viewModel.modelsList.observe( + viewLifecycleOwner + ) { + Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items") + val count = adapter.itemCount + adapter.submitList(it) + + // Wait for adapter to have items before setting it in the RecyclerView, + // otherwise scroll position isn't retained + if (binding.contactsAndSuggestionsList.adapter != adapter) { + binding.contactsAndSuggestionsList.adapter = adapter + } + + if (count == 0) { + (view.parent as? ViewGroup)?.doOnPreDraw { + startPostponedEnterTransition() + } + } + } + + viewModel.searchFilter.observe(viewLifecycleOwner) { filter -> + val trimmed = filter.trim() + viewModel.applyFilter(trimmed) + } + + viewModel.removedCharacterAtCurrentPositionEvent.observe(viewLifecycleOwner) { + it.consume { + val selectionStart = binding.searchBar.selectionStart + val selectionEnd = binding.searchBar.selectionEnd + if (selectionStart > 0) { + binding.searchBar.text = + binding.searchBar.text?.delete( + selectionStart - 1, + selectionEnd + ) + binding.searchBar.setSelection(selectionStart - 1) + } + } + } + + viewModel.appendDigitToSearchBarEvent.observe(viewLifecycleOwner) { + it.consume { digit -> + val newValue = "${binding.searchBar.text}$digit" + binding.searchBar.setText(newValue) + binding.searchBar.setSelection(newValue.length) + } + } + + viewModel.requestKeyboardVisibilityChangedEvent.observe(viewLifecycleOwner) { + it.consume { show -> + if (show) { + // To automatically open keyboard + binding.searchBar.showKeyboard() + } else { + binding.searchBar.requestFocus() + binding.searchBar.hideKeyboard() + } + } + } + + viewModel.isNumpadVisible.observe(viewLifecycleOwner) { visible -> + val standardBottomSheetBehavior = BottomSheetBehavior.from(binding.numpadLayout.root) + if (visible) { + standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + } else { + standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + } + } + + binding.root.setKeyboardInsetListener { keyboardVisible -> + if (keyboardVisible) { + viewModel.isNumpadVisible.value = false + } + } + } + + override fun onPause() { + super.onPause() + + numberOrAddressPickerDialog?.dismiss() + numberOrAddressPickerDialog = null + } + + private fun startCall(model: ConversationContactOrSuggestionModel) { + coreContext.postOnCoreThread { + val friend = model.friend + if (friend == null) { + action(model.address) + return@postOnCoreThread + } + + val singleAvailableAddress = LinphoneUtils.getSingleAvailableAddressForFriend(friend) + if (singleAvailableAddress != null) { + Log.i( + "$TAG Only 1 SIP address or phone number found for contact [${friend.name}], starting call directly" + ) + action(singleAvailableAddress) + } else { + val list = friend.getListOfSipAddressesAndPhoneNumbers(listener) + Log.i( + "$TAG [${list.size}] numbers or addresses found for contact [${friend.name}], showing selection dialog" + ) + + coreContext.postOnMainThread { + val numberOrAddressModel = NumberOrAddressPickerDialogModel(list) + val dialog = + DialogUtils.getNumberOrAddressPickerDialog( + requireActivity(), + numberOrAddressModel + ) + numberOrAddressPickerDialog = dialog + + numberOrAddressModel.dismissEvent.observe(viewLifecycleOwner) { event -> + event.consume { + dialog.dismiss() + } + } + + dialog.show() + } + } + } + } @WorkerThread - override fun action(address: Address) { + private fun action(address: Address) { Log.i("$TAG Calling [${address.asStringUriOnly()}]") coreContext.startAudioCall(address) 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 index 34801a668..1b5c23c18 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt @@ -19,48 +19,305 @@ */ package org.linphone.ui.call.fragment +import android.app.Dialog import android.os.Bundle +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import androidx.annotation.UiThread -import androidx.annotation.WorkerThread +import androidx.core.view.doOnPreDraw import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController +import androidx.navigation.navGraphViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.bottomsheet.BottomSheetBehavior +import kotlin.getValue import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R -import org.linphone.core.Address import org.linphone.core.tools.Log +import org.linphone.databinding.CallTransferFragmentBinding +import org.linphone.ui.call.adapter.CallsListAdapter +import org.linphone.ui.call.model.CallModel +import org.linphone.ui.call.model.ConfirmCallTransferDialogModel +import org.linphone.ui.call.viewmodel.CallsViewModel import org.linphone.ui.call.viewmodel.CurrentCallViewModel +import org.linphone.ui.main.adapter.ConversationsContactsAndSuggestionsListAdapter +import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener +import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel +import org.linphone.ui.main.history.viewmodel.StartCallViewModel +import org.linphone.ui.main.model.ConversationContactOrSuggestionModel +import org.linphone.utils.DialogUtils +import org.linphone.utils.RecyclerViewHeaderDecoration +import org.linphone.utils.hideKeyboard +import org.linphone.utils.setKeyboardInsetListener +import org.linphone.utils.showKeyboard @UiThread -class TransferCallFragment : AbstractNewTransferCallFragment() { +class TransferCallFragment : GenericCallFragment() { companion object { private const val TAG = "[Transfer Call Fragment]" } - override val title: String - get() = getString(R.string.call_transfer_title) + private lateinit var binding: CallTransferFragmentBinding + + private val viewModel: StartCallViewModel by navGraphViewModels( + R.id.call_nav_graph + ) private lateinit var callViewModel: CurrentCallViewModel + private lateinit var callsViewModel: CallsViewModel + + private lateinit var callsAdapter: CallsListAdapter + + private lateinit var contactsAdapter: ConversationsContactsAndSuggestionsListAdapter + + private var numberOrAddressPickerDialog: Dialog? = null + + private val listener = object : ContactNumberOrAddressClickListener { + @UiThread + override fun onClicked(model: ContactNumberOrAddressModel) { + val address = model.address + if (address != null) { + coreContext.postOnCoreThread { + // TODO FIXME: transfer call (blind) + } + } + } + + @UiThread + override fun onLongPress(model: ContactNumberOrAddressModel) { + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + callsAdapter = CallsListAdapter() + contactsAdapter = ConversationsContactsAndSuggestionsListAdapter() + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = CallTransferFragmentBinding.inflate(layoutInflater) + return binding.root + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding.lifecycleOwner = viewLifecycleOwner + callViewModel = requireActivity().run { ViewModelProvider(this)[CurrentCallViewModel::class.java] } - } - @WorkerThread - override fun action(address: Address) { - Log.i("$TAG Transferring current call to [${address.asStringUriOnly()}]") - callViewModel.blindTransferCallTo(address) + callsViewModel = requireActivity().run { + ViewModelProvider(this)[CallsViewModel::class.java] + } - coreContext.postOnMainThread { - try { - findNavController().popBackStack() - } catch (ise: IllegalStateException) { - Log.e("$TAG Can't go back: $ise") + binding.viewModel = viewModel + binding.callsViewModel = callsViewModel + observeToastEvents(viewModel) + + binding.setBackClickListener { + findNavController().popBackStack() + } + + binding.setHideNumpadClickListener { + viewModel.hideNumpad() + } + + binding.callsList.setHasFixedSize(true) + binding.contactsAndSuggestionsList.setHasFixedSize(true) + + callsAdapter.callClickedEvent.observe(viewLifecycleOwner) { + it.consume { model -> + showConfirmAttendedTransferDialog(model) + } + } + + val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), contactsAdapter) + binding.contactsAndSuggestionsList.addItemDecoration(headerItemDecoration) + + contactsAdapter.onClickedEvent.observe(viewLifecycleOwner) { + it.consume { model -> + showConfirmBlindTransferDialog(model) + } + } + + callsViewModel.callsExceptCurrentOne.observe(viewLifecycleOwner) { + Log.i("$TAG Calls list updated with [${it.size}] items") + callsAdapter.submitList(it) + + // Wait for adapter to have items before setting it in the RecyclerView, + // otherwise scroll position isn't retained + if (binding.callsList.adapter != callsAdapter) { + binding.callsList.adapter = callsAdapter + } + } + + binding.contactsAndSuggestionsList.layoutManager = LinearLayoutManager(requireContext()) + binding.callsList.layoutManager = LinearLayoutManager(requireContext()) + + viewModel.modelsList.observe( + viewLifecycleOwner + ) { + Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items") + val count = contactsAdapter.itemCount + contactsAdapter.submitList(it) + + // Wait for adapter to have items before setting it in the RecyclerView, + // otherwise scroll position isn't retained + if (binding.contactsAndSuggestionsList.adapter != contactsAdapter) { + binding.contactsAndSuggestionsList.adapter = contactsAdapter + } + + if (count == 0) { + (view.parent as? ViewGroup)?.doOnPreDraw { + startPostponedEnterTransition() + } + } + } + + viewModel.searchFilter.observe(viewLifecycleOwner) { filter -> + val trimmed = filter.trim() + viewModel.applyFilter(trimmed) + } + + viewModel.removedCharacterAtCurrentPositionEvent.observe(viewLifecycleOwner) { + it.consume { + val selectionStart = binding.searchBar.selectionStart + val selectionEnd = binding.searchBar.selectionEnd + if (selectionStart > 0) { + binding.searchBar.text = + binding.searchBar.text?.delete( + selectionStart - 1, + selectionEnd + ) + binding.searchBar.setSelection(selectionStart - 1) + } + } + } + + viewModel.appendDigitToSearchBarEvent.observe(viewLifecycleOwner) { + it.consume { digit -> + val newValue = "${binding.searchBar.text}$digit" + binding.searchBar.setText(newValue) + binding.searchBar.setSelection(newValue.length) + } + } + + viewModel.requestKeyboardVisibilityChangedEvent.observe(viewLifecycleOwner) { + it.consume { show -> + if (show) { + // To automatically open keyboard + binding.searchBar.showKeyboard() + } else { + binding.searchBar.requestFocus() + binding.searchBar.hideKeyboard() + } + } + } + + viewModel.isNumpadVisible.observe(viewLifecycleOwner) { visible -> + val standardBottomSheetBehavior = BottomSheetBehavior.from(binding.numpadLayout.root) + if (visible) { + standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + } else { + standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + } + } + + binding.root.setKeyboardInsetListener { keyboardVisible -> + if (keyboardVisible) { + viewModel.isNumpadVisible.value = false } } } + + override fun onPause() { + super.onPause() + + numberOrAddressPickerDialog?.dismiss() + numberOrAddressPickerDialog = null + } + + override fun onResume() { + super.onResume() + + viewModel.title.value = getString( + R.string.call_transfer_current_call_title, + callViewModel.displayedName.value ?: callViewModel.displayedAddress.value + ) + } + + private fun showConfirmAttendedTransferDialog(callModel: CallModel) { + val model = ConfirmCallTransferDialogModel( + callViewModel.displayedName.value.orEmpty(), + callModel.displayName.value.orEmpty() + ) + val dialog = DialogUtils.getConfirmCallTransferCallDialog( + requireActivity(), + model + ) + + model.cancelEvent.observe(viewLifecycleOwner) { + it.consume { + dialog.dismiss() + } + } + + model.confirmEvent.observe(viewLifecycleOwner) { + it.consume { + coreContext.postOnCoreThread { + val call = callModel.call + Log.i( + "$TAG Transferring (attended) call to [${call.remoteAddress.asStringUriOnly()}]" + ) + callViewModel.attendedTransferCallTo(call) + } + + dialog.dismiss() + findNavController().popBackStack() + } + } + + dialog.show() + } + + private fun showConfirmBlindTransferDialog(contactModel: ConversationContactOrSuggestionModel) { + val model = ConfirmCallTransferDialogModel( + callViewModel.displayedName.value.orEmpty(), + contactModel.name + ) + val dialog = DialogUtils.getConfirmCallTransferCallDialog( + requireActivity(), + model + ) + + model.cancelEvent.observe(viewLifecycleOwner) { + it.consume { + dialog.dismiss() + } + } + + model.confirmEvent.observe(viewLifecycleOwner) { + it.consume { + coreContext.postOnCoreThread { + val address = contactModel.address + Log.i("$TAG Transferring (blind) call to [${address.asStringUriOnly()}]") + callViewModel.blindTransferCallTo(address) + } + + dialog.dismiss() + findNavController().popBackStack() + } + } + + dialog.show() + } } diff --git a/app/src/main/java/org/linphone/ui/call/model/ConfirmCallTransferDialogModel.kt b/app/src/main/java/org/linphone/ui/call/model/ConfirmCallTransferDialogModel.kt new file mode 100644 index 000000000..f1a5ad06e --- /dev/null +++ b/app/src/main/java/org/linphone/ui/call/model/ConfirmCallTransferDialogModel.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2010-2024 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.ui.call.model + +import androidx.annotation.UiThread +import androidx.lifecycle.MutableLiveData +import org.linphone.utils.AppUtils +import org.linphone.utils.Event + +class ConfirmCallTransferDialogModel @UiThread constructor( + toTransfer: String, + toReceiveTransfer: String +) { + val cancelEvent = MutableLiveData>() + + val confirmEvent = MutableLiveData>() + + val message = MutableLiveData() + + init { + message.value = AppUtils.getFormattedString( + org.linphone.R.string.call_transfer_confirm_dialog_message, + toTransfer, + toReceiveTransfer + ) + } + + @UiThread + fun cancel() { + cancelEvent.value = Event(true) + } + + @UiThread + fun confirm() { + confirmEvent.value = Event(true) + } +} diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt index 0ee91f92a..1c33808e8 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt @@ -41,6 +41,8 @@ class CallsViewModel @UiThread constructor() : GenericViewModel() { val calls = MutableLiveData>() + val callsExceptCurrentOne = MutableLiveData>() + val callsCount = MutableLiveData() val showTopBar = MutableLiveData() @@ -237,6 +239,15 @@ class CallsViewModel @UiThread constructor() : GenericViewModel() { private fun updateOtherCallsInfo() { val core = coreContext.core + callsExceptCurrentOne.value.orEmpty().forEach(CallModel::destroy) + val list = arrayListOf() + for (call in core.calls) { + if (call != core.currentCall) { + list.add(CallModel(call)) + } + } + callsExceptCurrentOne.postValue(list) + if (core.callsNb > 1) { showTopBar.postValue(true) if (core.callsNb == 2) { 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 addae1256..81d5dc4ba 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 @@ -138,10 +138,6 @@ class CurrentCallViewModel @UiThread constructor() : GenericViewModel() { MutableLiveData>>() } - val goToInitiateBlindTransferEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - val goToEndedCallEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -236,7 +232,7 @@ class CurrentCallViewModel @UiThread constructor() : GenericViewModel() { MutableLiveData>() } - private lateinit var currentCall: Call + lateinit var currentCall: Call private val callListener = object : CallListenerStub() { @WorkerThread @@ -874,35 +870,6 @@ class CurrentCallViewModel @UiThread constructor() : GenericViewModel() { showNumpadBottomSheetEvent.value = Event(true) } - @UiThread - fun transferClicked() { - coreContext.postOnCoreThread { core -> - if (core.callsNb == 1) { - Log.i("$TAG Only one call, initiate blind call transfer") - goToInitiateBlindTransferEvent.postValue(Event(true)) - } else { - val callToTransferTo = core.calls.findLast { - it.state == Call.State.Paused && it != currentCall - } - if (callToTransferTo == null) { - Log.e( - "$TAG Couldn't find a call in Paused state to transfer current call to" - ) - return@postOnCoreThread - } - - Log.i( - "$TAG Doing an attended transfer between currently displayed call [${currentCall.remoteAddress.asStringUriOnly()}] and paused call [${callToTransferTo.remoteAddress.asStringUriOnly()}]" - ) - if (callToTransferTo.transferToAnother(currentCall) != 0) { - Log.e("$TAG Failed to make attended transfer!") - } else { - Log.i("$TAG Attended transfer is successful") - } - } - } - } - @UiThread fun createConversation() { if (::currentCall.isInitialized) { @@ -930,6 +897,20 @@ class CurrentCallViewModel @UiThread constructor() : GenericViewModel() { } } + @WorkerThread + fun attendedTransferCallTo(to: Call) { + if (::currentCall.isInitialized) { + Log.i( + "$TAG Doing an attended transfer between currently displayed call [${currentCall.remoteAddress.asStringUriOnly()}] and paused call [${to.remoteAddress.asStringUriOnly()}]" + ) + if (to.transferToAnother(currentCall) == 0) { + Log.i("$TAG Attended transfer is successful") + } else { + Log.e("$TAG Failed to make attended transfer!") + } + } + } + @WorkerThread fun blindTransferCallTo(to: Address) { if (::currentCall.isInitialized) { diff --git a/app/src/main/java/org/linphone/ui/main/contacts/model/TrustCallDialogModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/model/TrustCallDialogModel.kt index 6a30a984d..62b6ce21c 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/model/TrustCallDialogModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/model/TrustCallDialogModel.kt @@ -21,6 +21,7 @@ package org.linphone.ui.main.contacts.model import androidx.annotation.UiThread import androidx.lifecycle.MutableLiveData +import org.linphone.utils.AppUtils import org.linphone.utils.Event class TrustCallDialogModel @UiThread constructor(contact: String, device: String) { @@ -33,7 +34,11 @@ class TrustCallDialogModel @UiThread constructor(contact: String, device: String val confirmCallEvent = MutableLiveData>() init { - message.value = "You're about to call $contact's device $device.\nAre you sure you want to make a call now?" + message.value = AppUtils.getFormattedString( + org.linphone.R.string.contact_dialog_increase_trust_level_message, + contact, + device + ) } @UiThread diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt index 06a0c83a5..a4f7d09db 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt @@ -129,8 +129,6 @@ class MainViewModel @UiThread constructor() : ViewModel() { Log.i("$TAG Last call ended, removing in-call 'alert'") removeAlert(SINGLE_CALL) atLeastOneCall.postValue(false) - - // TODO: do not do it if nothing has changed! computeNonDefaultAccountNotificationsCount() } diff --git a/app/src/main/java/org/linphone/utils/AndroidUtils.kt b/app/src/main/java/org/linphone/utils/AndroidUtils.kt index dfda8bf9f..c6d0ebaf7 100644 --- a/app/src/main/java/org/linphone/utils/AndroidUtils.kt +++ b/app/src/main/java/org/linphone/utils/AndroidUtils.kt @@ -70,8 +70,13 @@ class AppUtils { } @AnyThread - fun getFormattedString(@StringRes id: Int, args: Any): String { - return coreContext.context.getString(id, args) + fun getFormattedString(@StringRes id: Int, arg: Any): String { + return coreContext.context.getString(id, arg) + } + + @AnyThread + fun getFormattedString(@StringRes id: Int, arg1: Any, arg2: Any): String { + return coreContext.context.getString(id, arg1, arg2) } @AnyThread diff --git a/app/src/main/java/org/linphone/utils/DialogUtils.kt b/app/src/main/java/org/linphone/utils/DialogUtils.kt index c49f0349e..d58f1b2cd 100644 --- a/app/src/main/java/org/linphone/utils/DialogUtils.kt +++ b/app/src/main/java/org/linphone/utils/DialogUtils.kt @@ -36,6 +36,7 @@ import androidx.lifecycle.LifecycleOwner import org.linphone.R import org.linphone.databinding.DialogAssistantAcceptConditionsAndPolicyBinding import org.linphone.databinding.DialogAssistantCreateAccountConfirmPhoneNumberBinding +import org.linphone.databinding.DialogCallConfirmTransferBinding import org.linphone.databinding.DialogCancelContactChangesBinding import org.linphone.databinding.DialogCancelMeetingBinding import org.linphone.databinding.DialogContactConfirmTrustCallBinding @@ -59,6 +60,7 @@ import org.linphone.databinding.DialogZrtpSasValidationBinding import org.linphone.databinding.DialogZrtpSecurityAlertBinding import org.linphone.ui.assistant.model.AcceptConditionsAndPolicyDialogModel import org.linphone.ui.assistant.model.ConfirmPhoneNumberDialogModel +import org.linphone.ui.call.model.ConfirmCallTransferDialogModel import org.linphone.ui.call.model.ZrtpAlertDialogModel import org.linphone.ui.call.model.ZrtpSasConfirmationDialogModel import org.linphone.ui.main.contacts.model.ContactTrustDialogModel @@ -430,6 +432,22 @@ class DialogUtils { return getDialog(context, binding) } + @UiThread + fun getConfirmCallTransferCallDialog( + context: Context, + viewModel: ConfirmCallTransferDialogModel + ): Dialog { + val binding: DialogCallConfirmTransferBinding = DataBindingUtil.inflate( + LayoutInflater.from(context), + R.layout.dialog_call_confirm_transfer, + null, + false + ) + binding.viewModel = viewModel + + return getDialog(context, binding) + } + @UiThread fun getKickConferenceParticipantConfirmationDialog( context: Context, diff --git a/app/src/main/res/layout-land/call_actions_bottom_sheet.xml b/app/src/main/res/layout-land/call_actions_bottom_sheet.xml index af64e2a6f..b2cfa84e8 100644 --- a/app/src/main/res/layout-land/call_actions_bottom_sheet.xml +++ b/app/src/main/res/layout-land/call_actions_bottom_sheet.xml @@ -5,6 +5,9 @@ + @@ -39,14 +42,14 @@ diff --git a/app/src/main/res/layout/call_actions_bottom_sheet.xml b/app/src/main/res/layout/call_actions_bottom_sheet.xml index 73fc9265e..06f089466 100644 --- a/app/src/main/res/layout/call_actions_bottom_sheet.xml +++ b/app/src/main/res/layout/call_actions_bottom_sheet.xml @@ -5,6 +5,9 @@ + @@ -39,14 +42,14 @@ diff --git a/app/src/main/res/layout/call_active_fragment.xml b/app/src/main/res/layout/call_active_fragment.xml index 8f2665606..a99e81887 100644 --- a/app/src/main/res/layout/call_active_fragment.xml +++ b/app/src/main/res/layout/call_active_fragment.xml @@ -8,6 +8,9 @@ + @@ -277,6 +280,7 @@ layout="@layout/call_actions_bottom_sheet" bind:viewModel="@{viewModel}" bind:callsViewModel="@{callsViewModel}" + bind:transferCallClickListener="@{transferCallClickListener}" bind:newCallClickListener="@{newCallClickListener}" bind:callsListClickListener="@{callsListClickListener}"/> diff --git a/app/src/main/res/layout/call_transfer_fragment.xml b/app/src/main/res/layout/call_transfer_fragment.xml new file mode 100644 index 000000000..2f148db08 --- /dev/null +++ b/app/src/main/res/layout/call_transfer_fragment.xml @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_call_confirm_transfer.xml b/app/src/main/res/layout/dialog_call_confirm_transfer.xml new file mode 100644 index 000000000..dd572708e --- /dev/null +++ b/app/src/main/res/layout/dialog_call_confirm_transfer.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/start_call_fragment.xml b/app/src/main/res/layout/start_call_fragment.xml index 79769e2a3..df35a6089 100644 --- a/app/src/main/res/layout/start_call_fragment.xml +++ b/app/src/main/res/layout/start_call_fragment.xml @@ -55,7 +55,7 @@ android:layout_height="wrap_content" android:layout_marginStart="10dp" android:layout_marginEnd="10dp" - android:text="@{viewModel.title, default=@string/history_call_start_title}" + android:text="@string/call_action_start_new_call" app:layout_constraintBottom_toBottomOf="@id/back" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/back" diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a0ca0bcf7..fe71639ef 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -572,9 +572,13 @@ Appel entrant Appel terminé Appel entrant pour %s + Transférer %s à… + Appels en cours + Pas d\'autre appel + Confirmer le transfert + Vous allez transférer %1$s à %2$s. - Transfert - Transfert + Transfert Nouvel appel Liste des appels Pavé @@ -599,7 +603,6 @@ Appel chiffré de point à point Appel non chiffré Liste des appels - Transférer l\'appel vers %s est en train d\'enregistrer %s appels %s appels en pause diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4b7074cde..bb5960641 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -610,9 +610,13 @@ Incoming call Call ended Incoming call for %s + Transfer %s to… + Current calls + No other call + Confirm call transfer + You\'re about to transfer call %1$s to %2$s. - Transfer - Attended transfer + Transfer New call Calls list Dialer @@ -637,7 +641,6 @@ Point-to-point encrypted by SRTP Call is not encrypted Calls list - Transfer call to %s is recording %s calls %s paused calls