Added contacts avatars

This commit is contained in:
Sylvain Berfini 2023-06-23 15:06:40 +02:00
parent 369a6f1977
commit c77fae435f
11 changed files with 228 additions and 94 deletions

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<ConsolidatedPresence>()
val name = MutableLiveData<String>()
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
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.contacts
import androidx.lifecycle.MutableLiveData
import org.linphone.core.*
class ContactSelectionData(searchResult: SearchResult) {
val name = MutableLiveData<String>()
init {
name.value = searchResult.friend?.name ?: searchResult.toString()
}
}

View file

@ -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<SearchResult, RecyclerView.ViewHolder>(SearchResultDiffCallback()) {
) : ListAdapter<ContactData, RecyclerView.ViewHolder>(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<SearchResult>() {
private class ContactDataDiffCallback : DiffUtil.ItemCallback<ContactData>() {
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
}
}

View file

@ -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<String>()
val lastMessage = MutableLiveData<SpannableStringBuilder>()
val lastMessage = MutableLiveData<String>()
val unreadChatCount = MutableLiveData<Int>()
@ -56,6 +57,8 @@ class ChatRoomData(val chatRoom: ChatRoom) {
val lastMessageImdnIcon = MutableLiveData<Int>()
val contactData = MutableLiveData<ContactData>()
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 {

View file

@ -93,6 +93,7 @@ class ConversationsFragment : Fragment() {
it.consume { data ->
}
}
adapter.chatRoomLongClickedEvent.observe(viewLifecycleOwner) {
it.consume { data ->
val modalBottomSheet = ConversationMenuDialogFragment(data.chatRoom) {

View file

@ -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<ChatRoomData>() {
}
override fun areContentsTheSame(oldItem: ChatRoomData, newItem: ChatRoomData): Boolean {
return false
return true
}
}

View file

@ -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<ArrayList<SearchResult>>()
val contactsList = MutableLiveData<ArrayList<ContactData>>()
val filter = MutableLiveData<String>()
private var previousFilter = "NotSet"
@ -80,8 +81,16 @@ class NewConversationViewModel : ViewModel() {
private fun processMagicSearchResults(results: Array<SearchResult>) {
Log.i("[New Conversation ViewModel] [${results.size}] matching results")
val list = arrayListOf<SearchResult>()
list.addAll(results)
contactsList.value.orEmpty().forEach(ContactData::onDestroy)
val list = arrayListOf<ContactData>()
for (searchResult in results) {
val friend = searchResult.friend
if (friend != null) {
val data = ContactData(friend)
list.add(data)
}
}
contactsList.postValue(list)
}
}

View file

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

View file

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

View file

@ -23,39 +23,47 @@
<ImageView
android:id="@+id/avatar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="36dp"
android:src="@{data.isOneToOne ? @drawable/contact_avatar : @drawable/group_avatar, default=@drawable/contact_avatar}"
coilContact="@{data.contactData}"
android:layout_marginStart="10dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:adjustViewBounds="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@{data.isOneToOne ? data.contactName : data.subject, default=`John Doe`}"
android:textColor="#000000"
android:textSize="14sp"
android:textStyle="@{data.unreadChatCount > 0 ? Typeface.BOLD : Typeface.NORMAL, default=normal}"
android:maxLines="1"
android:ellipsize="end"
app:layout_constraintBottom_toTopOf="@id/subtitle"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintEnd_toStartOf="@id/date_time"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/subtitle"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@{data.isComposing ? `... est en train d'écrire` : data.lastMessage, default=`Lorem Ipsum`}"
android:textColor="@{data.unreadChatCount > 0 ? @color/black : @color/gray_4, default=@color/gray_4}"
android:textSize="14sp"
android:textStyle="@{data.unreadChatCount > 0 ? Typeface.BOLD : Typeface.NORMAL, default=normal}"
android:maxLines="1"
android:ellipsize="end"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintEnd_toStartOf="@id/end_subtitle_barrier"
app:layout_constraintTop_toBottomOf="@id/title" />
<TextView
@ -70,6 +78,13 @@
app:layout_constraintTop_toTopOf="@id/title"
app:layout_constraintBottom_toBottomOf="@id/title" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/end_subtitle_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="imdn, unread"
app:barrierDirection="left" />
<ImageView
android:id="@+id/imdn"
android:layout_width="wrap_content"

View file

@ -7,7 +7,7 @@
<import type="org.linphone.core.ConsolidatedPresence"/>
<variable
name="data"
type="org.linphone.contacts.ContactSelectionData" />
type="org.linphone.contacts.ContactData" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
@ -17,11 +17,13 @@
<ImageView
android:id="@+id/avatar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="36dp"
android:src="@drawable/contact_avatar"
coilContact="@{data}"
android:layout_marginStart="10dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:adjustViewBounds="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@ -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" />