Started new call fragment

This commit is contained in:
Sylvain Berfini 2023-08-18 16:43:24 +02:00
parent 407e474896
commit 9ad121f7d7
17 changed files with 493 additions and 42 deletions

View file

@ -23,13 +23,58 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.doOnPreDraw
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.Address
import org.linphone.databinding.CallStartFragmentBinding
import org.linphone.ui.main.calls.viewmodel.StartCallViewModel
import org.linphone.ui.main.calls.viewmodel.SuggestionsListViewModel
import org.linphone.ui.main.contacts.adapter.ContactsListAdapter
import org.linphone.ui.main.contacts.model.ContactAvatarModel
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.contacts.viewmodel.ContactsListViewModel
import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.utils.DialogUtils
class StartCallFragment : GenericFragment() {
private lateinit var binding: CallStartFragmentBinding
private val viewModel: StartCallViewModel by navGraphViewModels(
R.id.startCallFragment
)
private val contactsListViewModel: ContactsListViewModel by navGraphViewModels(
R.id.startCallFragment
)
private val suggestionsListViewModel: SuggestionsListViewModel by navGraphViewModels(
R.id.startCallFragment
)
private lateinit var contactsAdapter: ContactsListAdapter
private lateinit var suggestionsAdapter: ContactsListAdapter
private val listener = object : ContactNumberOrAddressClickListener {
override fun onClicked(address: Address?) {
// UI thread
if (address != null) {
coreContext.postOnCoreThread {
coreContext.startCall(address)
}
}
}
override fun onLongPress(model: ContactNumberOrAddressModel) {
// UI thread
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -46,10 +91,111 @@ class StartCallFragment : GenericFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
postponeEnterTransition()
binding.setCancelClickListener {
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
binding.setBackClickListener {
goBack()
}
contactsAdapter = ContactsListAdapter(viewLifecycleOwner, false)
binding.contactsList.setHasFixedSize(true)
binding.contactsList.adapter = contactsAdapter
contactsAdapter.contactClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
startCall(model)
}
}
binding.contactsList.layoutManager = LinearLayoutManager(requireContext())
suggestionsAdapter = ContactsListAdapter(viewLifecycleOwner, false)
binding.suggestionsList.setHasFixedSize(true)
binding.suggestionsList.adapter = suggestionsAdapter
suggestionsAdapter.contactClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
startCall(model)
}
}
binding.suggestionsList.layoutManager = LinearLayoutManager(requireContext())
contactsListViewModel.contactsList.observe(
viewLifecycleOwner
) {
contactsAdapter.submitList(it)
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
}
suggestionsListViewModel.suggestionsList.observe(viewLifecycleOwner) {
suggestionsAdapter.submitList(it)
}
viewModel.searchFilter.observe(viewLifecycleOwner) { filter ->
contactsListViewModel.applyFilter(filter)
suggestionsListViewModel.applyFilter(filter)
}
}
private fun startCall(model: ContactAvatarModel) {
coreContext.postOnCoreThread { core ->
val friend = model.friend
val addressesCount = friend.addresses.size
val numbersCount = friend.phoneNumbers.size
if (addressesCount == 1 && numbersCount == 0) {
val address = friend.addresses.first()
coreContext.startCall(address)
} else if (addressesCount == 1 && numbersCount == 0) {
val number = friend.phoneNumbers.first()
val address = core.interpretUrl(number, true)
if (address != null) {
coreContext.startCall(address)
}
} else {
val list = arrayListOf<ContactNumberOrAddressModel>()
for (address in friend.addresses) {
val addressModel = ContactNumberOrAddressModel(
address,
address.asStringUriOnly(),
listener,
true
)
list.add(addressModel)
}
for (number in friend.phoneNumbersWithLabel) {
val address = core.interpretUrl(number.phoneNumber, true)
val addressModel = ContactNumberOrAddressModel(
address,
number.phoneNumber,
listener,
false,
number.label.orEmpty()
)
list.add(addressModel)
}
coreContext.postOnMainThread {
val model = NumberOrAddressPickerDialogModel(list)
val dialog =
DialogUtils.getNumberOrAddressPickerDialog(requireActivity(), model)
model.dismissEvent.observe(viewLifecycleOwner) { event ->
event.consume {
dialog.dismiss()
}
}
dialog.show()
}
}
}
}
}

View file

@ -0,0 +1,27 @@
/*
* 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.main.calls.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class StartCallViewModel : ViewModel() {
val searchFilter = MutableLiveData<String>()
}

View file

@ -0,0 +1,179 @@
/*
* 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.main.calls.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import java.util.ArrayList
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.contacts.ContactsListener
import org.linphone.core.Friend
import org.linphone.core.MagicSearch
import org.linphone.core.MagicSearchListenerStub
import org.linphone.core.SearchResult
import org.linphone.core.tools.Log
import org.linphone.ui.main.contacts.model.ContactAvatarModel
class SuggestionsListViewModel : ViewModel() {
companion object {
const val TAG = "[Suggestions List ViewModel]"
}
val suggestionsList = MutableLiveData<ArrayList<ContactAvatarModel>>()
private var currentFilter = ""
private var previousFilter = "NotSet"
private var limitSearchToLinphoneAccounts = true
private lateinit var magicSearch: MagicSearch
private val magicSearchListener = object : MagicSearchListenerStub() {
override fun onSearchResultsReceived(magicSearch: MagicSearch) {
// Core thread
Log.i("$TAG Magic search contacts available")
processMagicSearchResults(magicSearch.lastSearch)
}
}
private val contactsListener = object : ContactsListener {
override fun onContactsLoaded() {
// Core thread
Log.i("$TAG Contacts have been (re)loaded, updating list")
applyFilter(
currentFilter,
if (limitSearchToLinphoneAccounts) corePreferences.defaultDomain else "",
MagicSearch.Source.CallLogs.toInt() or MagicSearch.Source.ChatRooms.toInt(),
MagicSearch.Aggregation.Friend
)
}
}
init {
coreContext.postOnCoreThread { core ->
coreContext.contactsManager.addListener(contactsListener)
magicSearch = core.createMagicSearch()
magicSearch.limitedSearch = false
magicSearch.addListener(magicSearchListener)
}
applyFilter(currentFilter)
}
override fun onCleared() {
coreContext.postOnCoreThread {
magicSearch.removeListener(magicSearchListener)
coreContext.contactsManager.removeListener(contactsListener)
}
super.onCleared()
}
fun processMagicSearchResults(results: Array<SearchResult>) {
// Core thread
Log.i("$TAG Processing ${results.size} results")
suggestionsList.value.orEmpty().forEach(ContactAvatarModel::destroy)
val list = arrayListOf<ContactAvatarModel>()
for (result in results) {
val friend = result.friend
val model = if (friend != null) {
ContactAvatarModel(friend)
} else {
Log.w("$TAG SearchResult [$result] has no Friend!")
val fakeFriend =
createFriendFromSearchResult(result)
ContactAvatarModel(fakeFriend)
}
model.noAlphabet.postValue(true)
list.add(model)
}
suggestionsList.postValue(list)
Log.i("$TAG Processed ${results.size} results")
}
fun applyFilter(filter: String) {
// UI thread
coreContext.postOnCoreThread {
applyFilter(
filter,
if (limitSearchToLinphoneAccounts) corePreferences.defaultDomain else "",
MagicSearch.Source.CallLogs.toInt() or MagicSearch.Source.ChatRooms.toInt(),
MagicSearch.Aggregation.Friend
)
}
}
private fun applyFilter(
filter: String,
domain: String,
sources: Int,
aggregation: MagicSearch.Aggregation
) {
// Core thread
if (previousFilter.isNotEmpty() && (
previousFilter.length > filter.length ||
(previousFilter.length == filter.length && previousFilter != filter)
)
) {
magicSearch.resetSearchCache()
}
currentFilter = filter
previousFilter = filter
Log.i(
"$TAG Asking Magic search for contacts matching filter [$filter], domain [$domain] and in sources [$sources]"
)
magicSearch.getContactsListAsync(
filter,
domain,
sources,
aggregation
)
}
private fun createFriendFromSearchResult(searchResult: SearchResult): Friend {
// Core thread
val searchResultFriend = searchResult.friend
if (searchResultFriend != null) return searchResultFriend
val friend = coreContext.core.createFriend()
val address = searchResult.address
if (address != null) {
friend.address = address
}
val number = searchResult.phoneNumber
if (number != null) {
friend.addPhoneNumber(number)
if (address != null && address.username == number) {
friend.removeAddress(address)
}
}
return friend
}
}

View file

@ -122,7 +122,7 @@ private class ContactDiffCallback : DiffUtil.ItemCallback<ContactAvatarModel>()
}
override fun areContentsTheSame(oldItem: ContactAvatarModel, newItem: ContactAvatarModel): Boolean {
return oldItem.showFirstLetter.value == newItem.showFirstLetter.value &&
return oldItem.firstContactStartingByThatLetter.value == newItem.firstContactStartingByThatLetter.value &&
oldItem.presenceStatus.value == newItem.presenceStatus.value
}
}

View file

@ -131,7 +131,9 @@ class ContactFragment : GenericFragment() {
viewModel.showNumberOrAddressPickerDialogEvent.observe(viewLifecycleOwner) {
it.consume {
val model = NumberOrAddressPickerDialogModel(viewModel)
val model = NumberOrAddressPickerDialogModel(
viewModel.sipAddressesAndPhoneNumbers.value.orEmpty()
)
val dialog = DialogUtils.getNumberOrAddressPickerDialog(requireActivity(), model)
model.dismissEvent.observe(viewLifecycleOwner) { event ->

View file

@ -46,7 +46,9 @@ class ContactAvatarModel(val friend: Friend) {
val firstLetter: String = LinphoneUtils.getFirstLetter(friend.name.orEmpty())
val showFirstLetter = MutableLiveData<Boolean>()
val firstContactStartingByThatLetter = MutableLiveData<Boolean>()
val noAlphabet = MutableLiveData<Boolean>()
private val friendListener = object : FriendListenerStub() {
override fun onPresenceReceived(fr: Friend) {

View file

@ -20,16 +20,15 @@
package org.linphone.ui.main.contacts.model
import androidx.lifecycle.MutableLiveData
import org.linphone.ui.main.contacts.viewmodel.ContactViewModel
import org.linphone.utils.Event
class NumberOrAddressPickerDialogModel(viewModel: ContactViewModel) {
val sipAddressesAndPhoneNumbers = MutableLiveData<ArrayList<ContactNumberOrAddressModel>>()
class NumberOrAddressPickerDialogModel(list: List<ContactNumberOrAddressModel>) {
val sipAddressesAndPhoneNumbers = MutableLiveData<List<ContactNumberOrAddressModel>>()
val dismissEvent = MutableLiveData<Event<Boolean>>()
init {
sipAddressesAndPhoneNumbers.value = viewModel.sipAddressesAndPhoneNumbers.value
sipAddressesAndPhoneNumbers.value = list
}
fun dismiss() {

View file

@ -66,7 +66,7 @@ class ContactsListViewModel : ViewModel() {
applyFilter(
currentFilter,
if (limitSearchToLinphoneAccounts) corePreferences.defaultDomain else "",
MagicSearch.Source.Friends.toInt(),
MagicSearch.Source.Friends.toInt() or MagicSearch.Source.LdapServers.toInt(),
MagicSearch.Aggregation.Friend
)
}
@ -81,6 +81,7 @@ class ContactsListViewModel : ViewModel() {
magicSearch.limitedSearch = false
magicSearch.addListener(magicSearchListener)
}
applyFilter(currentFilter)
}
@ -125,7 +126,7 @@ class ContactsListViewModel : ViewModel() {
if (currentLetter != previousLetter) {
previousLetter = currentLetter
}
model.showFirstLetter.postValue(displayLetter)
model.firstContactStartingByThatLetter.postValue(displayLetter)
list.add(model)
if (friend?.starred == true) {

View file

@ -291,7 +291,11 @@ class CurrentCallViewModel() : ViewModel() {
displayedName.postValue(friend.name)
contact.postValue(ContactAvatarModel(friend))
} else {
displayedName.postValue(LinphoneUtils.getDisplayName(address))
val fakeFriend = coreContext.core.createFriend()
fakeFriend.name = LinphoneUtils.getDisplayName(address)
fakeFriend.addAddress(address)
contact.postValue(ContactAvatarModel(fakeFriend))
displayedName.postValue(fakeFriend.name)
}
updateEncryption()

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="5dp" />
<corners android:radius="28dp" />
<solid android:color="@color/gray_6"/>
</shape>

View file

@ -99,8 +99,8 @@
android:id="@+id/calls_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/top_bar"

View file

@ -108,8 +108,8 @@
android:visibility="@{viewModel.showFavourites &amp;&amp; !viewModel.isListFiltered &amp;&amp; viewModel.favourites.size() > 0 ? View.VISIBLE : View.GONE}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_marginTop="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
@ -135,8 +135,6 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/all_contacts_label"

View file

@ -26,13 +26,14 @@
android:onLongClickListener="@{onLongClickListener}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:background="@drawable/cell_background">
<io.getstream.avatarview.AvatarView
android:id="@+id/avatar"
android:layout_width="@dimen/avatar_list_cell_size"
android:layout_height="@dimen/avatar_list_cell_size"
android:layout_marginStart="12dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:adjustViewBounds="true"

View file

@ -6,8 +6,11 @@
<data>
<import type="android.view.View" />
<variable
name="cancelClickListener"
name="backClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.calls.viewmodel.StartCallViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
@ -17,7 +20,7 @@
<ImageView
android:id="@+id/back"
android:onClick="@{cancelClickListener}"
android:onClick="@{backClickListener}"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:layout_marginStart="10dp"
@ -44,25 +47,114 @@
app:layout_constraintStart_toEndOf="@id/back"
app:layout_constraintTop_toTopOf="parent"/>
<ScrollView
android:id="@+id/scrollView"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/gray_7"
<androidx.appcompat.widget.AppCompatEditText
style="@style/default_text_style"
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingStart="15dp"
android:paddingEnd="15dp"
android:drawableStart="@drawable/search"
android:drawableEnd="@drawable/dialer"
android:drawablePadding="10dp"
android:drawableTint="@color/gray_9"
android:background="@drawable/shape_search_square_background"
android:hint="Search contact or history call"
android:text="@={viewModel.searchFilter}"
android:textSize="14sp"
android:inputType="textPersonName|textNoSuggestions"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintBottom_toBottomOf="parent">
app:layout_constraintTop_toBottomOf="@id/title" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/group_call_icon"
android:visibility="@{viewModel.searchFilter.length() > 0 ? View.GONE : View.VISIBLE}"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_marginStart="16dp"
android:layout_marginTop="40dp"
android:padding="10dp"
android:src="@drawable/meetings"
android:background="@drawable/shape_orange_round"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/search_bar" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_800"
android:id="@+id/group_call_label"
android:visibility="@{viewModel.searchFilter.length() > 0 ? View.GONE : View.VISIBLE}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="5dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="Create a group call"
android:textSize="16sp"
android:textColor="@color/black"
android:drawableEnd="@drawable/arrow"
app:layout_constraintStart_toEndOf="@id/group_call_icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/group_call_icon"
app:layout_constraintBottom_toBottomOf="@id/group_call_icon"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_800"
android:id="@+id/all_contacts_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="5dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="32dp"
android:text="All contacts"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/group_call_icon"
app:layout_constraintBottom_toTopOf="@id/contacts_list"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/contacts_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/all_contacts_label"
app:layout_constraintBottom_toTopOf="@id/suggestions_label"/>
</ScrollView>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_800"
android:id="@+id/suggestions_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="5dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="32dp"
android:text="Suggestions"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/contacts_list"
app:layout_constraintBottom_toTopOf="@id/suggestions_list"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/suggestions_list"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintHeight_max="220dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/suggestions_label"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -99,8 +99,8 @@
android:id="@+id/calls_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/top_bar"

View file

@ -23,6 +23,8 @@
android:onLongClick="@{onLongClickListener}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="16dp"
android:background="@drawable/cell_background">
<androidx.appcompat.widget.AppCompatTextView
@ -30,11 +32,11 @@
android:id="@+id/header"
android:layout_width="18dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@{model.firstLetter, default=`A`}"
android:visibility="@{model.showFirstLetter ? View.VISIBLE : View.INVISIBLE}"
android:visibility="@{model.noAlphabet ? View.GONE : model.firstContactStartingByThatLetter ? View.VISIBLE : View.INVISIBLE}"
android:textColor="@color/gray_10"
android:textSize="20sp"
android:layout_marginStart="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>

View file

@ -99,8 +99,8 @@
android:visibility="@{viewModel.showFavourites &amp;&amp; !viewModel.isListFiltered &amp;&amp; viewModel.favourites.size() > 0 ? View.VISIBLE : View.GONE}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_marginTop="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -127,8 +127,6 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/all_contacts_label"