From 3340a6ed14d0d35dfa3d07bff08d98c9b6812bb5 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 8 Oct 2026 13:11:51 +0200 Subject: [PATCH 1/9] update item id for downloaded files because FragmentStateAdapter.java relies on item id to remove and replace Signed-off-by: alperozturk96 --- .../android/ui/preview/PreviewMediaPagerAdapter.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaPagerAdapter.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaPagerAdapter.kt index 5b96dee8ec1e..5aaf51253689 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaPagerAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaPagerAdapter.kt @@ -35,6 +35,7 @@ class PreviewMediaPagerAdapter( private val obsoletePositions: MutableSet = HashSet() private val downloadErrors: MutableSet = HashSet() private val cachedFragments: SparseArray = SparseArray() + private val downloadedFileIds: MutableSet = HashSet() fun delete(position: Int) { if (position < 0 || position >= mediaFiles.size) { @@ -124,6 +125,10 @@ class PreviewMediaPagerAdapter( cachedFragments[position]?.let { obsoleteFragments.add(it) } obsoletePositions.add(position) mediaFiles[position] = file + + if (!file.isAudioOrVideo()) { + downloadedFileIds.add(file.fileId) + } } fun pendingErrorAt(position: Int): Boolean = downloadErrors.contains(position) @@ -133,9 +138,10 @@ class PreviewMediaPagerAdapter( override fun getItemCount(): Int = mediaFiles.size override fun getItemId(position: Int): Long { - // The item ID function is needed to detect whether the deletion of the current item needs a UI update - return mediaFiles.getOrNull(position)?.fileId ?: position.toLong() + return mediaFiles.getOrNull(position)?.pageId() ?: position.toLong() } - override fun containsItem(itemId: Long): Boolean = mediaFiles.any { it.fileId == itemId } + override fun containsItem(itemId: Long): Boolean = mediaFiles.any { it.pageId() == itemId } + + private fun OCFile.pageId(): Long = if (fileId in downloadedFileIds) -fileId else fileId } From a967b60773368f65f8e9e0be5a8be37795cb3136 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 8 Oct 2026 13:12:26 +0200 Subject: [PATCH 2/9] decode SVG Signed-off-by: alperozturk96 --- .../android/utils/svg/SvgBitmapDecoder.kt | 75 +++++++++++++++++++ .../utils/svg/SvgSoftwareLayerSetter.kt | 52 ------------- 2 files changed, 75 insertions(+), 52 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/utils/svg/SvgBitmapDecoder.kt delete mode 100644 app/src/main/java/com/owncloud/android/utils/svg/SvgSoftwareLayerSetter.kt diff --git a/app/src/main/java/com/owncloud/android/utils/svg/SvgBitmapDecoder.kt b/app/src/main/java/com/owncloud/android/utils/svg/SvgBitmapDecoder.kt new file mode 100644 index 000000000000..57b8e1b2d238 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/utils/svg/SvgBitmapDecoder.kt @@ -0,0 +1,75 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.utils.svg + +import android.graphics.Bitmap +import android.graphics.Canvas +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.ResourceDecoder +import com.bumptech.glide.load.engine.Resource +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool +import com.bumptech.glide.load.resource.bitmap.BitmapResource +import com.bumptech.glide.request.target.Target +import com.caverock.androidsvg.RenderOptions +import com.caverock.androidsvg.SVG +import com.caverock.androidsvg.SVGParseException +import java.io.IOException +import java.io.InputStream +import kotlin.math.min +import kotlin.math.roundToInt + +class SvgBitmapDecoder(private val bitmapPool: BitmapPool) : ResourceDecoder { + + override fun handles(source: InputStream, options: Options): Boolean = true + + override fun decode(source: InputStream, width: Int, height: Int, options: Options): Resource? { + val svg = try { + SVG.getFromInputStream(source) + } catch (e: SVGParseException) { + throw IOException("Cannot load SVG from stream", e) + } + + val intrinsicWidth = svg.documentWidth.takeIf { it > 0 } ?: svg.documentViewBox?.width() + val intrinsicHeight = svg.documentHeight.takeIf { it > 0 } ?: svg.documentViewBox?.height() + + if (svg.documentViewBox == null && intrinsicWidth != null && intrinsicHeight != null) { + svg.setDocumentViewBox(0f, 0f, intrinsicWidth, intrinsicHeight) + } + svg.setDocumentWidth("100%") + svg.setDocumentHeight("100%") + + val (outWidth, outHeight) = outputSize(width, height, intrinsicWidth, intrinsicHeight) + val bitmap = bitmapPool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888) + svg.renderToCanvas( + Canvas(bitmap), + RenderOptions.create().viewPort(0f, 0f, outWidth.toFloat(), outHeight.toFloat()) + ) + return BitmapResource.obtain(bitmap, bitmapPool) + } + + @Suppress("ReturnCount") + private fun outputSize(width: Int, height: Int, intrinsicWidth: Float?, intrinsicHeight: Float?): Pair { + val isOriginalSize = width == Target.SIZE_ORIGINAL || height == Target.SIZE_ORIGINAL + + if (intrinsicWidth == null || intrinsicHeight == null) { + return if (isOriginalSize) DEFAULT_SIZE to DEFAULT_SIZE else width to height + } + + if (isOriginalSize) { + return intrinsicWidth.toPixels() to intrinsicHeight.toPixels() + } + + val scale = min(width / intrinsicWidth, height / intrinsicHeight) + return (intrinsicWidth * scale).toPixels() to (intrinsicHeight * scale).toPixels() + } + + private fun Float.toPixels(): Int = roundToInt().coerceAtLeast(1) + + companion object { + private const val DEFAULT_SIZE = 512 + } +} diff --git a/app/src/main/java/com/owncloud/android/utils/svg/SvgSoftwareLayerSetter.kt b/app/src/main/java/com/owncloud/android/utils/svg/SvgSoftwareLayerSetter.kt deleted file mode 100644 index 0f6900f9a440..000000000000 --- a/app/src/main/java/com/owncloud/android/utils/svg/SvgSoftwareLayerSetter.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2014 Google, Inc. All rights reserved. - * SPDX-License-Identifier: BSD-2-Clause - * - * Borrowed from: - * https://github.com/bumptech/glide/blob/master/samples/svg/src/main/java/com/bumptech/glide/samples/svg/ - * SvgSoftwareLayerSetter.java - */ -package com.owncloud.android.utils.svg - -import android.graphics.drawable.PictureDrawable -import android.widget.ImageView -import com.bumptech.glide.load.DataSource -import com.bumptech.glide.load.engine.GlideException -import com.bumptech.glide.request.RequestListener -import com.bumptech.glide.request.target.ImageViewTarget -import com.bumptech.glide.request.target.Target - -/** - * Listener which updates the [ImageView] to be software rendered, because [ ]/[Picture][android.graphics.Picture] - * can't render on a hardware backed [Canvas][android.graphics.Canvas]. - */ -class SvgSoftwareLayerSetter : RequestListener { - override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target, - isFirstResource: Boolean - ): Boolean { - if (target is ImageViewTarget) { - target.view.setLayerType(ImageView.LAYER_TYPE_NONE, null) - } - - return false - } - - override fun onResourceReady( - resource: PictureDrawable, - model: Any, - target: Target, - dataSource: DataSource, - isFirstResource: Boolean - ): Boolean { - if (target is ImageViewTarget) { - target.view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null) - } - - return false - } -} From 8658c63852d69ad1583665a00fbc556e6b1c35d2 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 8 Oct 2026 13:12:39 +0200 Subject: [PATCH 3/9] unify widget icon sizes Signed-off-by: alperozturk96 --- app/src/main/res/layout/dashboard_widget.xml | 4 ++-- app/src/main/res/layout/widget_item.xml | 4 ++-- app/src/main/res/values/dims.xml | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout/dashboard_widget.xml b/app/src/main/res/layout/dashboard_widget.xml index cc3b2bbaf034..ab32a41a97cb 100644 --- a/app/src/main/res/layout/dashboard_widget.xml +++ b/app/src/main/res/layout/dashboard_widget.xml @@ -27,8 +27,8 @@ 8dp 4dp 32dp + 32dp 12dp 2dp 16dp From 0e397e5d849fb8f90646d0b841dc8ee7a784dff9 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 8 Oct 2026 13:12:55 +0200 Subject: [PATCH 4/9] update widget calls Signed-off-by: alperozturk96 --- .../client/widget/DashboardWidgetService.kt | 60 +++++++------------ .../client/widget/DashboardWidgetUpdater.kt | 49 +++++++-------- 2 files changed, 44 insertions(+), 65 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt index a55157d00ec8..fbd78841c6b4 100644 --- a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt +++ b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt @@ -26,10 +26,6 @@ import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.utils.BitmapUtils import dagger.android.AndroidInjection -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import javax.inject.Inject class DashboardWidgetService : RemoteViewsService() { @@ -78,33 +74,29 @@ class StackRemoteViewsFactory( // TODO show error Log_OC.e(this, "No user found!") } - - onDataSetChanged() } override fun onDataSetChanged() { - CoroutineScope(Dispatchers.IO).launch { - try { - if (!widgetConfiguration.user.isPresent) { - Log_OC.w(TAG, "User not present for widget update") - return@launch - } - - val client = clientFactory.createNextcloudClient(widgetConfiguration.user.get()) - val result = DashboardGetWidgetItemsRemoteOperation(widgetConfiguration.widgetId, LIMIT_SIZE) - .execute(client) - widgetItems = if (result.isSuccess) { - result.resultData[widgetConfiguration.widgetId] ?: emptyList() - } else { - emptyList() - } - hasLoadMore = widgetConfiguration.moreButton != null && widgetItems.size == LIMIT_SIZE - } catch (e: ClientFactory.CreationException) { - Log_OC.e(TAG, "Error updating widget", e) - } + Log_OC.d(TAG, "onDataSetChanged") + + if (!widgetConfiguration.user.isPresent) { + Log_OC.w(TAG, "User not present for widget update") + return } - Log_OC.d(TAG, "onDataSetChanged") + try { + val client = clientFactory.createNextcloudClient(widgetConfiguration.user.get()) + val result = DashboardGetWidgetItemsRemoteOperation(widgetConfiguration.widgetId, LIMIT_SIZE) + .execute(client) + widgetItems = if (result.isSuccess) { + result.resultData[widgetConfiguration.widgetId] ?: emptyList() + } else { + emptyList() + } + hasLoadMore = widgetConfiguration.moreButton != null && widgetItems.size == LIMIT_SIZE + } catch (e: ClientFactory.CreationException) { + Log_OC.e(TAG, "Error updating widget", e) + } } override fun onDestroy() { @@ -157,17 +149,11 @@ class StackRemoteViewsFactory( } private fun loadIcon(widgetItem: DashboardWidgetItem, remoteViews: RemoteViews) { - CoroutineScope(Dispatchers.IO).launch { - val client = OwnCloudClientManagerFactory.getDefaultSingleton() - .getNextcloudClientFor(userAccountManager.user.toOwnCloudAccount(), context) - val pictureDrawable = GlideHelper.getDrawable(context, client, widgetItem.iconUrl) - val bitmap = pictureDrawable?.toBitmap() ?: return@launch - - withContext(Dispatchers.Main) { - remoteViews.setRemoteImageView(bitmap) - return@withContext - } - } + val client = OwnCloudClientManagerFactory.getDefaultSingleton() + .getNextcloudClientFor(userAccountManager.user.toOwnCloudAccount(), context) + val iconSize = context.resources.getDimensionPixelSize(R.dimen.dashboard_widget_icon_size) + val bitmap = GlideHelper.getDrawable(context, client, widgetItem.iconUrl, iconSize)?.toBitmap() ?: return + remoteViews.setRemoteImageView(bitmap) } @Suppress("TooGenericExceptionCaught") diff --git a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetUpdater.kt b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetUpdater.kt index abbff4c4c95d..8087f51c5208 100644 --- a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetUpdater.kt +++ b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetUpdater.kt @@ -11,12 +11,12 @@ import android.app.PendingIntent import android.appwidget.AppWidgetManager import android.content.Context import android.content.Intent +import android.graphics.Bitmap import android.os.Build import android.view.View import android.widget.RemoteViews import androidx.core.graphics.drawable.toBitmap import androidx.core.net.toUri -import com.bumptech.glide.request.target.AppWidgetTarget import com.nextcloud.android.lib.resources.dashboard.DashboardButton import com.nextcloud.client.account.CurrentAccountProvider import com.nextcloud.utils.GlideHelper @@ -26,7 +26,6 @@ import com.owncloud.android.utils.BitmapUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import javax.inject.Inject class DashboardWidgetUpdater @Inject constructor( @@ -47,23 +46,24 @@ class DashboardWidgetUpdater @Inject constructor( data = toUri(Intent.URI_INTENT_SCHEME).toUri() } - val views = RemoteViews(context.packageName, R.layout.dashboard_widget).apply { - setRemoteAdapter(R.id.list, intent) - setEmptyView(R.id.list, R.id.empty_view) - setTextViewText(R.id.title, title) + scope.launch { + val icon = if (iconUrl.isNotEmpty()) loadIcon(iconUrl) else null - setAddButton(addButton, appWidgetId, this) - setPendingReload(this, appWidgetId) - setPendingClick(this) + val views = RemoteViews(context.packageName, R.layout.dashboard_widget).apply { + icon?.let { setImageViewBitmap(R.id.icon, it) } + setRemoteAdapter(R.id.list, intent) + setEmptyView(R.id.list, R.id.empty_view) + setTextViewText(R.id.title, title) - if (iconUrl.isNotEmpty()) { - loadIcon(appWidgetId, iconUrl, this) + setAddButton(addButton, appWidgetId, this) + setPendingReload(this, appWidgetId) + setPendingClick(this) } - } - appWidgetManager.run { - updateAppWidget(appWidgetId, views) - notifyAppWidgetViewDataChanged(appWidgetId, R.id.list) + appWidgetManager.run { + updateAppWidget(appWidgetId, views) + notifyAppWidgetViewDataChanged(appWidgetId, R.id.list) + } } } @@ -152,18 +152,11 @@ class DashboardWidgetUpdater @Inject constructor( } // endregion - private fun loadIcon(appWidgetId: Int, iconUrl: String, remoteViews: RemoteViews) { - val target = AppWidgetTarget(context, R.id.icon, remoteViews, appWidgetId) - scope.launch { - val client = OwnCloudClientManagerFactory.getDefaultSingleton() - .getNextcloudClientFor(accountProvider.user.toOwnCloudAccount(), context) - val drawable = GlideHelper.getDrawable(context, client, iconUrl) - val bitmap = drawable?.toBitmap() ?: return@launch - val tintedBitmap = BitmapUtils.tintImage(bitmap, R.color.black) - - withContext(Dispatchers.Main) { - target.onResourceReady(tintedBitmap, null) - } - } + private fun loadIcon(iconUrl: String): Bitmap? { + val client = OwnCloudClientManagerFactory.getDefaultSingleton() + .getNextcloudClientFor(accountProvider.user.toOwnCloudAccount(), context) + val iconSize = context.resources.getDimensionPixelSize(R.dimen.dashboard_widget_icon_size) + val bitmap = GlideHelper.getDrawable(context, client, iconUrl, iconSize)?.toBitmap() ?: return null + return BitmapUtils.tintImage(bitmap, context.getColor(R.color.black)) } } From 53e60b0fa81f4a8fa0d634c2225c7861afc4b2cb Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 8 Oct 2026 13:13:14 +0200 Subject: [PATCH 5/9] update glide Signed-off-by: alperozturk96 --- .../java/com/nextcloud/utils/GlideHelper.kt | 55 +++++++------------ .../utils/glide/NextcloudGlideModule.kt | 7 +++ 2 files changed, 27 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/GlideHelper.kt b/app/src/main/java/com/nextcloud/utils/GlideHelper.kt index 7075b90c1736..2b03442c776f 100644 --- a/app/src/main/java/com/nextcloud/utils/GlideHelper.kt +++ b/app/src/main/java/com/nextcloud/utils/GlideHelper.kt @@ -13,7 +13,6 @@ 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 @@ -21,7 +20,6 @@ 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 import com.bumptech.glide.RequestBuilder @@ -33,6 +31,7 @@ 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.DrawableImageViewTarget import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.transition.Transition import com.nextcloud.common.NextcloudClient @@ -40,7 +39,6 @@ import com.nextcloud.utils.LinkHelper.validateAndGetURL import com.owncloud.android.lib.common.OwnCloudAccount import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.utils.Log_OC -import com.owncloud.android.utils.svg.SvgSoftwareLayerSetter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -112,7 +110,7 @@ object GlideHelper { ?.error(placeholder) ?.apply { if (circleCrop) circleCrop() } ?.withLogging("loadIntoImageView", url ?: "null") - ?.into(imageView) ?: imageView.setImageResource(placeholder) + ?.into(UntintedDrawableImageViewTarget(imageView)) ?: imageView.setImageResource(placeholder) } catch (e: Exception) { Log_OC.e(TAG, "exception loadIntoImageView: $e") imageView.setImageResource(placeholder) @@ -121,8 +119,7 @@ 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. + * takes effect. * * @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. @@ -180,8 +177,16 @@ object GlideHelper { return bitmap.toDrawable(context.resources) } - fun getDrawable(context: Context, client: NextcloudClient?, urlString: String?): Drawable? = try { - createRequestBuilder(context, client, urlString)?.submit()?.get() + fun getDrawable( + context: Context, + client: NextcloudClient?, + urlString: String?, + sizePx: Int = Target.SIZE_ORIGINAL + ): Drawable? = try { + createRequestBuilder(context, client, urlString) + ?.withLogging("getDrawable", urlString ?: "null") + ?.submit(sizePx, sizePx) + ?.get() } catch (e: Exception) { Log_OC.e(TAG, "exception getDrawable: $e") null @@ -251,30 +256,16 @@ object GlideHelper { } } - private fun isSVG(url: String): Boolean = (url.toUri().encodedPath?.endsWith(".svg") == true) - private fun RequestBuilder.withLogging(methodName: String, identifier: String): RequestBuilder = listener(GlideLogger(methodName, identifier)) - @SuppressLint("CheckResult") - private fun createSvgRequestBuilder( - context: Context, - uri: String, - client: NextcloudClient, - placeholder: Int? = null - ): RequestBuilder { - val glideUrl = createGlideUrl(uri, client) - - return Glide.with(context) - .`as`(PictureDrawable::class.java) - .load(glideUrl) - .apply { - placeholder?.let { - placeholder(it) - error(it) - } + private class UntintedDrawableImageViewTarget(imageView: ImageView) : DrawableImageViewTarget(imageView) { + override fun setResource(resource: Drawable?) { + if (resource != null) { + view.imageTintList = null } - .listener(SvgSoftwareLayerSetter()) + super.setResource(resource) + } } private fun createUrlRequestBuilder( @@ -299,13 +290,7 @@ object GlideHelper { val validatedUrl = validateAndGetURL(url) ?: return null return try { - val isSVG = isSVG(validatedUrl) - - if (isSVG) { - createSvgRequestBuilder(context, validatedUrl, client) - } else { - createUrlRequestBuilder(context, client, validatedUrl) - }.withLogging("createRequestBuilder", validatedUrl) as RequestBuilder? + createUrlRequestBuilder(context, client, validatedUrl) as RequestBuilder? } catch (e: Exception) { Log_OC.e(TAG, "exception createRequestBuilder: $e") null diff --git a/app/src/main/java/com/owncloud/android/utils/glide/NextcloudGlideModule.kt b/app/src/main/java/com/owncloud/android/utils/glide/NextcloudGlideModule.kt index 4041e5f1ac39..e07ee40827fc 100644 --- a/app/src/main/java/com/owncloud/android/utils/glide/NextcloudGlideModule.kt +++ b/app/src/main/java/com/owncloud/android/utils/glide/NextcloudGlideModule.kt @@ -17,6 +17,7 @@ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule import com.caverock.androidsvg.SVG import com.owncloud.android.utils.svg.SVGorImage +import com.owncloud.android.utils.svg.SvgBitmapDecoder import com.owncloud.android.utils.svg.SvgDecoder import com.owncloud.android.utils.svg.SvgDrawableTranscoder import com.owncloud.android.utils.svg.SvgOrImageBitmapTranscoder @@ -31,6 +32,12 @@ class NextcloudGlideModule : AppGlideModule() { .register(SVGorImage::class.java, Bitmap::class.java, SvgOrImageBitmapTranscoder(SVG_SIZE, SVG_SIZE)) .register(SVG::class.java, PictureDrawable::class.java, SvgDrawableTranscoder()) .append(InputStream::class.java, SVG::class.java, SvgDecoder()) + .append( + Registry.BUCKET_BITMAP, + InputStream::class.java, + Bitmap::class.java, + SvgBitmapDecoder(glide.bitmapPool) + ) } // Disable manifest parsing to avoid adding similar modules twice. From 0f5f56b93bae4181c21d382102e1d0491b701b8c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 8 Oct 2026 13:14:00 +0200 Subject: [PATCH 6/9] add monochrome icon support and use correct size for unified search Signed-off-by: alperozturk96 --- .../extensions/SearchResultEntryExtensions.kt | 27 +++++++++++++++++++ .../ui/adapter/UnifiedSearchItemViewHolder.kt | 25 ++++++++++++++--- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/SearchResultEntryExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/SearchResultEntryExtensions.kt index 8a42a2f869ec..1ef7b7adadb0 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/SearchResultEntryExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/SearchResultEntryExtensions.kt @@ -7,9 +7,16 @@ package com.nextcloud.utils.extensions +import androidx.core.net.toUri import com.nextcloud.model.SearchResultEntryType import com.owncloud.android.lib.common.SearchResultEntry +private const val SVG_EXTENSION = ".svg" +private const val COLORED_ICON_SUFFIX = "-color.svg" +private const val PREVIEW_PATH = "/core/preview" +private const val PREVIEW_WIDTH_PARAMETER = "x" +private const val PREVIEW_HEIGHT_PARAMETER = "y" + fun SearchResultEntry.getType(): SearchResultEntryType { val value = icon.lowercase() @@ -42,3 +49,23 @@ fun SearchResultEntry.getType(): SearchResultEntryType { else -> SearchResultEntryType.Unknown } } + +fun SearchResultEntry.isMonochromeIcon(): Boolean { + val path = thumbnailUrl.toUri().path ?: return false + return path.endsWith(SVG_EXTENSION, ignoreCase = true) && !path.endsWith(COLORED_ICON_SUFFIX, ignoreCase = true) +} + +fun SearchResultEntry.thumbnailUrlForSize(sizePx: Int): String { + val uri = thumbnailUrl.toUri() + if (uri.path?.endsWith(PREVIEW_PATH) != true) { + return thumbnailUrl + } + + val builder = uri.buildUpon().clearQuery() + uri.queryParameterNames.forEach { name -> + val isSizeParameter = name == PREVIEW_WIDTH_PARAMETER || name == PREVIEW_HEIGHT_PARAMETER + val value = if (isSizeParameter) sizePx.toString() else uri.getQueryParameter(name) + builder.appendQueryParameter(name, value) + } + return builder.build().toString() +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchItemViewHolder.kt index 5618eb892e3c..a33cf6c1637b 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchItemViewHolder.kt @@ -11,6 +11,8 @@ import android.content.Context import android.view.View import android.widget.TextView import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap +import androidx.core.graphics.drawable.toDrawable import androidx.core.widget.ImageViewCompat import com.afollestad.sectionedrecyclerview.SectionedViewHolder import com.bumptech.glide.Glide @@ -21,7 +23,9 @@ import com.nextcloud.utils.CalendarEventManager import com.nextcloud.utils.ContactManager import com.nextcloud.utils.GlideHelper import com.nextcloud.utils.extensions.getType +import com.nextcloud.utils.extensions.isMonochromeIcon import com.nextcloud.utils.extensions.setVisibleIf +import com.nextcloud.utils.extensions.thumbnailUrlForSize import com.nextcloud.utils.thumbnail.ThumbnailArguments import com.nextcloud.utils.thumbnail.ThumbnailGenerator import com.owncloud.android.R @@ -104,7 +108,10 @@ class UnifiedSearchItemViewHolder( private fun bindLocalFileThumbnail(file: OCFile) { if (file.remoteId == null || !file.isPreviewAvailable) { + val iconSize = context.resources.getDimensionPixelSize(R.dimen.file_icon_size) val icon = MimeTypeUtil.getFileTypeIcon(file.mimeType, file.fileName, context, viewThemeUtils) + ?.toBitmap(iconSize, iconSize) + ?.toDrawable(context.resources) binding.thumbnail.apply { setImageDrawable(icon) clearColorFilter() @@ -126,21 +133,31 @@ class UnifiedSearchItemViewHolder( } if (entry.thumbnailUrl.isNotBlank()) { + val iconSize = context.resources.getDimensionPixelSize(R.dimen.file_icon_size) filesAction.loadFileThumbnail(entry) { client -> - if (entryType == SearchResultEntryType.Avatar) { - GlideHelper.loadCircularBitmapIntoImageView( + when { + entryType == SearchResultEntryType.Avatar -> GlideHelper.loadCircularBitmapIntoImageView( context, entry.thumbnailUrl, binding.thumbnail, ContextCompat.getDrawable(context, R.drawable.ic_user) ) - } else { - GlideHelper.loadIntoImageView( + + entry.isMonochromeIcon() -> GlideHelper.loadTintableIconIntoImageView( context, client, entry.thumbnailUrl, binding.thumbnail, entryType.iconId(), + iconSize + ) + + else -> GlideHelper.loadIntoImageView( + context, + client, + entry.thumbnailUrlForSize(iconSize), + binding.thumbnail, + entryType.iconId(), circleCrop = entry.rounded ) } From cc3fb27b0e2651ece973133e5275b66513dbd303 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 8 Oct 2026 13:14:09 +0200 Subject: [PATCH 7/9] handle picture drawable as well Signed-off-by: alperozturk96 --- .../com/owncloud/android/ui/preview/PreviewImageFragment.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt index a5315591a1e9..c10cb7c8231e 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt @@ -705,6 +705,9 @@ class PreviewImageFragment : bitmapWidth = convertDpToPixel(bitmap.width.toFloat(), getActivity()) bitmapHeight = convertDpToPixel(bitmap.height.toFloat(), getActivity()) + } else if (bitmapDrawable is PictureDrawable) { + bitmapWidth = bitmapDrawable.intrinsicWidth + bitmapHeight = bitmapDrawable.intrinsicHeight } else { if (bitmapDrawable == null) { return layerDrawable From cf9afed44270893174b9ced72a9431c90038f2f3 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 8 Oct 2026 14:09:24 +0200 Subject: [PATCH 8/9] fix kotlin spotless Signed-off-by: alperozturk96 --- .../owncloud/android/ui/preview/PreviewMediaPagerAdapter.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaPagerAdapter.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaPagerAdapter.kt index 5aaf51253689..7c8ffbf3860e 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaPagerAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaPagerAdapter.kt @@ -137,9 +137,7 @@ class PreviewMediaPagerAdapter( override fun getItemCount(): Int = mediaFiles.size - override fun getItemId(position: Int): Long { - return mediaFiles.getOrNull(position)?.pageId() ?: position.toLong() - } + override fun getItemId(position: Int): Long = mediaFiles.getOrNull(position)?.pageId() ?: position.toLong() override fun containsItem(itemId: Long): Boolean = mediaFiles.any { it.pageId() == itemId } From c982815607bbe97ee204dba0ca5475cbf92e2a20 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 14 Oct 2026 11:25:21 +0200 Subject: [PATCH 9/9] make widget images rounded as well Signed-off-by: alperozturk96 --- .../client/widget/DashboardWidgetService.kt | 3 ++- .../java/com/owncloud/android/utils/BitmapUtils.kt | 14 ++++++++++++++ app/src/main/res/values/dims.xml | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt index fbd78841c6b4..dedd888271f1 100644 --- a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt +++ b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt @@ -162,7 +162,8 @@ class StackRemoteViewsFactory( val bitmap: Bitmap = if (widgetConfiguration.roundIcon) { BitmapUtils.roundBitmap(source) } else { - source + val radius = context.resources.getDimension(R.dimen.file_icon_rounded_corner_radius) + BitmapUtils.roundCorners(source, radius) } setImageViewBitmap(R.id.icon, bitmap) diff --git a/app/src/main/java/com/owncloud/android/utils/BitmapUtils.kt b/app/src/main/java/com/owncloud/android/utils/BitmapUtils.kt index 66ca4c0fe8f1..b2fe6043ba9f 100644 --- a/app/src/main/java/com/owncloud/android/utils/BitmapUtils.kt +++ b/app/src/main/java/com/owncloud/android/utils/BitmapUtils.kt @@ -307,6 +307,20 @@ object BitmapUtils { } } + fun roundCorners(bitmap: Bitmap, radius: Float): Bitmap { + val rect = Rect(0, 0, bitmap.width, bitmap.height) + val paint = Paint().apply { + isAntiAlias = true + color = resources.getColor(R.color.white, null) + } + + return createBitmap(bitmap.width, bitmap.height).applyCanvas { + drawRoundRect(RectF(rect), radius, radius, paint) + paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) + drawBitmap(bitmap, rect, rect, paint) + } + } + fun tintImage(bitmap: Bitmap, color: Int): Bitmap { val paint = Paint().apply { colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) } return createBitmap(bitmap.width, bitmap.height).applyCanvas { drawBitmap(bitmap, 0f, 0f, paint) } diff --git a/app/src/main/res/values/dims.xml b/app/src/main/res/values/dims.xml index d9e7378d760c..16afb882cbe3 100644 --- a/app/src/main/res/values/dims.xml +++ b/app/src/main/res/values/dims.xml @@ -22,7 +22,7 @@ 40dp 100dp 128dp - 8dp + 4dp 3dp 512 28dp