From 5c16feecc6d587977486955d15cdcc8df623a14d Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Tue, 23 Jun 2026 23:47:19 +0200 Subject: [PATCH] fix(activity,notifications): tint leading list icons in dark mode Assisted-by: ClaudeCode:claude-opus-4-8 Signed-off-by: Andy Scherzinger --- .../java/com/nextcloud/utils/GlideHelper.kt | 68 +++++++++++++++++++ .../activities/adapter/ActivityListAdapter.kt | 40 +++++------ .../notifications/NotificationsFragment.kt | 4 +- 3 files changed, 90 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/GlideHelper.kt b/app/src/main/java/com/nextcloud/utils/GlideHelper.kt index 36847652f071..7075b90c1736 100644 --- a/app/src/main/java/com/nextcloud/utils/GlideHelper.kt +++ b/app/src/main/java/com/nextcloud/utils/GlideHelper.kt @@ -10,12 +10,17 @@ package com.nextcloud.utils import android.annotation.SuppressLint import android.content.Context import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.PictureDrawable import android.widget.ImageView import androidx.activity.ComponentActivity import androidx.annotation.DrawableRes +import androidx.core.graphics.createBitmap import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory +import androidx.core.graphics.drawable.toDrawable +import androidx.core.graphics.scale import androidx.core.net.toUri import androidx.lifecycle.lifecycleScope import com.bumptech.glide.Glide @@ -27,7 +32,9 @@ import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.LazyHeaders import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.BitmapImageViewTarget +import com.bumptech.glide.request.target.CustomViewTarget import com.bumptech.glide.request.target.Target +import com.bumptech.glide.request.transition.Transition import com.nextcloud.common.NextcloudClient import com.nextcloud.utils.LinkHelper.validateAndGetURL import com.owncloud.android.lib.common.OwnCloudAccount @@ -112,6 +119,67 @@ object GlideHelper { } } + /** + * Loads an image into an [ImageView], rasterizing the result into a tintable drawable so a tint set on the view + * takes effect. SVGs decode into a [PictureDrawable] that ignores color filters and tint lists; rasterizing + * works around that. + * + * @param context context used to build the Glide request and the rasterized drawable. + * @param client authenticated client whose credentials are attached to the request; the load is skipped when null. + * @param url image URL to load; an invalid or null URL leaves the [placeholder] in place. + * @param imageView target view that receives the loaded drawable. + * @param placeholder drawable resource shown while loading and on failure. + * @param sizePx width and height, in pixels, of the rasterized square drawable. + */ + fun loadTintableIconIntoImageView( + context: Context, + client: NextcloudClient?, + url: String?, + imageView: ImageView, + @DrawableRes placeholder: Int, + sizePx: Int + ) { + imageView.setImageResource(placeholder) + try { + createRequestBuilder(context, client, url) + ?.error(placeholder) + ?.withLogging("loadTintableIconIntoImageView", url ?: "null") + ?.into(object : CustomViewTarget(imageView) { + override fun onResourceReady(resource: Drawable, transition: Transition?) { + imageView.setImageDrawable(rasterizeToTintableDrawable(context, resource, sizePx)) + } + + override fun onLoadFailed(errorDrawable: Drawable?) { + errorDrawable?.let { imageView.setImageDrawable(it) } + } + + override fun onResourceCleared(placeholderDrawable: Drawable?) = Unit + }) + } catch (e: Exception) { + Log_OC.e(TAG, "exception loadTintableIconIntoImageView: $e") + imageView.setImageResource(placeholder) + } + } + + private fun rasterizeToTintableDrawable(context: Context, drawable: Drawable, sizePx: Int): Drawable { + if (drawable is BitmapDrawable) { + return drawable.bitmap.scale(sizePx, sizePx).toDrawable(context.resources) + } + + val width = drawable.intrinsicWidth + val height = drawable.intrinsicHeight + val bitmap = createBitmap(sizePx, sizePx) + val canvas = Canvas(bitmap) + if (width > 0 && height > 0) { + canvas.scale(sizePx / width.toFloat(), sizePx / height.toFloat()) + drawable.setBounds(0, 0, width, height) + } else { + drawable.setBounds(0, 0, sizePx, sizePx) + } + drawable.draw(canvas) + return bitmap.toDrawable(context.resources) + } + fun getDrawable(context: Context, client: NextcloudClient?, urlString: String?): Drawable? = try { createRequestBuilder(context, client, urlString)?.submit()?.get() } catch (e: Exception) { diff --git a/app/src/main/java/com/owncloud/android/ui/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/activities/adapter/ActivityListAdapter.kt index 6311715cb1a6..2ae062e338f4 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/adapter/ActivityListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/activities/adapter/ActivityListAdapter.kt @@ -7,9 +7,6 @@ package com.owncloud.android.ui.activities.adapter import android.content.Context -import android.content.res.Configuration -import android.graphics.Color -import android.graphics.PorterDuff import android.graphics.Typeface import android.text.Spannable import android.text.SpannableStringBuilder @@ -30,6 +27,7 @@ import androidx.annotation.DrawableRes import androidx.fragment.app.FragmentActivity import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.CurrentAccountProvider import com.nextcloud.common.NextcloudClient import com.nextcloud.utils.GlideHelper @@ -134,20 +132,20 @@ open class ActivityListAdapter( } if (activity.icon.isNotEmpty()) { - loadImageAsync(activity.icon, holder.binding.icon, R.drawable.ic_activity) + GlideHelper.loadTintableIconIntoImageView( + context, + client, + activity.icon, + holder.binding.icon, + R.drawable.ic_activity, + context.resources.getDimensionPixelSize(R.dimen.activity_icon_width) + ) } - val isNightMode = - (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == - Configuration.UI_MODE_NIGHT_YES - val isFileCreatedOrDeleted = activity.type.equals("file_created", ignoreCase = true) || - activity.type.equals("file_deleted", ignoreCase = true) - - if (!isFileCreatedOrDeleted) { - holder.binding.icon.setColorFilter( - if (isNightMode) Color.WHITE else Color.BLACK, - PorterDuff.Mode.SRC_IN - ) + if (activity.icon.endsWith(COLORED_ICON_SUFFIX, ignoreCase = true)) { + holder.binding.icon.imageTintList = null + } else { + viewThemeUtils.platform.colorImageView(holder.binding.icon, ColorRole.ON_SURFACE_VARIANT) } val richObjectList = activity.richSubjectElement.richObjectList @@ -178,14 +176,15 @@ open class ActivityListAdapter( } } + private suspend fun nextcloudClient(): NextcloudClient = withContext(Dispatchers.IO) { + OwnCloudClientManagerFactory.getDefaultSingleton() + .getNextcloudClientFor(currentAccountProvider.user.toOwnCloudAccount(), context) + } + private fun loadImageAsync(url: String, imageView: ImageView, @DrawableRes placeholder: Int) { context.lifecycleScope.launch { runCatching { - val client = withContext(Dispatchers.IO) { - OwnCloudClientManagerFactory.getDefaultSingleton() - .getNextcloudClientFor(currentAccountProvider.user.toOwnCloudAccount(), context) - } - GlideHelper.loadIntoImageView(context, client, url, imageView, placeholder, false) + GlideHelper.loadIntoImageView(context, nextcloudClient(), url, imageView, placeholder, false) }.onFailure { Log_OC.e(TAG, "Exception loading image: $it") } @@ -318,6 +317,7 @@ open class ActivityListAdapter( companion object { const val HEADER_TYPE = 100 const val ACTIVITY_TYPE = 101 + private const val COLORED_ICON_SUFFIX = "-color.svg" private val TAG: String = ActivityListAdapter::class.java.simpleName } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt index 7fe9a3bf27ff..43641f933d6e 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt @@ -359,13 +359,13 @@ class NotificationsFragment : } override fun onBindIcon(imageView: ImageView, url: String) { - GlideHelper.loadIntoImageView( + GlideHelper.loadTintableIconIntoImageView( requireContext(), client, url, imageView, R.drawable.ic_notification, - false + resources.getDimensionPixelSize(R.dimen.notification_icon_width) ) }