From 97588a6e43fad56c8c896969d174e061b99deaa3 Mon Sep 17 00:00:00 2001 From: Aymeric MARIAUX Date: Wed, 15 Apr 2026 16:17:59 +0200 Subject: [PATCH 1/4] feat: Added avatar generation in bitmap logic --- Coil/build.gradle.kts | 6 ++ .../com/infomaniak/core/coil/AvatarTypeExt.kt | 80 +++++++++++++++++++ .../com/infomaniak/core/coil/CoilXmllExt.kt | 52 +++++++++--- 3 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 Coil/src/main/java/com/infomaniak/core/coil/AvatarTypeExt.kt 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..c0b88b568 100644 --- a/Coil/src/main/java/com/infomaniak/core/coil/CoilXmllExt.kt +++ b/Coil/src/main/java/com/infomaniak/core/coil/CoilXmllExt.kt @@ -17,6 +17,7 @@ */ package com.infomaniak.core.coil +import android.R import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas @@ -36,6 +37,8 @@ import coil3.request.placeholder import com.infomaniak.core.auth.models.user.User import com.infomaniak.core.avatar.getBackgroundColorResBasedOnId import com.infomaniak.core.coil.ImageLoaderProvider.simpleImageLoader +import androidx.core.graphics.drawable.toDrawable +import androidx.core.graphics.createBitmap fun Context.getBackgroundColorBasedOnId(id: Int, @ArrayRes array: Int? = null): GradientDrawable { return getBackgroundColorGradientDrawable(getBackgroundColorResBasedOnId(id, array)) @@ -89,19 +92,50 @@ 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) - Paint().apply { + + background.setBackground(canvas) + + val paint = createInitialsAvatar( + size = size, + initialsColor = initialsColor, + ) + + val xPos = canvas.width / 2f + val yPos = (canvas.height / 2f - (paint.descent() + paint.ascent()) / 2f) + canvas.drawText(initials, xPos, yPos, paint) + + return bitmap +} + +private fun createInitialsAvatar( + size: Int = 350, + @ColorInt initialsColor: Int = Color.WHITE, +): Paint { + return 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) } - return BitmapDrawable(this.resources, bitmap) +} +private fun Drawable.setBackground(canvas: Canvas){ + setBounds(canvas.clipBounds.left, canvas.clipBounds.top, canvas.clipBounds.right, canvas.clipBounds.bottom) + draw(canvas) } From 62e33e3b4a2eea7cf95d4f6f99523dabd441cfa2 Mon Sep 17 00:00:00 2001 From: Aymeric MARIAUX Date: Fri, 17 Apr 2026 09:09:10 +0200 Subject: [PATCH 2/4] refactor: Refactored initials avatar generation --- .../com/infomaniak/core/coil/CoilXmllExt.kt | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) 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 c0b88b568..5a0c01058 100644 --- a/Coil/src/main/java/com/infomaniak/core/coil/CoilXmllExt.kt +++ b/Coil/src/main/java/com/infomaniak/core/coil/CoilXmllExt.kt @@ -17,18 +17,18 @@ */ package com.infomaniak.core.coil -import android.R 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.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 @@ -37,8 +37,6 @@ import coil3.request.placeholder import com.infomaniak.core.auth.models.user.User import com.infomaniak.core.avatar.getBackgroundColorResBasedOnId import com.infomaniak.core.coil.ImageLoaderProvider.simpleImageLoader -import androidx.core.graphics.drawable.toDrawable -import androidx.core.graphics.createBitmap fun Context.getBackgroundColorBasedOnId(id: Int, @ArrayRes array: Int? = null): GradientDrawable { return getBackgroundColorGradientDrawable(getBackgroundColorResBasedOnId(id, array)) @@ -110,32 +108,35 @@ fun generateInitialsAvatarBitmap( val bitmap = createBitmap(size, size) val canvas = Canvas(bitmap) - background.setBackground(canvas) - - val paint = createInitialsAvatar( + canvas.setBackground(background) + createInitialsAvatar( + canvas = canvas, size = size, + initials = initials, initialsColor = initialsColor, ) - val xPos = canvas.width / 2f - val yPos = (canvas.height / 2f - (paint.descent() + paint.ascent()) / 2f) - canvas.drawText(initials, xPos, yPos, paint) - return bitmap } private fun createInitialsAvatar( + canvas: Canvas, size: Int = 350, + initials: String, @ColorInt initialsColor: Int = Color.WHITE, -): Paint { - return Paint().apply { +) { + Paint().apply { isAntiAlias = true textAlign = Paint.Align.CENTER color = initialsColor textSize = (size / 2).toFloat() + val xPos = canvas.width / 2f + val yPos = (canvas.height / 2f - (descent() + ascent()) / 2f) + canvas.drawText(initials, xPos, yPos, this) } } -private fun Drawable.setBackground(canvas: Canvas){ - setBounds(canvas.clipBounds.left, canvas.clipBounds.top, canvas.clipBounds.right, canvas.clipBounds.bottom) - draw(canvas) + +private fun Canvas.setBackground(background: Drawable){ + background.setBounds(clipBounds.left, clipBounds.top, clipBounds.right, clipBounds.bottom) + background.draw(this) } From af37e1b9b3ed36ee3615a7885995672bb3e45f94 Mon Sep 17 00:00:00 2001 From: Aymeric MARIAUX Date: Fri, 17 Apr 2026 10:31:13 +0200 Subject: [PATCH 3/4] refactor: Convert the function who draw initials on the canvas into a canvas extension --- .../java/com/infomaniak/core/coil/CoilXmllExt.kt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) 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 5a0c01058..5659a0442 100644 --- a/Coil/src/main/java/com/infomaniak/core/coil/CoilXmllExt.kt +++ b/Coil/src/main/java/com/infomaniak/core/coil/CoilXmllExt.kt @@ -109,8 +109,7 @@ fun generateInitialsAvatarBitmap( val canvas = Canvas(bitmap) canvas.setBackground(background) - createInitialsAvatar( - canvas = canvas, + canvas.drawInitials( size = size, initials = initials, initialsColor = initialsColor, @@ -119,8 +118,7 @@ fun generateInitialsAvatarBitmap( return bitmap } -private fun createInitialsAvatar( - canvas: Canvas, +private fun Canvas.drawInitials( size: Int = 350, initials: String, @ColorInt initialsColor: Int = Color.WHITE, @@ -130,9 +128,9 @@ private fun createInitialsAvatar( textAlign = Paint.Align.CENTER color = initialsColor textSize = (size / 2).toFloat() - val xPos = canvas.width / 2f - val yPos = (canvas.height / 2f - (descent() + ascent()) / 2f) - canvas.drawText(initials, xPos, yPos, this) + val xPos = width / 2f + val yPos = (height / 2f - (descent() + ascent()) / 2f) + drawText(initials, xPos, yPos, this) } } From d1f46fa5bad2344d07f021b04bf800bfd795926b Mon Sep 17 00:00:00 2001 From: Aymeric MARIAUX Date: Wed, 22 Apr 2026 12:57:40 +0200 Subject: [PATCH 4/4] refactor: Use bounds property instead of setBounds --- Coil/src/main/java/com/infomaniak/core/coil/CoilXmllExt.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5659a0442..03756aba8 100644 --- a/Coil/src/main/java/com/infomaniak/core/coil/CoilXmllExt.kt +++ b/Coil/src/main/java/com/infomaniak/core/coil/CoilXmllExt.kt @@ -135,6 +135,6 @@ private fun Canvas.drawInitials( } private fun Canvas.setBackground(background: Drawable){ - background.setBounds(clipBounds.left, clipBounds.top, clipBounds.right, clipBounds.bottom) + background.bounds = clipBounds background.draw(this) }