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