Started new call fragment while in call

This commit is contained in:
Sylvain Berfini 2023-09-18 10:37:39 +02:00
parent 88c5e28577
commit d1029af180
9 changed files with 388 additions and 31 deletions

View file

@ -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!")
}
}
}

View file

@ -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]
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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()
}
}
}
}
}

View file

@ -5,5 +5,5 @@
android:viewportHeight="256">
<path
android:pathData="M216,48L40,48A16,16 0,0 0,24 64L24,224a15.85,15.85 0,0 0,9.24 14.5A16.13,16.13 0,0 0,40 240a15.89,15.89 0,0 0,10.25 -3.78,0.69 0.69,0 0,0 0.13,-0.11L82.5,208L216,208a16,16 0,0 0,16 -16L232,64A16,16 0,0 0,216 48ZM40,224h0ZM216,192L82.5,192a16,16 0,0 0,-10.3 3.75l-0.12,0.11L40,224L40,64L216,64ZM88,112a8,8 0,0 1,8 -8h64a8,8 0,0 1,0 16L96,120A8,8 0,0 1,88 112ZM88,144a8,8 0,0 1,8 -8h64a8,8 0,1 1,0 16L96,152A8,8 0,0 1,88 144Z"
android:fillColor="#ffffff"/>
android:fillColor="#4e6074"/>
</vector>

View file

@ -5,6 +5,15 @@
<data>
<import type="android.view.View" />
<variable
name="transferClickListener"
type="View.OnClickListener" />
<variable
name="newCallClickListener"
type="View.OnClickListener" />
<variable
name="callsListClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.voip.viewmodel.CurrentCallViewModel" />
@ -30,6 +39,7 @@
<ImageView
android:id="@+id/transfer"
android:onClick="@{transferClickListener}"
android:layout_width="0dp"
android:layout_height="@dimen/voip_button_size"
android:layout_marginTop="@dimen/voip_extra_button_top_margin"
@ -44,6 +54,7 @@
<ImageView
android:id="@+id/new_call"
android:onClick="@{newCallClickListener}"
android:layout_width="0dp"
android:layout_height="@dimen/voip_button_size"
android:layout_marginTop="@dimen/voip_extra_button_top_margin"
@ -58,6 +69,7 @@
<ImageView
android:id="@+id/calls_list"
android:onClick="@{callsListClickListener}"
android:layout_width="0dp"
android:layout_height="@dimen/voip_button_size"
android:layout_marginTop="@dimen/voip_extra_button_top_margin"
@ -127,8 +139,9 @@
app:tint="@color/white" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/transfer_label"
style="@style/in_call_extra_action_label_style"
android:id="@+id/transfer_label"
android:onClick="@{transferClickListener}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingBottom="15dp"
@ -138,8 +151,9 @@
app:layout_constraintTop_toBottomOf="@id/transfer"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/new_call_label"
style="@style/in_call_extra_action_label_style"
android:id="@+id/new_call_label"
android:onClick="@{newCallClickListener}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingBottom="15dp"
@ -149,8 +163,9 @@
app:layout_constraintTop_toBottomOf="@id/new_call"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/calls_list_label"
style="@style/in_call_extra_action_label_style"
android:id="@+id/calls_list_label"
android:onClick="@{callsListClickListener}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingBottom="15dp"
@ -160,8 +175,8 @@
app:layout_constraintTop_toBottomOf="@id/calls_list"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/dialer_label"
style="@style/in_call_extra_action_label_style"
android:id="@+id/dialer_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingBottom="15dp"
@ -171,8 +186,8 @@
app:layout_constraintTop_toBottomOf="@id/chat" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/chat_label"
style="@style/in_call_extra_action_label_style"
android:id="@+id/chat_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingBottom="15dp"
@ -182,8 +197,8 @@
app:layout_constraintTop_toBottomOf="@id/chat" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/pause_call_label"
style="@style/in_call_extra_action_label_style"
android:id="@+id/pause_call_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingBottom="15dp"
@ -193,8 +208,8 @@
app:layout_constraintTop_toBottomOf="@id/pause_call" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/record_call_label"
style="@style/in_call_extra_action_label_style"
android:id="@+id/record_call_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingBottom="15dp"

View file

@ -11,6 +11,9 @@
<variable
name="hideNumpadClickListener"
type="View.OnClickListener" />
<variable
name="hideGroupChatButton"
type="Boolean" />
<variable
name="viewModel"
type="org.linphone.ui.main.calls.viewmodel.StartCallViewModel" />
@ -23,7 +26,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/gray_7">
android:background="@color/white">
<ImageView
android:id="@+id/back"
@ -55,7 +58,6 @@
android:id="@+id/background"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -113,6 +115,12 @@
app:layout_constraintTop_toTopOf="@id/search_bar"
app:tint="@color/gray_9" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_call"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="group_call_icon, gradient_background, group_call_label"
android:visibility="@{hideGroupChatButton ? View.GONE : View.VISIBLE}" />
<!-- margin start must be half the size of the group_call_icon below -->
<View

View file

@ -5,6 +5,15 @@
<data>
<import type="android.view.View" />
<variable
name="transferClickListener"
type="View.OnClickListener" />
<variable
name="newCallClickListener"
type="View.OnClickListener" />
<variable
name="callsListClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.voip.viewmodel.CurrentCallViewModel" />
@ -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}"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -5,6 +5,15 @@
<data>
<import type="android.view.View" />
<variable
name="transferClickListener"
type="View.OnClickListener" />
<variable
name="newCallClickListener"
type="View.OnClickListener" />
<variable
name="callsListClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.voip.viewmodel.CurrentCallViewModel" />
@ -30,6 +39,7 @@
<ImageView
android:id="@+id/transfer"
android:onClick="@{transferClickListener}"
android:layout_width="0dp"
android:layout_height="@dimen/voip_button_size"
android:layout_marginTop="@dimen/voip_extra_button_top_margin"
@ -44,6 +54,7 @@
<ImageView
android:id="@+id/new_call"
android:onClick="@{newCallClickListener}"
android:layout_width="0dp"
android:layout_height="@dimen/voip_button_size"
android:layout_marginTop="@dimen/voip_extra_button_top_margin"
@ -58,6 +69,7 @@
<ImageView
android:id="@+id/calls_list"
android:onClick="@{callsListClickListener}"
android:layout_width="0dp"
android:layout_height="@dimen/voip_button_size"
android:layout_marginTop="@dimen/voip_extra_button_top_margin"
@ -127,8 +139,9 @@
app:layout_constraintEnd_toEndOf="@id/calls_list" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/transfer_label"
style="@style/in_call_extra_action_label_style"
android:id="@+id/transfer_label"
android:onClick="@{transferClickListener}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/voip_action_call_transfer"
@ -137,8 +150,9 @@
app:layout_constraintEnd_toStartOf="@id/new_call_label"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/new_call_label"
style="@style/in_call_extra_action_label_style"
android:id="@+id/new_call_label"
android:onClick="@{newCallClickListener}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/voip_action_start_new_call"
@ -147,8 +161,9 @@
app:layout_constraintEnd_toStartOf="@id/calls_list_label" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/calls_list_label"
style="@style/in_call_extra_action_label_style"
android:id="@+id/calls_list_label"
android:onClick="@{callsListClickListener}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/voip_action_go_to_calls_list"
@ -157,8 +172,8 @@
app:layout_constraintEnd_toStartOf="@id/dialer_label" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/dialer_label"
style="@style/in_call_extra_action_label_style"
android:id="@+id/dialer_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/voip_action_show_dialer"
@ -167,8 +182,8 @@
app:layout_constraintEnd_toEndOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/chat_label"
style="@style/in_call_extra_action_label_style"
android:id="@+id/chat_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingBottom="15dp"
@ -178,8 +193,8 @@
app:layout_constraintEnd_toEndOf="@id/transfer_label" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/pause_call_label"
style="@style/in_call_extra_action_label_style"
android:id="@+id/pause_call_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingBottom="15dp"
@ -189,8 +204,8 @@
app:layout_constraintEnd_toEndOf="@id/new_call_label" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/record_call_label"
style="@style/in_call_extra_action_label_style"
android:id="@+id/record_call_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingBottom="15dp"

View file

@ -47,7 +47,14 @@
android:id="@+id/activeCallFragment"
android:name="org.linphone.ui.voip.fragment.ActiveCallFragment"
android:label="ActiveCallFragment"
tools:layout="@layout/voip_active_call_fragment"/>
tools:layout="@layout/voip_active_call_fragment">
<action
android:id="@+id/action_activeCallFragment_to_newCallFragment"
app:destination="@id/newCallFragment"
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out"
app:launchSingleTop="true" />
</fragment>
<action android:id="@+id/action_global_activeCallFragment"
app:destination="@id/activeCallFragment"
@ -55,4 +62,10 @@
app:popUpToInclusive="true"
app:launchSingleTop="true"/>
<fragment
android:id="@+id/newCallFragment"
android:name="org.linphone.ui.voip.fragment.NewCallFragment"
android:label="NewCallFragment"
tools:layout="@layout/call_start_fragment" />
</navigation>