From 43cf0f5cba785fe46e93e9a734f22ee3d011c65f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Sat, 14 Oct 2023 15:27:45 +0200 Subject: [PATCH] Finished removing AvatarView (still borders to do and fix display issue when group has 3 participants) --- app/build.gradle | 3 - .../java/org/linphone/LinphoneApplication.kt | 7 - .../ui/main/chat/model/ConversationModel.kt | 26 +- .../chat/viewmodel/ConversationViewModel.kt | 14 +- .../main/contacts/model/ContactAvatarModel.kt | 16 +- .../main/contacts/model/GroupAvatarModel.kt | 81 ------ .../org/linphone/utils/DataBindingUtils.kt | 257 +++++++----------- .../account_profile_secure_mode_fragment.xml | 23 +- .../layout/assistant_secure_mode_fragment.xml | 23 +- .../res/layout/chat_conversation_fragment.xml | 33 +-- app/src/main/res/layout/chat_list_cell.xml | 35 +-- .../start_call_suggestion_list_cell.xml | 11 +- 12 files changed, 146 insertions(+), 383 deletions(-) delete mode 100644 app/src/main/java/org/linphone/ui/main/contacts/model/GroupAvatarModel.kt diff --git a/app/build.gradle b/app/build.gradle index c0c736068..8c888265f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -99,9 +99,6 @@ dependencies { implementation("io.coil-kt:coil-svg:$coil_version") implementation("io.coil-kt:coil-video:$coil_version") - // https://github.com/GetStream/avatarview-android/blob/main/LICENSE Apache v2.0 - implementation "io.getstream:avatarview-coil:1.0.7" - // https://github.com/tommybuonomo/dotsindicator/blob/master/LICENSE Apache v2.0 implementation("com.tbuonomo:dotsindicator:5.0") diff --git a/app/src/main/java/org/linphone/LinphoneApplication.kt b/app/src/main/java/org/linphone/LinphoneApplication.kt index fe049bfe1..9a6d81c8a 100644 --- a/app/src/main/java/org/linphone/LinphoneApplication.kt +++ b/app/src/main/java/org/linphone/LinphoneApplication.kt @@ -30,8 +30,6 @@ import coil.decode.VideoFrameDecoder import coil.disk.DiskCache import coil.memory.MemoryCache import com.google.android.material.color.DynamicColors -import io.getstream.avatarview.coil.AvatarCoil -import io.getstream.avatarview.coil.AvatarImageLoaderFactory import org.linphone.core.CoreContext import org.linphone.core.CorePreferences import org.linphone.core.Factory @@ -76,11 +74,6 @@ class LinphoneApplication : Application(), ImageLoaderFactory { coreContext.start() DynamicColors.applyToActivitiesIfAvailable(this) - AvatarCoil.setImageLoader( - AvatarImageLoaderFactory(context) { - newImageLoader() - } - ) } override fun newImageLoader(): ImageLoader { diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt index 4a9df93f5..3416cb875 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt @@ -34,7 +34,6 @@ import org.linphone.core.EventLog import org.linphone.core.Friend import org.linphone.core.tools.Log import org.linphone.ui.main.contacts.model.ContactAvatarModel -import org.linphone.ui.main.contacts.model.GroupAvatarModel import org.linphone.utils.AppUtils import org.linphone.utils.LinphoneUtils import org.linphone.utils.TimestampUtils @@ -78,8 +77,6 @@ class ConversationModel @WorkerThread constructor(private val chatRoom: ChatRoom val avatarModel = MutableLiveData() - val groupAvatarModel: GroupAvatarModel - private var lastMessage: ChatMessage? = null private val chatRoomListener = object : ChatRoomListenerStub() { @@ -145,15 +142,24 @@ class ConversationModel @WorkerThread constructor(private val chatRoom: ChatRoom } firstParticipant?.address ?: chatRoom.peerAddress } - val friend = coreContext.contactsManager.findContactByAddress(address) - if (friend != null) { - avatarModel.postValue(ContactAvatarModel(friend)) - } else { + + if (isGroup) { val fakeFriend = coreContext.core.createFriend() - fakeFriend.address = address - avatarModel.postValue(ContactAvatarModel(fakeFriend)) + val model = ContactAvatarModel(fakeFriend) + model.addPicturesFromFriends(friends) + avatarModel.postValue(model) + } else { + val friend = coreContext.contactsManager.findContactByAddress(address) + if (friend != null) { + val model = ContactAvatarModel(friend) + avatarModel.postValue(model) + } else { + val fakeFriend = coreContext.core.createFriend() + fakeFriend.address = address + val model = ContactAvatarModel(fakeFriend) + avatarModel.postValue(model) + } } - groupAvatarModel = GroupAvatarModel(friends) isMuted.postValue(chatRoom.muted) isEphemeral.postValue(chatRoom.isEphemeralEnabled) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt index d8f376b2b..688e77c7e 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt @@ -35,7 +35,6 @@ import org.linphone.core.tools.Log import org.linphone.ui.main.chat.model.ChatMessageModel import org.linphone.ui.main.chat.model.EventLogModel import org.linphone.ui.main.contacts.model.ContactAvatarModel -import org.linphone.ui.main.contacts.model.GroupAvatarModel import org.linphone.utils.AppUtils import org.linphone.utils.Event import org.linphone.utils.LinphoneUtils @@ -51,8 +50,6 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { val avatarModel = MutableLiveData() - val groupAvatarModel = MutableLiveData() - val events = MutableLiveData>() val isGroup = MutableLiveData() @@ -244,10 +241,15 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { val firstParticipant = chatRoom.participants.firstOrNull() firstParticipant?.address ?: chatRoom.peerAddress } - val avatar = getAvatarModelForAddress(address) + + val avatar = if (isGroupChatRoom) { + val fakeFriend = coreContext.core.createFriend() + ContactAvatarModel(fakeFriend) + } else { + getAvatarModelForAddress(address) + } + avatar.addPicturesFromFriends(friends) avatarModel.postValue(avatar) - val groupAvatar = GroupAvatarModel(friends) - groupAvatarModel.postValue(groupAvatar) val history = chatRoom.getHistoryEvents(0) val eventsList = getEventsListFromHistory(history, isGroupChatRoom) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt index 0c35d27d4..9ebbb89a7 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt @@ -71,7 +71,7 @@ class ContactAvatarModel @WorkerThread constructor(val friend: Friend) : Abstrac initials.postValue(AppUtils.getInitials(friend.name.orEmpty())) trust.postValue(SecurityLevel.Safe) // TODO FIXME: use API showTrust.postValue(coreContext.core.defaultAccount?.isInSecureMode()) - images.postValue(arrayListOf(getAvatarUri().toString())) + images.postValue(arrayListOf(getAvatarUri(friend).toString())) name.postValue(friend.name) computePresence() @@ -83,7 +83,19 @@ class ContactAvatarModel @WorkerThread constructor(val friend: Friend) : Abstrac } @WorkerThread - private fun getAvatarUri(): Uri? { + fun addPicturesFromFriends(friends: List) { + if (friends.isNotEmpty()) { + val list = arrayListOf() + list.addAll(images.value.orEmpty()) + for (friend in friends) { + list.add(getAvatarUri(friend).toString()) + } + images.postValue(list) + } + } + + @WorkerThread + private fun getAvatarUri(friend: Friend): Uri? { val picturePath = friend.photo if (!picturePath.isNullOrEmpty()) { return Uri.parse(picturePath) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/model/GroupAvatarModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/model/GroupAvatarModel.kt deleted file mode 100644 index 61d95a3db..000000000 --- a/app/src/main/java/org/linphone/ui/main/contacts/model/GroupAvatarModel.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.main.contacts.model - -import android.content.ContentUris -import android.net.Uri -import android.provider.ContactsContract -import androidx.annotation.WorkerThread -import androidx.lifecycle.MutableLiveData -import org.linphone.core.ChatRoom.SecurityLevel -import org.linphone.core.Friend -import org.linphone.core.tools.Log - -class GroupAvatarModel @WorkerThread constructor(friends: ArrayList) { - companion object { - private const val TAG = "[Group Avatar Model]" - } - - val trust = MutableLiveData() - - val uris = MutableLiveData>() - - init { - trust.postValue(SecurityLevel.Safe) // TODO FIXME: use API - - val list = arrayListOf() - Log.d("$TAG [${friends.size}] friends to use") - for (friend in friends) { - val uri = getAvatarUri(friend) - if (uri != null) { - if (!list.contains(uri)) { - list.add(uri) - } - } - } - uris.postValue(list.toList()) - } - - @WorkerThread - private fun getAvatarUri(friend: Friend): Uri? { - val picturePath = friend.photo - if (!picturePath.isNullOrEmpty()) { - return Uri.parse(picturePath) - } - - val refKey = friend.refKey - if (refKey != null) { - try { - val lookupUri = ContentUris.withAppendedId( - ContactsContract.Contacts.CONTENT_URI, - refKey.toLong() - ) - return Uri.withAppendedPath( - lookupUri, - ContactsContract.Contacts.Photo.CONTENT_DIRECTORY - ) - } catch (numberFormatException: NumberFormatException) { - // Expected for contacts created by Linphone - } - } - - return null - } -} diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index 4bbbf3ecb..f106431da 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -21,7 +21,10 @@ package org.linphone.utils import android.annotation.SuppressLint import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas import android.graphics.PorterDuff +import android.graphics.Rect import android.text.Editable import android.text.TextWatcher import android.view.LayoutInflater @@ -37,6 +40,7 @@ import androidx.annotation.UiThread import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.AppCompatEditText import androidx.appcompat.widget.AppCompatTextView +import androidx.core.graphics.drawable.toBitmap import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.doOnLayout @@ -48,10 +52,10 @@ import androidx.emoji2.emojipicker.EmojiViewItem import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import coil.dispose +import coil.imageLoader import coil.load +import coil.request.ImageRequest import coil.transform.CircleCropTransformation -import io.getstream.avatarview.AvatarView -import io.getstream.avatarview.coil.loadImage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch @@ -60,10 +64,8 @@ import org.linphone.BR import org.linphone.R import org.linphone.contacts.AbstractAvatarModel import org.linphone.contacts.AvatarGenerator -import org.linphone.core.ChatRoom import org.linphone.core.ConsolidatedPresence import org.linphone.core.tools.Log -import org.linphone.ui.main.contacts.model.GroupAvatarModel /** * This file contains all the data binding necessary for the app @@ -264,6 +266,20 @@ fun ImageView.loadCallAvatarWithCoil(model: AbstractAvatarModel?) { } } +@UiThread +@BindingAdapter("coilInitials") +fun ImageView.loadInitialsAvatarWithCoil(initials: String?) { + Log.i("[Data Binding Utils] Displaying initials [$initials] on ImageView") + val imageView = this + (context as AppCompatActivity).lifecycleScope.launch { + withContext(Dispatchers.IO) { + val builder = AvatarGenerator(context) + builder.setInitials(initials.orEmpty()) + load(builder.build()) + } + } +} + @SuppressLint("ResourceType") private suspend fun loadContactPictureWithCoil( imageView: ImageView, @@ -276,173 +292,86 @@ private suspend fun loadContactPictureWithCoil( val context = imageView.context if (model != null) { - val image = model.images.value?.firstOrNull() - imageView.load(image) { - transformations(CircleCropTransformation()) - error( - coroutineScope { - withContext(Dispatchers.IO) { - val builder = AvatarGenerator(context) - builder.setInitials(model.initials.value.orEmpty()) - if (size > 0) { - builder.setAvatarSize(AppUtils.getDimension(size).toInt()) + val images = model.images.value.orEmpty() + val count = images.size + if (count == 1) { + val image = images.firstOrNull() + imageView.load(image) { + transformations(CircleCropTransformation()) + error( + coroutineScope { + withContext(Dispatchers.IO) { + val builder = AvatarGenerator(context) + builder.setInitials(model.initials.value.orEmpty()) + if (size > 0) { + builder.setAvatarSize(AppUtils.getDimension(size).toInt()) + } + if (textSize > 0) { + builder.setTextSize(AppUtils.getDimension(textSize)) + } + builder.build() } - if (textSize > 0) { - builder.setTextSize(AppUtils.getDimension(textSize)) - } - /*if (color > 0) { - builder.setBackgroundColorAttribute(color) - } - if (textColor > 0) { - builder.setTextColorResource(textColor) - }*/ - builder.build() } - } - ) + ) + } + } else if (count > 1) { + val w = if (size > 0) { + AppUtils.getDimension(size).toInt() + } else { + AppUtils.getDimension(R.dimen.avatar_list_cell_size).toInt() + } + val bitmap = Bitmap.createBitmap(w, w, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + + val drawables = images.mapNotNull { + val request = ImageRequest.Builder(imageView.context) + .data(it) + .size(w / 2) + .allowHardware(false) + .build() + context.imageLoader.execute(request).drawable + } + + val rectangles = if (drawables.size == 2) { + arrayListOf( + Rect(0, 0, w / 2, w), + Rect(w / 2, 0, w, w) + ) + } else if (drawables.size == 3) { + // TODO FIXME + arrayListOf( + Rect(0, 0, w / 2, w / 2), + Rect(w / 2, 0, w, w / 2), + Rect(0, w / 2, w, w) + ) + } else if (drawables.size >= 4) { + arrayListOf( + Rect(0, 0, w / 2, w / 2), + Rect(w / 2, 0, w, w / 2), + Rect(0, w / 2, w / 2, w), + Rect(w / 2, w / 2, w, w) + ) + } else { + arrayListOf() + } + + for (i in 0 until rectangles.size) { + canvas.drawBitmap( + drawables[i].toBitmap(w, w, Bitmap.Config.ARGB_8888), + null, + rectangles[i], + null + ) + } + + imageView.load(bitmap) { + transformations(CircleCropTransformation()) + } } } } } -/*@UiThread -@BindingAdapter("avatarInitials") -fun AvatarView.loadInitials(initials: String?) { - Log.i("[Data Binding Utils] Displaying initials [$initials] on AvatarView") - if (initials.orEmpty() != "+") { - avatarInitials = initials.orEmpty() - } -} - -@UiThread -@BindingAdapter("accountAvatar") -fun AvatarView.loadAccountAvatar(account: AccountModel?) { - Log.i("[Data Binding Utils] Loading account picture [${account?.avatar?.value}] with coil") - if (account == null) { - loadImage(R.drawable.user_circle) - } else { - val lifecycleOwner = findViewTreeLifecycleOwner() - if (lifecycleOwner != null) { - account.avatar.observe(lifecycleOwner) { uri -> - loadImage( - data = uri, - onStart = { - if (account.showTrust.value == true) { - avatarBorderColor = - resources.getColor(R.color.blue_info_500, context.theme) - avatarBorderWidth = - AppUtils.getDimension(R.dimen.avatar_trust_border_width).toInt() - } else { - avatarBorderWidth = AppUtils.getDimension(R.dimen.zero).toInt() - } - }, - onError = { _, _ -> - // Use initials as fallback - val initials = account.initials.value.orEmpty() - if (initials != "+") { - avatarInitials = initials - } - } - ) - } - } else { - loadImage( - data = account.avatar.value, - onStart = { - if (account.showTrust.value == true) { - avatarBorderColor = resources.getColor(R.color.blue_info_500, context.theme) - avatarBorderWidth = AppUtils.getDimension(R.dimen.avatar_trust_border_width).toInt() - } else { - avatarBorderWidth = AppUtils.getDimension(R.dimen.zero).toInt() - } - }, - onError = { _, _ -> - // Use initials as fallback - val initials = account.initials.value.orEmpty() - if (initials != "+") { - avatarInitials = initials - } - } - ) - } - } -} - -@UiThread -@BindingAdapter("contactAvatar") -fun AvatarView.loadContactAvatar(contact: ContactAvatarModel?) { - if (contact == null) { - loadImage(R.drawable.user_circle) - } else { - val uri = contact.avatar.value - loadImage( - data = uri, - onStart = { - when (contact.trust.value) { - ChatRoom.SecurityLevel.Unsafe -> { - avatarBorderColor = - resources.getColor(R.color.red_danger_500, context.theme) - avatarBorderWidth = - AppUtils.getDimension(R.dimen.avatar_trust_border_width).toInt() - } - ChatRoom.SecurityLevel.Safe -> { - avatarBorderColor = - resources.getColor(R.color.blue_info_500, context.theme) - avatarBorderWidth = - AppUtils.getDimension(R.dimen.avatar_trust_border_width).toInt() - } - else -> { - avatarBorderWidth = AppUtils.getDimension(R.dimen.zero).toInt() - } - } - }, - onError = { _, result -> - Log.e("[Contact Avatar Model] Can't load data: ${result.throwable}") - // Use initials as fallback - val initials = contact.initials - if (initials != "+") { - avatarInitials = initials - } - } - ) - } -}*/ - -@UiThread -@BindingAdapter("groupAvatar") -fun AvatarView.loadGroupAvatar(contact: GroupAvatarModel?) { - if (contact == null) { - loadImage(R.drawable.user_circle) - } else { - val uris = contact.uris.value.orEmpty() - loadImage( - data = uris, - onStart = { - when (contact.trust.value) { - ChatRoom.SecurityLevel.Unsafe -> { - avatarBorderColor = - resources.getColor(R.color.red_danger_500, context.theme) - avatarBorderWidth = - AppUtils.getDimension(R.dimen.avatar_trust_border_width).toInt() - } - ChatRoom.SecurityLevel.Encrypted -> { - avatarBorderColor = - resources.getColor(R.color.blue_info_500, context.theme) - avatarBorderWidth = - AppUtils.getDimension(R.dimen.avatar_trust_border_width).toInt() - } - else -> { - avatarBorderWidth = AppUtils.getDimension(R.dimen.zero).toInt() - } - } - }, - onError = { _, result -> - Log.e("[Group Avatar Model] Can't load data: ${result.throwable}") - } - ) - } -} - @UiThread @BindingAdapter("onValueChanged") fun AppCompatEditText.editTextSetting(lambda: () -> Unit) { diff --git a/app/src/main/res/layout/account_profile_secure_mode_fragment.xml b/app/src/main/res/layout/account_profile_secure_mode_fragment.xml index a8d312447..f5bd021aa 100644 --- a/app/src/main/res/layout/account_profile_secure_mode_fragment.xml +++ b/app/src/main/res/layout/account_profile_secure_mode_fragment.xml @@ -111,7 +111,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> - - - - - - - - @@ -51,7 +50,7 @@ android:background="@drawable/led_background" android:padding="@dimen/avatar_presence_badge_padding" app:presenceIcon="@{model.avatarModel.presenceStatus}" - android:visibility="@{model.isGroup || model.avatarModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE}" + android:visibility="@{model.avatarModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE}" app:layout_constraintEnd_toEndOf="@id/avatar" app:layout_constraintBottom_toBottomOf="@id/avatar"/> @@ -60,38 +59,10 @@ android:layout_width="@dimen/avatar_presence_badge_size" android:layout_height="@dimen/avatar_presence_badge_size" android:src="@{model.avatarModel.trust == SecurityLevel.Encrypted ? @drawable/trusted : @drawable/not_trusted, default=@drawable/trusted}" - android:visibility="@{!model.isGroup && model.avatarModel.trust == SecurityLevel.Encrypted || model.avatarModel.trust == SecurityLevel.Unsafe ? View.VISIBLE : View.GONE}" + android:visibility="@{model.avatarModel.trust == SecurityLevel.Encrypted || model.avatarModel.trust == SecurityLevel.Unsafe ? View.VISIBLE : View.GONE}" app:layout_constraintStart_toStartOf="@id/avatar" app:layout_constraintBottom_toBottomOf="@id/avatar"/> - - - - diff --git a/app/src/main/res/layout/start_call_suggestion_list_cell.xml b/app/src/main/res/layout/start_call_suggestion_list_cell.xml index 6ac2a2909..571aeb409 100644 --- a/app/src/main/res/layout/start_call_suggestion_list_cell.xml +++ b/app/src/main/res/layout/start_call_suggestion_list_cell.xml @@ -21,7 +21,7 @@ android:layout_marginStart="16dp" android:layout_marginEnd="16dp"> -