From c77fae435f19a7f7f208c8e615c4a534948f902a Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 23 Jun 2023 15:06:40 +0200 Subject: [PATCH] Added contacts avatars --- .../java/org/linphone/contacts/ContactData.kt | 69 ++++++++++++++ .../linphone/contacts/ContactSelectionData.kt | 31 ------ .../contacts/ContactsSelectionAdapter.kt | 24 ++--- .../linphone/ui/conversations/ChatRoomData.kt | 94 +++++++++++-------- .../ui/conversations/ConversationsFragment.kt | 1 + .../conversations/ConversationsListAdapter.kt | 3 +- .../conversations/NewConversationViewModel.kt | 15 ++- .../org/linphone/utils/DataBindingUtils.kt | 14 +++ .../java/org/linphone/utils/LinphoneUtils.kt | 42 +++++++++ .../main/res/layout/chat_room_list_cell.xml | 21 ++++- .../res/layout/contact_selection_cell.xml | 8 +- 11 files changed, 228 insertions(+), 94 deletions(-) create mode 100644 app/src/main/java/org/linphone/contacts/ContactData.kt delete mode 100644 app/src/main/java/org/linphone/contacts/ContactSelectionData.kt diff --git a/app/src/main/java/org/linphone/contacts/ContactData.kt b/app/src/main/java/org/linphone/contacts/ContactData.kt new file mode 100644 index 000000000..99647f6f0 --- /dev/null +++ b/app/src/main/java/org/linphone/contacts/ContactData.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2010-2020 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.contacts + +import android.content.ContentUris +import android.net.Uri +import android.provider.ContactsContract +import androidx.lifecycle.MutableLiveData +import org.linphone.core.* + +class ContactData(val friend: Friend) { + val presenceStatus = MutableLiveData() + + val name = MutableLiveData() + + val avatar = getAvatarUri() + + private val friendListener = object : FriendListenerStub() { + override fun onPresenceReceived(fr: Friend) { + presenceStatus.postValue(fr.consolidatedPresence) + } + } + + init { + name.postValue(friend.name) + presenceStatus.postValue(friend.consolidatedPresence) + + friend.addListener(friendListener) + + presenceStatus.postValue(ConsolidatedPresence.Offline) + } + + fun onDestroy() { + friend.removeListener(friendListener) + } + + private fun getAvatarUri(): Uri? { + val refKey = friend.refKey + if (refKey != null) { + val lookupUri = ContentUris.withAppendedId( + ContactsContract.Contacts.CONTENT_URI, + refKey.toLong() + ) + return Uri.withAppendedPath( + lookupUri, + ContactsContract.Contacts.Photo.CONTENT_DIRECTORY + ) + } + + return null + } +} diff --git a/app/src/main/java/org/linphone/contacts/ContactSelectionData.kt b/app/src/main/java/org/linphone/contacts/ContactSelectionData.kt deleted file mode 100644 index 36cca5069..000000000 --- a/app/src/main/java/org/linphone/contacts/ContactSelectionData.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2010-2020 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.contacts - -import androidx.lifecycle.MutableLiveData -import org.linphone.core.* - -class ContactSelectionData(searchResult: SearchResult) { - val name = MutableLiveData() - - init { - name.value = searchResult.friend?.name ?: searchResult.toString() - } -} diff --git a/app/src/main/java/org/linphone/contacts/ContactsSelectionAdapter.kt b/app/src/main/java/org/linphone/contacts/ContactsSelectionAdapter.kt index ed6055352..a94078e69 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsSelectionAdapter.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsSelectionAdapter.kt @@ -27,12 +27,11 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import org.linphone.R -import org.linphone.core.SearchResult import org.linphone.databinding.ContactSelectionCellBinding class ContactsSelectionAdapter( private val viewLifecycleOwner: LifecycleOwner -) : ListAdapter(SearchResultDiffCallback()) { +) : ListAdapter(ContactDataDiffCallback()) { init { } @@ -53,10 +52,9 @@ class ContactsSelectionAdapter( inner class ViewHolder( private val binding: ContactSelectionCellBinding ) : RecyclerView.ViewHolder(binding.root) { - fun bind(searchResult: SearchResult) { + fun bind(contactData: ContactData) { with(binding) { - val searchResultViewModel = ContactSelectionData(searchResult) - data = searchResultViewModel + data = contactData lifecycleOwner = viewLifecycleOwner @@ -66,20 +64,18 @@ class ContactsSelectionAdapter( } } -private class SearchResultDiffCallback : DiffUtil.ItemCallback() { +private class ContactDataDiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame( - oldItem: SearchResult, - newItem: SearchResult + oldItem: ContactData, + newItem: ContactData ): Boolean { - val oldAddress = oldItem.address - val newAddress = newItem.address - return if (oldAddress != null && newAddress != null) oldAddress.weakEqual(newAddress) else false + return oldItem.friend.refKey == newItem.friend.refKey } override fun areContentsTheSame( - oldItem: SearchResult, - newItem: SearchResult + oldItem: ContactData, + newItem: ContactData ): Boolean { - return newItem.friend != null + return true } } diff --git a/app/src/main/java/org/linphone/ui/conversations/ChatRoomData.kt b/app/src/main/java/org/linphone/ui/conversations/ChatRoomData.kt index e5bcb19dd..39bb8ac48 100644 --- a/app/src/main/java/org/linphone/ui/conversations/ChatRoomData.kt +++ b/app/src/main/java/org/linphone/ui/conversations/ChatRoomData.kt @@ -19,11 +19,12 @@ */ package org.linphone.ui.conversations -import android.text.SpannableStringBuilder import androidx.lifecycle.MutableLiveData +import java.lang.StringBuilder import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R +import org.linphone.contacts.ContactData import org.linphone.core.* import org.linphone.core.tools.Log import org.linphone.utils.LinphoneUtils @@ -36,7 +37,7 @@ class ChatRoomData(val chatRoom: ChatRoom) { val subject = MutableLiveData() - val lastMessage = MutableLiveData() + val lastMessage = MutableLiveData() val unreadChatCount = MutableLiveData() @@ -56,6 +57,8 @@ class ChatRoomData(val chatRoom: ChatRoom) { val lastMessageImdnIcon = MutableLiveData() + val contactData = MutableLiveData() + var chatRoomDataListener: ChatRoomDataListener? = null val isOneToOne: Boolean by lazy { @@ -92,9 +95,46 @@ class ChatRoomData(val chatRoom: ChatRoom) { } init { - coreContext.postOnCoreThread { core -> - chatRoom.addListener(chatRoomListener) + chatRoom.addListener(chatRoomListener) + + if (chatRoom.hasCapability(ChatRoom.Capabilities.Basic.toInt())) { + val remoteAddress = chatRoom.peerAddress + val friend = chatRoom.core.findFriend(remoteAddress) + if (friend != null) { + contactData.postValue(ContactData(friend)) + } + contactName.postValue(friend?.name ?: LinphoneUtils.getDisplayName(remoteAddress)) + } else { + if (chatRoom.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())) { + val first = chatRoom.participants.firstOrNull() + if (first != null) { + val remoteAddress = first.address + val friend = chatRoom.core.findFriend(remoteAddress) + if (friend != null) { + contactData.postValue(ContactData(friend)) + } + contactName.postValue( + friend?.name ?: LinphoneUtils.getDisplayName(remoteAddress) + ) + } else { + Log.e("[Chat Room Data] No participant in the chat room!") + } + } } + subject.postValue( + chatRoom.subject ?: LinphoneUtils.getDisplayName(chatRoom.peerAddress) + ) + + lastMessageImdnIcon.postValue(R.drawable.imdn_sent) + showLastMessageImdnIcon.postValue(false) + computeLastMessage() + + unreadChatCount.postValue(chatRoom.unreadMessagesCount) + isComposing.postValue(chatRoom.isRemoteComposing) + isSecure.postValue(chatRoom.securityLevel == ChatRoom.SecurityLevel.Encrypted) + isSecureVerified.postValue(chatRoom.securityLevel == ChatRoom.SecurityLevel.Safe) + isEphemeral.postValue(chatRoom.isEphemeralEnabled) + isMuted.postValue(areNotificationsMuted()) } fun onCleared() { @@ -112,39 +152,6 @@ class ChatRoomData(val chatRoom: ChatRoom) { return true } - fun update() { - if (chatRoom.hasCapability(ChatRoom.Capabilities.Basic.toInt())) { - val remoteAddress = chatRoom.peerAddress - val friend = chatRoom.core.findFriend(remoteAddress) - contactName.postValue(friend?.name ?: LinphoneUtils.getDisplayName(remoteAddress)) - } else { - if (chatRoom.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())) { - val first = chatRoom.participants.firstOrNull() - if (first != null) { - val remoteAddress = first.address - val friend = chatRoom.core.findFriend(remoteAddress) - contactName.postValue( - friend?.name ?: LinphoneUtils.getDisplayName(remoteAddress) - ) - } else { - Log.e("[Chat Room Data] No participant in the chat room!") - } - } - } - subject.postValue(chatRoom.subject ?: LinphoneUtils.getDisplayName(chatRoom.peerAddress)) - - lastMessageImdnIcon.postValue(R.drawable.imdn_sent) - showLastMessageImdnIcon.postValue(false) - computeLastMessage() - - unreadChatCount.postValue(chatRoom.unreadMessagesCount) - isComposing.postValue(chatRoom.isRemoteComposing) - isSecure.postValue(chatRoom.securityLevel == ChatRoom.SecurityLevel.Encrypted) - isSecureVerified.postValue(chatRoom.securityLevel == ChatRoom.SecurityLevel.Safe) - isEphemeral.postValue(chatRoom.isEphemeralEnabled) - isMuted.postValue(areNotificationsMuted()) - } - private fun computeLastMessageImdnIcon(message: ChatMessage) { val state = message.state showLastMessageImdnIcon.postValue( @@ -174,7 +181,7 @@ class ChatRoomData(val chatRoom: ChatRoom) { val lastUpdateTime = chatRoom.lastUpdateTime lastUpdate.postValue(TimestampUtils.toString(lastUpdateTime, true)) - val builder = SpannableStringBuilder() + val builder = StringBuilder() val message = chatRoom.lastMessageInHistory if (message != null) { @@ -185,6 +192,10 @@ class ChatRoomData(val chatRoom: ChatRoom) { message.addListener(object : ChatMessageListenerStub() { override fun onMsgStateChanged(message: ChatMessage, state: ChatMessage.State) { computeLastMessageImdnIcon(message) + + if (state == ChatMessage.State.Displayed) { + message.removeListener(this) + } } }) } @@ -206,7 +217,12 @@ class ChatRoomData(val chatRoom: ChatRoom) { builder.trim() } - lastMessage.postValue(builder) + val text = builder.toString() + if (text.length > 128) { // This brings a huge performance improvement when scrolling + lastMessage.postValue(text.substring(0, 128)) + } else { + lastMessage.postValue(text) + } } private fun areNotificationsMuted(): Boolean { diff --git a/app/src/main/java/org/linphone/ui/conversations/ConversationsFragment.kt b/app/src/main/java/org/linphone/ui/conversations/ConversationsFragment.kt index 7262342a4..4df1c5c14 100644 --- a/app/src/main/java/org/linphone/ui/conversations/ConversationsFragment.kt +++ b/app/src/main/java/org/linphone/ui/conversations/ConversationsFragment.kt @@ -93,6 +93,7 @@ class ConversationsFragment : Fragment() { it.consume { data -> } } + adapter.chatRoomLongClickedEvent.observe(viewLifecycleOwner) { it.consume { data -> val modalBottomSheet = ConversationMenuDialogFragment(data.chatRoom) { diff --git a/app/src/main/java/org/linphone/ui/conversations/ConversationsListAdapter.kt b/app/src/main/java/org/linphone/ui/conversations/ConversationsListAdapter.kt index 8f4e22a58..1c3dd5d9e 100644 --- a/app/src/main/java/org/linphone/ui/conversations/ConversationsListAdapter.kt +++ b/app/src/main/java/org/linphone/ui/conversations/ConversationsListAdapter.kt @@ -68,7 +68,6 @@ class ConversationsListAdapter( ) : RecyclerView.ViewHolder(binding.root) { fun bind(chatRoomData: ChatRoomData) { with(binding) { - chatRoomData.update() data = chatRoomData lifecycleOwner = viewLifecycleOwner @@ -98,6 +97,6 @@ private class ConversationDiffCallback : DiffUtil.ItemCallback() { } override fun areContentsTheSame(oldItem: ChatRoomData, newItem: ChatRoomData): Boolean { - return false + return true } } diff --git a/app/src/main/java/org/linphone/ui/conversations/NewConversationViewModel.kt b/app/src/main/java/org/linphone/ui/conversations/NewConversationViewModel.kt index 77b3ab612..335d16423 100644 --- a/app/src/main/java/org/linphone/ui/conversations/NewConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/conversations/NewConversationViewModel.kt @@ -22,13 +22,14 @@ package org.linphone.ui.conversations import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.contacts.ContactData import org.linphone.core.MagicSearch import org.linphone.core.MagicSearchListenerStub import org.linphone.core.SearchResult import org.linphone.core.tools.Log class NewConversationViewModel : ViewModel() { - val contactsList = MutableLiveData>() + val contactsList = MutableLiveData>() val filter = MutableLiveData() private var previousFilter = "NotSet" @@ -80,8 +81,16 @@ class NewConversationViewModel : ViewModel() { private fun processMagicSearchResults(results: Array) { Log.i("[New Conversation ViewModel] [${results.size}] matching results") - val list = arrayListOf() - list.addAll(results) + contactsList.value.orEmpty().forEach(ContactData::onDestroy) + + val list = arrayListOf() + for (searchResult in results) { + val friend = searchResult.friend + if (friend != null) { + val data = ContactData(friend) + list.add(data) + } + } contactsList.postValue(list) } } diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index 7b17eefbf..32aa00b79 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -22,6 +22,10 @@ package org.linphone.utils import android.widget.ImageView import android.widget.TextView import androidx.databinding.BindingAdapter +import coil.load +import coil.transform.CircleCropTransformation +import org.linphone.R +import org.linphone.contacts.ContactData /** * This file contains all the data binding necessary for the app @@ -36,3 +40,13 @@ fun ImageView.setSourceImageResource(resource: Int) { fun TextView.setTypeface(typeface: Int) { this.setTypeface(null, typeface) } + +@BindingAdapter("coilContact") +fun loadContactPictureWithCoil(imageView: ImageView, contact: ContactData?) { + contact ?: return + + imageView.load(contact.avatar) { + transformations(CircleCropTransformation()) + error(R.drawable.contact_avatar) + } +} diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index f48796c03..2bf88b787 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -19,9 +19,14 @@ */ package org.linphone.utils +import android.content.ContentUris +import android.net.Uri +import android.provider.ContactsContract +import java.io.IOException import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.core.Address import org.linphone.core.ChatRoom +import org.linphone.core.Friend class LinphoneUtils { companion object { @@ -52,5 +57,42 @@ class LinphoneUtils { // Do not return an empty display name return address.displayName ?: address.username ?: address.asString() } + + fun Friend.getPictureUri(thumbnailPreferred: Boolean = false): Uri? { + val refKey = refKey + if (refKey != null) { + try { + val lookupUri = ContentUris.withAppendedId( + ContactsContract.Contacts.CONTENT_URI, + refKey.toLong() + ) + + if (!thumbnailPreferred) { + val pictureUri = Uri.withAppendedPath( + lookupUri, + ContactsContract.Contacts.Photo.DISPLAY_PHOTO + ) + // Check that the URI points to a real file + val contentResolver = coreContext.context.contentResolver + try { + if (contentResolver.openAssetFileDescriptor(pictureUri, "r") != null) { + return pictureUri + } + } catch (ioe: IOException) { } + } + + // Fallback to thumbnail if high res picture isn't available + return Uri.withAppendedPath( + lookupUri, + ContactsContract.Contacts.Photo.CONTENT_DIRECTORY + ) + } catch (e: Exception) { } + } else if (photo != null) { + try { + return Uri.parse(photo) + } catch (e: Exception) { } + } + return null + } } } diff --git a/app/src/main/res/layout/chat_room_list_cell.xml b/app/src/main/res/layout/chat_room_list_cell.xml index 543d33276..6f4b81b4f 100644 --- a/app/src/main/res/layout/chat_room_list_cell.xml +++ b/app/src/main/res/layout/chat_room_list_cell.xml @@ -23,39 +23,47 @@ + + + type="org.linphone.contacts.ContactData" /> @@ -34,6 +36,8 @@ android:text="@{data.name, default=`John Doe`}" android:textColor="@color/black" android:textSize="14sp" + android:maxLines="1" + android:ellipsize="end" app:layout_constraintBottom_toBottomOf="@id/avatar" app:layout_constraintStart_toEndOf="@id/avatar" app:layout_constraintTop_toTopOf="parent" />