talk-android/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt
Marcel Hibbe e0d3cb58ca fix to handle whitespaces for guest avatars
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2024-11-28 16:28:25 +00:00

418 lines
14 KiB
Kotlin

/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2022 Tim Krüger <t@timkrueger.me>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@file:Suppress("TooManyFunctions")
package com.nextcloud.talk.extensions
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.util.Log
import android.widget.ImageView
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import coil.annotation.ExperimentalCoilApi
import coil.imageLoader
import coil.load
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil.request.SuccessResult
import coil.result
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation
import com.nextcloud.talk.R
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.TextDrawable
import java.util.Locale
private const val ROUNDING_PIXEL = 16f
private const val TAG = "ImageViewExtensions"
@Deprecated("use other constructor that expects com.nextcloud.talk.models.domain.ConversationModel")
fun ImageView.loadConversationAvatar(
user: User,
conversation: Conversation,
ignoreCache: Boolean,
viewThemeUtils: ViewThemeUtils?
): io.reactivex.disposables.Disposable {
return loadConversationAvatar(
user,
ConversationModel.mapToConversationModel(conversation, user),
ignoreCache,
viewThemeUtils
)
}
@Suppress("ReturnCount")
fun ImageView.loadConversationAvatar(
user: User,
conversation: ConversationModel,
ignoreCache: Boolean,
viewThemeUtils: ViewThemeUtils?
): io.reactivex.disposables.Disposable {
val imageRequestUri = ApiUtils.getUrlForConversationAvatarWithVersion(
1,
user.baseUrl,
conversation.token,
DisplayUtils.isDarkModeOn(this.context),
conversation.avatarVersion
)
if (conversation.avatarVersion.isNullOrEmpty() && viewThemeUtils != null) {
when (conversation.type) {
ConversationEnums.ConversationType.ROOM_GROUP_CALL ->
return loadDefaultGroupCallAvatar(viewThemeUtils)
ConversationEnums.ConversationType.ROOM_PUBLIC_CALL ->
return loadDefaultPublicCallAvatar(viewThemeUtils)
else -> {}
}
}
// these placeholders are only used when the request fails completely. The server also return default avatars
// when no own images are set. (although these default avatars can not be themed for the android app..)
val errorPlaceholder =
when (conversation.type) {
ConversationEnums.ConversationType.ROOM_GROUP_CALL ->
ContextCompat.getDrawable(context, R.drawable.ic_circular_group)
ConversationEnums.ConversationType.ROOM_PUBLIC_CALL ->
ContextCompat.getDrawable(context, R.drawable.ic_circular_link)
else -> ContextCompat.getDrawable(context, R.drawable.account_circle_96dp)
}
return loadAvatarInternal(user, imageRequestUri, ignoreCache, errorPlaceholder)
}
fun ImageView.loadUserAvatar(
user: User,
avatarId: String,
requestBigSize: Boolean = true,
ignoreCache: Boolean
): io.reactivex.disposables.Disposable {
val imageRequestUri = ApiUtils.getUrlForAvatar(
user.baseUrl!!,
avatarId,
requestBigSize
)
return loadAvatarInternal(user, imageRequestUri, ignoreCache, null)
}
fun ImageView.loadFederatedUserAvatar(message: ChatMessage): io.reactivex.disposables.Disposable {
val cloudId = message.actorId!!
val darkTheme = if (DisplayUtils.isDarkModeOn(context)) 1 else 0
val ignoreCache = false
val requestBigSize = true
return loadFederatedUserAvatar(
message.activeUser!!,
message.activeUser!!.baseUrl!!,
message.token!!,
cloudId,
darkTheme,
requestBigSize,
ignoreCache
)
}
@Suppress("LongParameterList")
fun ImageView.loadFederatedUserAvatar(
user: User,
baseUrl: String,
token: String,
cloudId: String,
darkTheme: Int,
requestBigSize: Boolean = true,
ignoreCache: Boolean
): io.reactivex.disposables.Disposable {
val imageRequestUri = ApiUtils.getUrlForFederatedAvatar(
baseUrl,
token,
cloudId,
darkTheme,
requestBigSize
)
Log.d(TAG, "federated avatar URL: $imageRequestUri")
return loadAvatarInternal(user, imageRequestUri, ignoreCache, null)
}
@OptIn(ExperimentalCoilApi::class)
private fun ImageView.loadAvatarInternal(
user: User?,
url: String,
ignoreCache: Boolean,
errorPlaceholder: Drawable?
): io.reactivex.disposables.Disposable {
val cachePolicy = if (ignoreCache) {
CachePolicy.WRITE_ONLY
} else {
CachePolicy.ENABLED
}
if (ignoreCache && this.result is SuccessResult) {
val result = this.result as SuccessResult
val memoryCacheKey = result.memoryCacheKey
val memoryCache = context.imageLoader.memoryCache
memoryCacheKey?.let { memoryCache?.remove(it) }
val diskCacheKey = result.diskCacheKey
val diskCache = context.imageLoader.diskCache
diskCacheKey?.let { diskCache?.remove(it) }
}
return DisposableWrapper(
load(url) {
user?.let {
addHeader(
"Authorization",
ApiUtils.getCredentials(user.username, user.token)!!
)
}
transformations(CircleCropTransformation())
error(errorPlaceholder ?: ContextCompat.getDrawable(context, R.drawable.account_circle_96dp))
fallback(errorPlaceholder ?: ContextCompat.getDrawable(context, R.drawable.account_circle_96dp))
listener(onError = { _, result ->
Log.w(TAG, "Can't load avatar with URL: $url", result.throwable)
})
memoryCachePolicy(cachePolicy)
diskCachePolicy(cachePolicy)
}
)
}
@Deprecated("Use function loadAvatar", level = DeprecationLevel.WARNING)
fun ImageView.loadAvatarWithUrl(user: User? = null, url: String): io.reactivex.disposables.Disposable {
return loadAvatarInternal(user, url, false, null)
}
fun ImageView.loadThumbnail(url: String, user: User): io.reactivex.disposables.Disposable {
val requestBuilder = ImageRequest.Builder(context)
.data(url)
.crossfade(true)
.target(this)
.transformations(CircleCropTransformation())
val layers = arrayOfNulls<Drawable>(2)
layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background)
layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground)
requestBuilder.placeholder(LayerDrawable(layers))
if (url.startsWith(user.baseUrl!!) &&
(url.contains("index.php/core/preview") || url.contains("/avatar/"))
) {
requestBuilder.addHeader(
"Authorization",
ApiUtils.getCredentials(user.username, user.token)!!
)
}
return DisposableWrapper(context.imageLoader.enqueue(requestBuilder.build()))
}
fun ImageView.loadImage(url: String, user: User, placeholder: Drawable? = null): io.reactivex.disposables.Disposable {
var finalPlaceholder = placeholder
if (finalPlaceholder == null) {
finalPlaceholder = ContextCompat.getDrawable(context!!, R.drawable.ic_mimetype_file)
}
val requestBuilder = ImageRequest.Builder(context)
.data(url)
.crossfade(true)
.target(this)
.placeholder(finalPlaceholder)
.error(finalPlaceholder)
.transformations(RoundedCornersTransformation(ROUNDING_PIXEL, ROUNDING_PIXEL, ROUNDING_PIXEL, ROUNDING_PIXEL))
if (url.startsWith(user.baseUrl!!) &&
(url.contains("index.php/core/preview") || url.contains("/avatar/"))
) {
requestBuilder.addHeader(
"Authorization",
ApiUtils.getCredentials(user.username, user.token)!!
)
}
return DisposableWrapper(context.imageLoader.enqueue(requestBuilder.build()))
}
fun ImageView.loadAvatarOrImagePreview(
url: String,
user: User,
placeholder: Drawable? = null
): io.reactivex.disposables.Disposable {
return if (url.contains("/avatar/")) {
loadAvatarInternal(user, url, false, null)
} else {
loadImage(url, user, placeholder)
}
}
fun ImageView.loadUserAvatar(any: Any?): io.reactivex.disposables.Disposable {
return DisposableWrapper(
load(any) {
transformations(CircleCropTransformation())
}
)
}
fun ImageView.loadSystemAvatar(): io.reactivex.disposables.Disposable {
val layers = arrayOfNulls<Drawable>(2)
layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background)
layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground)
val layerDrawable = LayerDrawable(layers)
val data: Any = layerDrawable
return DisposableWrapper(
load(data) {
transformations(CircleCropTransformation())
}
)
}
fun ImageView.loadNoteToSelfAvatar(): io.reactivex.disposables.Disposable {
val layers = arrayOfNulls<Drawable>(2)
layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background)
layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_note_to_self)
val layerDrawable = LayerDrawable(layers)
val data: Any = layerDrawable
return DisposableWrapper(
load(data) {
transformations(CircleCropTransformation())
}
)
}
fun ImageView.loadFirstLetterAvatar(name: String): io.reactivex.disposables.Disposable {
val layers = arrayOfNulls<Drawable>(2)
layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background)
layers[1] = createTextDrawable(context, name.trimStart().uppercase(Locale.ROOT))
val layerDrawable = LayerDrawable(layers)
val data: Any = layerDrawable
return DisposableWrapper(
load(data) {
transformations(CircleCropTransformation())
}
)
}
fun ImageView.loadChangelogBotAvatar(): io.reactivex.disposables.Disposable {
return loadSystemAvatar()
}
fun ImageView.loadBotsAvatar(): io.reactivex.disposables.Disposable {
val layers = arrayOfNulls<Drawable>(2)
layers[0] = ColorDrawable(context.getColor(R.color.black))
layers[1] = TextDrawable(context, ">")
val layerDrawable = LayerDrawable(layers)
val data: Any = layerDrawable
return DisposableWrapper(
load(data) {
transformations(CircleCropTransformation())
}
)
}
fun ImageView.loadDefaultGroupCallAvatar(viewThemeUtils: ViewThemeUtils): io.reactivex.disposables.Disposable {
val data: Any = viewThemeUtils.talk.themePlaceholderAvatar(this, R.drawable.ic_avatar_group) as Any
return loadUserAvatar(data)
}
fun ImageView.loadDefaultAvatar(viewThemeUtils: ViewThemeUtils): io.reactivex.disposables.Disposable {
val data: Any = viewThemeUtils.talk.themePlaceholderAvatar(this, R.drawable.account_circle_96dp) as Any
return loadUserAvatar(data)
}
fun ImageView.loadDefaultPublicCallAvatar(viewThemeUtils: ViewThemeUtils): io.reactivex.disposables.Disposable {
val data: Any = viewThemeUtils.talk.themePlaceholderAvatar(this, R.drawable.ic_avatar_link) as Any
return loadUserAvatar(data)
}
fun ImageView.loadMailAvatar(viewThemeUtils: ViewThemeUtils): io.reactivex.disposables.Disposable {
val data: Any = viewThemeUtils.talk.themePlaceholderAvatar(this, R.drawable.ic_avatar_mail) as Any
return loadUserAvatar(data)
}
fun ImageView.loadGuestAvatar(user: User, name: String, big: Boolean): io.reactivex.disposables.Disposable {
return loadGuestAvatar(user.baseUrl!!, name, big)
}
fun ImageView.loadGuestAvatar(baseUrl: String, name: String, big: Boolean): io.reactivex.disposables.Disposable {
val imageRequestUri = ApiUtils.getUrlForGuestAvatar(
baseUrl,
name,
big
)
return DisposableWrapper(
load(imageRequestUri) {
transformations(CircleCropTransformation())
listener(onError = { _, result ->
Log.w(TAG, "Can't load guest avatar with URL: $imageRequestUri", result.throwable)
})
}
)
}
@Suppress("MagicNumber")
private fun createTextDrawable(context: Context, letter: String): Drawable {
val size = 100
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val paint = Paint().apply {
color = ResourcesCompat.getColor(context.resources, R.color.grey_600, null)
style = Paint.Style.FILL
}
canvas.drawRect(0f, 0f, size.toFloat(), size.toFloat(), paint)
val textPaint = Paint().apply {
color = Color.WHITE
textSize = size / 2f
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
val xPos = size / 2f
val yPos = (canvas.height / 2 - (textPaint.descent() + textPaint.ascent()) / 2)
canvas.drawText(letter.take(1), xPos, yPos, textPaint)
return BitmapDrawable(context.resources, bitmap)
}
private class DisposableWrapper(private val disposable: coil.request.Disposable) : io.reactivex.disposables
.Disposable {
override fun dispose() {
disposable.dispose()
}
override fun isDisposed(): Boolean {
return disposable.isDisposed
}
}