diff --git a/Coil/build.gradle.kts b/Coil/build.gradle.kts index b4507a938..c001a3169 100644 --- a/Coil/build.gradle.kts +++ b/Coil/build.gradle.kts @@ -50,6 +50,12 @@ dependencies { api(project(":Auth")) implementation(project(":Avatar")) implementation(project(":Network")) + implementation(project(":Sentry")) + + implementation(core.compose.material3) + implementation(core.androidx.core.ktx) + implementation(core.androidx.core) + implementation(core.coil.svg) api(core.coil) api(core.coil.network.okhttp) diff --git a/Coil/src/main/java/com/infomaniak/core/coil/AvatarTypeExt.kt b/Coil/src/main/java/com/infomaniak/core/coil/AvatarTypeExt.kt new file mode 100644 index 000000000..5d9d2af38 --- /dev/null +++ b/Coil/src/main/java/com/infomaniak/core/coil/AvatarTypeExt.kt @@ -0,0 +1,80 @@ +/* + * Infomaniak Core - Android + * Copyright (C) 2026 Infomaniak Network SA + * + * 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 com.infomaniak.core.coil + +import android.content.Context +import android.graphics.Bitmap +import androidx.compose.ui.graphics.toArgb +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap +import coil3.imageLoader +import coil3.request.CachePolicy +import coil3.request.ErrorResult +import coil3.request.ImageRequest +import coil3.request.ImageResult +import coil3.request.SuccessResult +import coil3.request.allowHardware +import coil3.toBitmap +import com.infomaniak.core.avatar.models.AvatarType +import com.infomaniak.core.avatar.models.AvatarType.DrawableResource +import com.infomaniak.core.avatar.models.AvatarType.WithInitials +import coil3.svg.SvgDecoder +import com.infomaniak.core.sentry.SentryLog + + + +suspend fun AvatarType.toBitmap(appContext: Context): Bitmap? = when (this) { + is WithInitials.Url -> loadAsBitmap(url = url, appContext) ?: generateWithInitials() + is WithInitials.Initials -> generateWithInitials() + is DrawableResource -> ContextCompat.getDrawable(appContext, resource)?.toBitmap() +} + +private fun WithInitials.generateWithInitials(): Bitmap = generateInitialsAvatarBitmap( + initials = initials, + background = getBackgroundColorGradientDrawable(backgroundColorRes = colors.containerColor.toArgb()), + initialsColor = colors.contentColor.toArgb(), +) + +private suspend fun loadAsBitmap(url: String?, appContext: Context): Bitmap? { + if (url.isNullOrBlank()) return null + val size = 256 + val request = buildImageRequest( + url = url, + size = size, + appContext = appContext, + ) + return appContext.imageLoader.execute(request).toBitmap(size) +} + +private fun ImageResult.toBitmap(size: Int): Bitmap? = when (this) { + is SuccessResult -> image.toBitmap(size, size) + is ErrorResult -> { + SentryLog.e("AvatarTypeExt", "Failed to load avatar in Bitmap", throwable) + null + } +} + +private fun buildImageRequest(url: String, size: Int, appContext: Context): ImageRequest { + return ImageRequest.Builder(appContext) + .data(url) + .size(size) + .networkCachePolicy(policy = CachePolicy.ENABLED) + .allowHardware(false) + .decoderFactory(SvgDecoder.Factory()) + .build() +} diff --git a/Coil/src/main/java/com/infomaniak/core/coil/CoilXmllExt.kt b/Coil/src/main/java/com/infomaniak/core/coil/CoilXmllExt.kt index d1d75b587..03756aba8 100644 --- a/Coil/src/main/java/com/infomaniak/core/coil/CoilXmllExt.kt +++ b/Coil/src/main/java/com/infomaniak/core/coil/CoilXmllExt.kt @@ -22,12 +22,13 @@ 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.Drawable import android.graphics.drawable.GradientDrawable import android.widget.ImageView import androidx.annotation.ArrayRes import androidx.annotation.ColorInt +import androidx.core.graphics.createBitmap +import androidx.core.graphics.drawable.toDrawable import coil3.ImageLoader import coil3.load import coil3.request.error @@ -89,19 +90,51 @@ fun Context.generateInitialsAvatarDrawable( background: Drawable, @ColorInt initialsColor: Int = Color.WHITE, ): Drawable { - val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) + val bitmap = generateInitialsAvatarBitmap( + size = size, + initials = initials, + background = background, + initialsColor = initialsColor, + ) + return bitmap.toDrawable(resources) +} + +fun generateInitialsAvatarBitmap( + size: Int = 350, + initials: String, + background: Drawable, + @ColorInt initialsColor: Int = Color.WHITE, +): Bitmap { + val bitmap = createBitmap(size, size) val canvas = Canvas(bitmap) - background.setBounds(canvas.clipBounds.left, canvas.clipBounds.top, canvas.clipBounds.right, canvas.clipBounds.bottom) - background.draw(canvas) + + canvas.setBackground(background) + canvas.drawInitials( + size = size, + initials = initials, + initialsColor = initialsColor, + ) + + return bitmap +} + +private fun Canvas.drawInitials( + size: Int = 350, + initials: String, + @ColorInt initialsColor: Int = Color.WHITE, +) { Paint().apply { isAntiAlias = true textAlign = Paint.Align.CENTER color = initialsColor textSize = (size / 2).toFloat() - - val xPos = canvas.width / 2 - val yPos = (canvas.height / 2 - (descent() + ascent()) / 2) - canvas.drawText(initials, xPos.toFloat(), yPos, this) + val xPos = width / 2f + val yPos = (height / 2f - (descent() + ascent()) / 2f) + drawText(initials, xPos, yPos, this) } - return BitmapDrawable(this.resources, bitmap) +} + +private fun Canvas.setBackground(background: Drawable){ + background.bounds = clipBounds + background.draw(this) }