diff --git a/app/src/main/java/org/linphone/telecom/TelecomManager.kt b/app/src/main/java/org/linphone/telecom/TelecomManager.kt
index a87ba5385..4bf99493a 100644
--- a/app/src/main/java/org/linphone/telecom/TelecomManager.kt
+++ b/app/src/main/java/org/linphone/telecom/TelecomManager.kt
@@ -61,7 +61,11 @@ class TelecomManager @WorkerThread constructor(context: Context) {
} else {
CallAttributesCompat.DIRECTION_INCOMING
}
- val type = CallAttributesCompat.CALL_TYPE_AUDIO_CALL or CallAttributesCompat.CALL_TYPE_VIDEO_CALL
+ val type = if (core.isVideoEnabled) {
+ CallAttributesCompat.CALL_TYPE_VIDEO_CALL
+ } else {
+ CallAttributesCompat.CALL_TYPE_AUDIO_CALL
+ }
val capabilities = CallAttributesCompat.SUPPORTS_SET_INACTIVE or CallAttributesCompat.SUPPORTS_TRANSFER
val callAttributes = CallAttributesCompat(
@@ -71,21 +75,27 @@ class TelecomManager @WorkerThread constructor(context: Context) {
type,
capabilities
)
+ Log.i("$TAG Adding call to Telecom's CallsManager with attributes [$callAttributes]")
+
scope.launch {
- callsManager.addCall(callAttributes) {
- val callbacks = TelecomCallControlCallback(call, this, scope)
+ try {
+ callsManager.addCall(callAttributes) {
+ val callbacks = TelecomCallControlCallback(call, this, scope)
- coreContext.postOnCoreThread {
- val callId = call.callLog.callId.orEmpty()
- if (callId.isNotEmpty()) {
- Log.i("$TAG Storing our callbacks for call ID [$callId]")
- map[callId] = callbacks
+ coreContext.postOnCoreThread {
+ val callId = call.callLog.callId.orEmpty()
+ if (callId.isNotEmpty()) {
+ Log.i("$TAG Storing our callbacks for call ID [$callId]")
+ map[callId] = callbacks
+ }
}
- }
- setCallback(callbacks)
- // We must first call setCallback on callControlScope before using it
- callbacks.onCallControlCallbackSet()
+ setCallback(callbacks)
+ // We must first call setCallback on callControlScope before using it
+ callbacks.onCallControlCallbackSet()
+ }
+ } catch (e: Exception) {
+ Log.e("$TAG Failed to add call to Telecom's CallsManager!")
}
}
}
diff --git a/app/src/main/java/org/linphone/ui/voip/fragment/ActiveCallFragment.kt b/app/src/main/java/org/linphone/ui/voip/fragment/ActiveCallFragment.kt
index 54f68c992..cb285f645 100644
--- a/app/src/main/java/org/linphone/ui/voip/fragment/ActiveCallFragment.kt
+++ b/app/src/main/java/org/linphone/ui/voip/fragment/ActiveCallFragment.kt
@@ -26,9 +26,12 @@ import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
+import android.view.animation.Animation
+import android.view.animation.AnimationUtils
import androidx.annotation.UiThread
import androidx.constraintlayout.widget.ConstraintSet
import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
import androidx.window.layout.FoldingFeature
import com.google.android.material.bottomsheet.BottomSheetBehavior
import org.linphone.LinphoneApplication.Companion.coreContext
@@ -80,6 +83,14 @@ class ActiveCallFragment : GenericCallFragment() {
}
}
+ override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
+ if (findNavController().currentDestination?.id == R.id.newCallFragment) {
+ // Holds fragment in place while new contact fragment slides over it
+ return AnimationUtils.loadAnimation(activity, R.anim.hold)
+ }
+ return AnimationUtils.loadAnimation(activity, R.anim.hold)
+ }
+
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -102,6 +113,11 @@ class ActiveCallFragment : GenericCallFragment() {
val bottomSheetBehavior = BottomSheetBehavior.from(binding.bottomBar.root)
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
+ binding.setNewCallClickListener {
+ val action = ActiveCallFragmentDirections.actionActiveCallFragmentToNewCallFragment()
+ findNavController().navigate(action)
+ }
+
sharedViewModel = requireActivity().run {
ViewModelProvider(this)[SharedCallViewModel::class.java]
}
diff --git a/app/src/main/java/org/linphone/ui/voip/fragment/NewCallFragment.kt b/app/src/main/java/org/linphone/ui/voip/fragment/NewCallFragment.kt
new file mode 100644
index 000000000..bb30b8409
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/voip/fragment/NewCallFragment.kt
@@ -0,0 +1,268 @@
+/*
+ * 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.voip.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.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.tools.Log
+import org.linphone.databinding.CallStartFragmentBinding
+import org.linphone.ui.main.calls.adapter.ContactsAndSuggestionsListAdapter
+import org.linphone.ui.main.calls.model.ContactOrSuggestionModel
+import org.linphone.ui.main.calls.viewmodel.StartCallViewModel
+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.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() {
+ companion object {
+ private const val TAG = "[New Call Fragment]"
+ }
+
+ private lateinit var binding: CallStartFragmentBinding
+
+ private val viewModel: StartCallViewModel by navGraphViewModels(
+ R.id.voip_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 {
+ 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 = CallStartFragmentBinding.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()
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/drawable/chat_text.xml b/app/src/main/res/drawable/chat_text.xml
index 40d711414..a2adf78df 100644
--- a/app/src/main/res/drawable/chat_text.xml
+++ b/app/src/main/res/drawable/chat_text.xml
@@ -5,5 +5,5 @@
android:viewportHeight="256">
+ android:fillColor="#4e6074"/>
diff --git a/app/src/main/res/layout-land/voip_call_extra_actions.xml b/app/src/main/res/layout-land/voip_call_extra_actions.xml
index 9806d5311..336bcf291 100644
--- a/app/src/main/res/layout-land/voip_call_extra_actions.xml
+++ b/app/src/main/res/layout-land/voip_call_extra_actions.xml
@@ -5,6 +5,15 @@
+
+
+
@@ -30,6 +39,7 @@
+
@@ -23,7 +26,7 @@
+ android:background="@color/white">
+
+
+
+
@@ -205,7 +214,10 @@
android:id="@+id/bottom_bar"
android:visibility="@{viewModel.fullScreenMode || viewModel.pipMode ? View.INVISIBLE : View.VISIBLE}"
layout="@layout/voip_call_extra_actions"
- bind:viewModel="@{viewModel}"/>
+ bind:viewModel="@{viewModel}"
+ bind:transferClickListener="@{transferClickListener}"
+ bind:newCallClickListener="@{newCallClickListener}"
+ bind:callsListClickListener="@{callsListClickListener}"/>
diff --git a/app/src/main/res/layout/voip_call_extra_actions.xml b/app/src/main/res/layout/voip_call_extra_actions.xml
index 051c982ee..c97d3acda 100644
--- a/app/src/main/res/layout/voip_call_extra_actions.xml
+++ b/app/src/main/res/layout/voip_call_extra_actions.xml
@@ -5,6 +5,15 @@
+
+
+
@@ -30,6 +39,7 @@
+ tools:layout="@layout/voip_active_call_fragment">
+
+
+
+
\ No newline at end of file