From 724b01b8c79d7b2fba3a038aaaec81dadac4a254 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 27 Mar 2026 11:06:53 +0100 Subject: [PATCH 1/5] fix(glide): crashes Signed-off-by: alperozturk96 --- .../java/com/nextcloud/utils/GlideHelper.kt | 168 ++++++++++-------- .../utils/glide/CustomGlideUriLoader.kt | 2 +- .../utils/glide/GlideStringStreamFetcher.kt | 38 ++-- .../utils/glide/GlideStringStreamLoader.kt | 2 +- .../android/utils/glide/HttpStreamFetcher.kt | 64 +++---- 5 files changed, 140 insertions(+), 134 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/GlideHelper.kt b/app/src/main/java/com/nextcloud/utils/GlideHelper.kt index ce89d910eacd..96c18913a026 100644 --- a/app/src/main/java/com/nextcloud/utils/GlideHelper.kt +++ b/app/src/main/java/com/nextcloud/utils/GlideHelper.kt @@ -41,6 +41,96 @@ import com.owncloud.android.utils.svg.SvgSoftwareLayerSetter object GlideHelper { private const val TAG = "GlideHelper" + @Suppress("TooGenericExceptionCaught") + fun getBitmap(context: Context, url: String?): Bitmap? { + val validatedUrl = validateAndGetURL(url) ?: return null + + return try { + Glide.with(context) + .asBitmap() + .load(validatedUrl) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + .withLogging("downloadImageSynchronous", validatedUrl) + .submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) + .get() + } catch (e: Exception) { + Log_OC.e(TAG, "exception getBitmap: $e") + null + } + } + + fun loadCircularBitmapIntoImageView(context: Context, url: String?, imageView: ImageView, placeholder: Drawable) { + val validatedUrl = validateAndGetURL(url) ?: return + + try { + Glide.with(context) + .asBitmap() + .load(validatedUrl) + .placeholder(placeholder) + .error(placeholder) + .withLogging("loadCircularBitmapIntoImageView", validatedUrl) + .into(object : BitmapImageViewTarget(imageView) { + override fun setResource(resource: Bitmap?) { + val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(context.resources, resource) + circularBitmapDrawable.isCircular = true + imageView.setImageDrawable(circularBitmapDrawable) + } + }) + } catch (e: Exception) { + Log_OC.e(TAG, "exception loadCircularBitmapIntoImageView: $e") + imageView.setImageDrawable(placeholder) + } + } + + @SuppressLint("CheckResult") + fun loadIntoImageView( + context: Context, + client: NextcloudClient?, + url: String?, + imageView: ImageView, + @DrawableRes placeholder: Int, + circleCrop: Boolean = false + ) { + try { + createRequestBuilder(context, client, url) + ?.placeholder(placeholder) + ?.error(placeholder) + ?.apply { if (circleCrop) circleCrop() } + ?.into(imageView) ?: imageView.setImageResource(placeholder) + } catch (e: Exception) { + Log_OC.e(TAG, "exception loadIntoImageView: $e") + imageView.setImageResource(placeholder) + } + } + + fun getDrawable(context: Context, client: NextcloudClient?, urlString: String?): Drawable? { + return try { + createRequestBuilder(context, client, urlString)?.submit()?.get() + } catch (e: Exception) { + Log_OC.e(TAG, "exception getDrawable: $e") + null + } + } + + fun loadIntoTarget( + context: Context, + client: NextcloudClient?, + url: String, + target: Target, + @DrawableRes placeholder: Int + ) { + try { + createRequestBuilder(context, client, url) + ?.placeholder(placeholder) + ?.error(placeholder) + ?.into(target) + } catch (e: Exception) { + Log_OC.e(TAG, "exception loadIntoTarget: $e") + } + } + + // region private methods private class GlideLogger(private val methodName: String, private val identifier: String) : RequestListener { override fun onLoadFailed(p0: GlideException?, p1: Any?, p2: Target, p3: Boolean): Boolean { Log_OC.e(TAG, "$methodName: Load failed for $identifier") @@ -81,8 +171,10 @@ object GlideHelper { .`as`(PictureDrawable::class.java) .load(glideUrl) .apply { - placeholder?.let { placeholder(it) } - placeholder?.let { error(it) } + placeholder?.let { + placeholder(it) + error(it) + } } .listener(SvgSoftwareLayerSetter()) } @@ -94,46 +186,11 @@ object GlideHelper { ): RequestBuilder { val glideUrl = createGlideUrl(url, client) return Glide.with(context) + .asDrawable() .load(glideUrl) .centerCrop() } - @Suppress("TooGenericExceptionCaught") - fun getBitmap(context: Context, url: String?): Bitmap? { - val validatedUrl = validateAndGetURL(url) ?: return null - - return try { - Glide.with(context) - .asBitmap() - .load(validatedUrl) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .skipMemoryCache(true) - .withLogging("downloadImageSynchronous", validatedUrl) - .submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) - .get() - } catch (e: Exception) { - Log_OC.e(TAG, "Could not download image $e") - null - } - } - - fun loadCircularBitmapIntoImageView(context: Context, url: String?, imageView: ImageView, placeholder: Drawable) { - val validatedUrl = validateAndGetURL(url) ?: return - - Glide.with(context) - .asBitmap() - .load(validatedUrl) - .placeholder(placeholder) - .error(placeholder) - .withLogging("loadCircularBitmapIntoImageView", validatedUrl) - .into(object : BitmapImageViewTarget(imageView) { - override fun setResource(resource: Bitmap?) { - val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(context.resources, resource) - circularBitmapDrawable.isCircular = true - imageView.setImageDrawable(circularBitmapDrawable) - } - }) - } @Suppress("UNCHECKED_CAST", "TooGenericExceptionCaught", "ReturnCount") private fun createRequestBuilder(context: Context, client: NextcloudClient?, url: String?): RequestBuilder? { @@ -154,40 +211,9 @@ object GlideHelper { } .withLogging("createRequestBuilder", validatedUrl) as RequestBuilder? } catch (e: Exception) { - Log_OC.e(TAG, "Error createRequestBuilder: $e") + Log_OC.e(TAG, "exception createRequestBuilder: $e") null } } - - @SuppressLint("CheckResult") - fun loadIntoImageView( - context: Context, - client: NextcloudClient?, - url: String?, - imageView: ImageView, - @DrawableRes placeholder: Int, - circleCrop: Boolean = false - ) { - createRequestBuilder(context, client, url) - ?.placeholder(placeholder) - ?.error(placeholder) - ?.apply { if (circleCrop) circleCrop() } - ?.into(imageView) - } - - fun getDrawable(context: Context, client: NextcloudClient?, urlString: String?): Drawable? = - createRequestBuilder(context, client, urlString)?.submit()?.get() - - fun loadIntoTarget( - context: Context, - client: NextcloudClient?, - url: String, - target: Target, - @DrawableRes placeholder: Int - ) { - createRequestBuilder(context, client, url) - ?.placeholder(placeholder) - ?.error(placeholder) - ?.into(target) - } + // endregion } diff --git a/app/src/main/java/com/owncloud/android/utils/glide/CustomGlideUriLoader.kt b/app/src/main/java/com/owncloud/android/utils/glide/CustomGlideUriLoader.kt index b626bde5bc11..9a4f1cf450a1 100644 --- a/app/src/main/java/com/owncloud/android/utils/glide/CustomGlideUriLoader.kt +++ b/app/src/main/java/com/owncloud/android/utils/glide/CustomGlideUriLoader.kt @@ -20,7 +20,7 @@ import java.io.InputStream */ class CustomGlideUriLoader : ModelLoader { override fun buildLoadData(uri: Uri, width: Int, height: Int, options: Options): LoadData = - LoadData(ObjectKey(uri), HttpStreamFetcher(uri.toString())) + LoadData(ObjectKey(uri), HttpStreamFetcher(uri.toString())) override fun handles(uri: Uri): Boolean = false } diff --git a/app/src/main/java/com/owncloud/android/utils/glide/GlideStringStreamFetcher.kt b/app/src/main/java/com/owncloud/android/utils/glide/GlideStringStreamFetcher.kt index 685973c24f33..cddbf7f39455 100644 --- a/app/src/main/java/com/owncloud/android/utils/glide/GlideStringStreamFetcher.kt +++ b/app/src/main/java/com/owncloud/android/utils/glide/GlideStringStreamFetcher.kt @@ -25,46 +25,46 @@ import java.io.InputStream class GlideStringStreamFetcher(private val url: String?) : DataFetcher { private var stream: InputStream? = null + private var get: GetMethod? = null @Suppress("TooGenericExceptionCaught") override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { - var get: GetMethod? = null try { - val ownCloudAccount = - UserAccountManagerImpl.fromContext(MainApp.getAppContext()).currentOwnCloudAccount - val client = OwnCloudClientManagerFactory.getDefaultSingleton() - .getClientFor(ownCloudAccount, MainApp.getAppContext()) + val ownCloudAccount = UserAccountManagerImpl.fromContext(MainApp.getAppContext()).currentOwnCloudAccount + val client = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ownCloudAccount, MainApp.getAppContext()) + get = GetMethod(url) - get.setRequestHeader("Cookie", "nc_sameSiteCookielax=true;nc_sameSiteCookiestrict=true") - get.setRequestHeader(RemoteOperation.OCS_API_HEADER, RemoteOperation.OCS_API_HEADER_VALUE) - val status = client.executeMethod(get) + get?.setRequestHeader("Cookie", "nc_sameSiteCookielax=true;nc_sameSiteCookiestrict=true") + get?.setRequestHeader(RemoteOperation.OCS_API_HEADER, RemoteOperation.OCS_API_HEADER_VALUE) + + val status = client?.executeMethod(get) if (status == HttpStatus.SC_OK) { - val inputStream = get.getResponseBodyAsStream() - this.stream = inputStream - callback.onDataReady(inputStream) + stream = get?.responseBodyAsStream + stream?.let { callback.onDataReady(it) } ?: callback.onLoadFailed(IOException("Stream is null")) } else { - client.exhaustResponse(get.getResponseBodyAsStream()) + client?.exhaustResponse(get?.responseBodyAsStream) callback.onLoadFailed(IOException("Unexpected HTTP status $status")) } } catch (e: Exception) { - Log_OC.e(TAG, e.message, e) + Log_OC.e(TAG, "exception GlideStringStreamFetcher.loadData: $e") callback.onLoadFailed(e) - } finally { - get?.releaseConnection() } } override fun cleanup() { - Log_OC.i(TAG, "Cleanup") try { stream?.close() - } catch (e: IOException) { - Log_OC.w(TAG, "Cleanup failed$e") + } catch (_: IOException) { + } finally { + get?.releaseConnection() } } override fun cancel() { - Log_OC.i(TAG, "Cancel") + try { + get?.abort() + } catch (_: Exception) { + } } override fun getDataClass(): Class = InputStream::class.java diff --git a/app/src/main/java/com/owncloud/android/utils/glide/GlideStringStreamLoader.kt b/app/src/main/java/com/owncloud/android/utils/glide/GlideStringStreamLoader.kt index 7ea6350cc64a..bb80e611ffe4 100644 --- a/app/src/main/java/com/owncloud/android/utils/glide/GlideStringStreamLoader.kt +++ b/app/src/main/java/com/owncloud/android/utils/glide/GlideStringStreamLoader.kt @@ -18,7 +18,7 @@ import java.io.InputStream */ class GlideStringStreamLoader : ModelLoader { override fun buildLoadData(url: String, width: Int, height: Int, options: Options): LoadData = - LoadData(ObjectKey(url), GlideStringStreamFetcher(url)) + LoadData(ObjectKey(url), GlideStringStreamFetcher(url)) override fun handles(s: String): Boolean = true } diff --git a/app/src/main/java/com/owncloud/android/utils/glide/HttpStreamFetcher.kt b/app/src/main/java/com/owncloud/android/utils/glide/HttpStreamFetcher.kt index 6985cba53083..ff452915fd30 100644 --- a/app/src/main/java/com/owncloud/android/utils/glide/HttpStreamFetcher.kt +++ b/app/src/main/java/com/owncloud/android/utils/glide/HttpStreamFetcher.kt @@ -15,84 +15,64 @@ import com.nextcloud.client.account.UserAccountManagerImpl import com.owncloud.android.MainApp import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.operations.RemoteOperation -import com.owncloud.android.lib.common.utils.Log_OC import org.apache.commons.httpclient.HttpStatus import org.apache.commons.httpclient.methods.GetMethod -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream import java.io.IOException import java.io.InputStream + @Suppress("TooGenericExceptionCaught") class HttpStreamFetcher internal constructor(private val url: String) : DataFetcher { private var stream: InputStream? = null + private var get: GetMethod? = null - @Throws(Exception::class) override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { - val ownCloudAccount = - UserAccountManagerImpl.fromContext(MainApp.getAppContext()).currentOwnCloudAccount - val client = OwnCloudClientManagerFactory.getDefaultSingleton() - .getClientFor(ownCloudAccount, MainApp.getAppContext()) - if (client == null || url.isBlank()) { - callback.onLoadFailed(IllegalStateException("Invalid client or URL")) - return - } - - var get: GetMethod? = null try { + val ownCloudAccount = UserAccountManagerImpl.fromContext(MainApp.getAppContext()).currentOwnCloudAccount + val client = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ownCloudAccount, MainApp.getAppContext()) + + if (client == null || url.isBlank()) { + callback.onLoadFailed(IllegalStateException("Invalid client or URL")) + return + } + get = GetMethod(url) - get.setRequestHeader("Cookie", "nc_sameSiteCookielax=true;nc_sameSiteCookiestrict=true") - get.setRequestHeader(RemoteOperation.OCS_API_HEADER, RemoteOperation.OCS_API_HEADER_VALUE) + get?.setRequestHeader("Cookie", "nc_sameSiteCookielax=true;nc_sameSiteCookiestrict=true") + get?.setRequestHeader(RemoteOperation.OCS_API_HEADER, RemoteOperation.OCS_API_HEADER_VALUE) val status = client.executeMethod(get) if (status == HttpStatus.SC_OK) { - val inputStream = getResponseAsInputStream(get) - this.stream = inputStream - callback.onDataReady(inputStream) + stream = get?.responseBodyAsStream + stream?.let { callback.onDataReady(it) } ?: callback.onLoadFailed(IOException("Stream is null")) } else { - client.exhaustResponse(get.responseBodyAsStream) + client.exhaustResponse(get?.responseBodyAsStream) callback.onLoadFailed(IOException("Unexpected HTTP status $status")) } } catch (e: Exception) { - Log_OC.e(TAG, e.message, e) callback.onLoadFailed(e) - } finally { - get?.releaseConnection() } } - private fun getResponseAsInputStream(getMethod: GetMethod): ByteArrayInputStream { - val byteOutputStream = ByteArrayOutputStream() - getMethod.responseBodyAsStream.use { input -> - byteOutputStream.use { output -> - input.copyTo(output) - } - } - - return ByteArrayInputStream(byteOutputStream.toByteArray()) - } - override fun cleanup() { - Log_OC.i(TAG, "Cleanup") try { stream?.close() - } catch (e: IOException) { - Log_OC.w(TAG, "Cleanup failed$e") + } catch (_: IOException) { + } finally { + get?.releaseConnection() } } fun getId(): String = url override fun cancel() { - Log_OC.i(TAG, "Cancel") + try { + get?.abort() + } catch (_: Exception) { + } } override fun getDataClass(): Class = InputStream::class.java override fun getDataSource(): DataSource = DataSource.REMOTE - - companion object { - private val TAG = HttpStreamFetcher::class.java.name - } } From 5d85d8afa1f2c9d5e513af5ee3a962654219acd7 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 27 Mar 2026 11:48:12 +0100 Subject: [PATCH 2/5] fix(glide): lifecycle aware usages Signed-off-by: alperozturk96 --- .../DashboardWidgetConfigurationActivity.kt | 7 +- .../client/widget/DashboardWidgetService.kt | 86 +++++++++-------- .../client/widget/DashboardWidgetUpdater.kt | 40 ++++---- .../java/com/nextcloud/utils/GlideHelper.kt | 93 +++++++++++++------ .../android/ui/activity/DrawerActivity.java | 41 +++----- .../android/ui/activity/UserInfoActivity.java | 14 +-- .../ui/adapter/DashboardWidgetListAdapter.kt | 3 + .../ui/adapter/WidgetListItemViewHolder.kt | 3 +- .../contactsbackup/BackupListAdapter.kt | 27 ++---- 9 files changed, 168 insertions(+), 146 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationActivity.kt b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationActivity.kt index 57b5e2aa6e09..a4d806d9dc05 100644 --- a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationActivity.kt +++ b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationActivity.kt @@ -14,6 +14,7 @@ import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.nextcloud.android.lib.resources.dashboard.DashBoardButtonType import com.nextcloud.android.lib.resources.dashboard.DashboardListWidgetsRemoteOperation @@ -77,7 +78,11 @@ class DashboardWidgetConfigurationActivity : val layoutManager = LinearLayoutManager(this) // TODO follow our new architecture - mAdapter = DashboardWidgetListAdapter(accountManager, clientFactory, this, this) + mAdapter = DashboardWidgetListAdapter(lifecycleScope, + accountManager, + clientFactory, + this, + this) binding.list.apply { setHasFooter(false) setAdapter(mAdapter) 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..55e5b3f67353 100644 --- a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt +++ b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt @@ -22,14 +22,9 @@ import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.network.ClientFactory import com.nextcloud.utils.GlideHelper import com.owncloud.android.R -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() { @@ -67,6 +62,7 @@ class StackRemoteViewsFactory( private lateinit var widgetConfiguration: WidgetConfiguration private var widgetItems: List = emptyList() private var hasLoadMore = false + private val bitmaps = mutableMapOf() override fun onCreate() { Log_OC.d(TAG, "onCreate") @@ -83,25 +79,39 @@ class StackRemoteViewsFactory( } override fun onDataSetChanged() { - CoroutineScope(Dispatchers.IO).launch { - try { - if (!widgetConfiguration.user.isPresent) { - Log_OC.w(TAG, "User not present for widget update") - return@launch - } + try { + if (!widgetConfiguration.user.isPresent) { + Log_OC.w(TAG, "User not present for widget update") + return + } - 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() + 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 + + bitmaps.clear() + + widgetItems.forEachIndexed { index, item -> + if (item.iconUrl.isNotEmpty()) { + try { + val drawable = GlideHelper.getDrawable(context, client, item.iconUrl) + val bmp = drawable?.toBitmap() + if (bmp != null) { + bitmaps[index] = bmp + } + } catch (e: Exception) { + Log_OC.e(TAG, "Bitmap preload failed", e) + } } - hasLoadMore = widgetConfiguration.moreButton != null && widgetItems.size == LIMIT_SIZE - } catch (e: ClientFactory.CreationException) { - Log_OC.e(TAG, "Error updating widget", e) } + } catch (e: ClientFactory.CreationException) { + Log_OC.e(TAG, "Error updating widget", e) } Log_OC.d(TAG, "onDataSetChanged") @@ -144,7 +154,7 @@ class StackRemoteViewsFactory( val widgetItem = widgetItems[position] if (widgetItem.iconUrl.isNotEmpty()) { - loadIcon(widgetItem, this) + loadIcon(position, this) } updateTexts(widgetItem, this) @@ -156,33 +166,21 @@ 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 - } + private fun loadIcon(position: Int, remoteViews: RemoteViews) { + val bitmap = bitmaps[position] ?: run { + remoteViews.setImageViewResource(R.id.icon, R.drawable.ic_dashboard) + return } - } - @Suppress("TooGenericExceptionCaught") - private fun RemoteViews.setRemoteImageView(source: Bitmap) { try { - val bitmap: Bitmap = if (widgetConfiguration.roundIcon) { - BitmapUtils.roundBitmap(source) - } else { - source - } + val finalBitmap = if (widgetConfiguration.roundIcon) { + BitmapUtils.roundBitmap(bitmap) + } else bitmap - setImageViewBitmap(R.id.icon, bitmap) + remoteViews.setImageViewBitmap(R.id.icon, finalBitmap) } catch (e: Exception) { - Log_OC.d(TAG, "Error setting icon", e) - setImageViewResource(R.id.icon, R.drawable.ic_dashboard) + Log_OC.e(TAG, "Error setting bitmap", e) + remoteViews.setImageViewResource(R.id.icon, R.drawable.ic_dashboard) } } 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 3430664dfd8d..cdb34b77db61 100644 --- a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetUpdater.kt +++ b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetUpdater.kt @@ -11,28 +11,27 @@ import android.app.PendingIntent import android.appwidget.AppWidgetManager import android.content.Context import android.content.Intent +import android.graphics.Bitmap +import android.graphics.drawable.Drawable 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.Glide import com.bumptech.glide.request.target.AppWidgetTarget +import com.bumptech.glide.request.transition.Transition import com.nextcloud.android.lib.resources.dashboard.DashboardButton import com.nextcloud.client.account.CurrentAccountProvider import com.nextcloud.client.network.ClientFactory import com.nextcloud.utils.GlideHelper +import com.nextcloud.utils.LinkHelper import com.owncloud.android.R import com.owncloud.android.lib.common.OwnCloudClientManagerFactory 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( private val context: Context, - private val clientFactory: ClientFactory, private val accountProvider: CurrentAccountProvider ) { @@ -154,17 +153,26 @@ class DashboardWidgetUpdater @Inject constructor( // endregion private fun loadIcon(appWidgetId: Int, iconUrl: String, remoteViews: RemoteViews) { - val target = AppWidgetTarget(context, R.id.icon, remoteViews, appWidgetId) - CoroutineScope(Dispatchers.IO).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) + val safeUrl = LinkHelper.validateAndGetURL(iconUrl) ?: return + val client = OwnCloudClientManagerFactory.getDefaultSingleton() + .getNextcloudClientFor(accountProvider.user.toOwnCloudAccount(), context) + val glideUrl = GlideHelper.createGlideUrl(safeUrl, client) + + val target = object : AppWidgetTarget(context, R.id.icon, remoteViews, appWidgetId) { + override fun onResourceReady(resource: Bitmap, transition: Transition?) { + val tinted = BitmapUtils.tintImage(resource, R.color.black) + super.onResourceReady(tinted, transition) + } + + override fun onLoadFailed(errorDrawable: Drawable?) { + remoteViews.setImageViewResource(R.id.icon, R.drawable.ic_dashboard) + AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, remoteViews) } } + + Glide.with(context) + .asBitmap() + .load(glideUrl) + .into(target) } } diff --git a/app/src/main/java/com/nextcloud/utils/GlideHelper.kt b/app/src/main/java/com/nextcloud/utils/GlideHelper.kt index 96c18913a026..d269bde4ee87 100644 --- a/app/src/main/java/com/nextcloud/utils/GlideHelper.kt +++ b/app/src/main/java/com/nextcloud/utils/GlideHelper.kt @@ -13,9 +13,11 @@ import android.graphics.Bitmap 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.drawable.RoundedBitmapDrawableFactory import androidx.core.net.toUri +import androidx.lifecycle.lifecycleScope import com.bumptech.glide.Glide import com.bumptech.glide.RequestBuilder import com.bumptech.glide.load.DataSource @@ -28,8 +30,13 @@ import com.bumptech.glide.request.target.BitmapImageViewTarget import com.bumptech.glide.request.target.Target import com.nextcloud.common.NextcloudClient 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 /** * Utility object for loading images (including SVGs) using Glide. @@ -97,6 +104,7 @@ object GlideHelper { ?.placeholder(placeholder) ?.error(placeholder) ?.apply { if (circleCrop) circleCrop() } + ?.withLogging("loadIntoImageView", url ?: "null") ?.into(imageView) ?: imageView.setImageResource(placeholder) } catch (e: Exception) { Log_OC.e(TAG, "exception loadIntoImageView: $e") @@ -114,49 +122,78 @@ object GlideHelper { } fun loadIntoTarget( - context: Context, - client: NextcloudClient?, + activity: ComponentActivity, + account: OwnCloudAccount?, url: String, target: Target, @DrawableRes placeholder: Int ) { - try { - createRequestBuilder(context, client, url) - ?.placeholder(placeholder) - ?.error(placeholder) - ?.into(target) - } catch (e: Exception) { - Log_OC.e(TAG, "exception loadIntoTarget: $e") + if (account == null) { + Log_OC.e(TAG, "loadIntoTargetWithActivity: account cannot be null") + return + } + + activity.lifecycleScope.launch(Dispatchers.IO) { + val clientFactory = OwnCloudClientManagerFactory.getDefaultSingleton() + val client = clientFactory.getNextcloudClientFor(account, activity) + withContext(Dispatchers.Main) { + try { + createRequestBuilder(activity, client, url) + ?.placeholder(placeholder) + ?.error(placeholder) + ?.withLogging("loadIntoTarget", url) + ?.into(target) + } catch (e: Exception) { + Log_OC.e(TAG, "exception loadIntoTarget: $e") + } + } } } + fun createGlideUrl(url: String, client: NextcloudClient) = GlideUrl( + url, + LazyHeaders.Builder() + .addHeader("Authorization", client.credentials) + .addHeader("User-Agent", "Mozilla/5.0 (Android) Nextcloud-android") + .build() + ) + // region private methods - private class GlideLogger(private val methodName: String, private val identifier: String) : RequestListener { - override fun onLoadFailed(p0: GlideException?, p1: Any?, p2: Target, p3: Boolean): Boolean { + private class GlideLogger( + private val methodName: String, + private val identifier: String + ) : RequestListener { + + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target, + isFirstResource: Boolean + ): Boolean { Log_OC.e(TAG, "$methodName: Load failed for $identifier") - Log_OC.e(TAG, "$methodName: Error: ${p0?.message}") - p0?.logRootCauses(TAG) + Log_OC.e(TAG, "$methodName: Error: ${e?.message}") + e?.logRootCauses(TAG) return false } - override fun onResourceReady(p0: T & Any, p1: Any, p2: Target?, p3: DataSource, p4: Boolean): Boolean { - Log_OC.i(TAG, "Glide load completed: $p0") + override fun onResourceReady( + resource: T & Any, + model: Any?, + target: Target?, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + Log_OC.i(TAG, "$methodName: Successfully loaded $identifier from $dataSource") return false } } private fun isSVG(url: String): Boolean = (url.toUri().encodedPath?.endsWith(".svg") == true) - private fun createGlideUrl(url: String, client: NextcloudClient) = GlideUrl( - url, - LazyHeaders.Builder() - .addHeader("Authorization", client.credentials) - .addHeader("User-Agent", "Mozilla/5.0 (Android) Nextcloud-android") - .build() - ) - - private fun RequestBuilder.withLogging(methodName: String, identifier: String): RequestBuilder = - listener(GlideLogger(methodName, identifier)) + private fun RequestBuilder.withLogging( + methodName: String, + identifier: String + ): RequestBuilder = listener(GlideLogger(methodName, identifier)) @SuppressLint("CheckResult") private fun createSvgRequestBuilder( @@ -191,7 +228,6 @@ object GlideHelper { .centerCrop() } - @Suppress("UNCHECKED_CAST", "TooGenericExceptionCaught", "ReturnCount") private fun createRequestBuilder(context: Context, client: NextcloudClient?, url: String?): RequestBuilder? { if (client == null) { @@ -204,12 +240,11 @@ object GlideHelper { return try { val isSVG = isSVG(validatedUrl) - return if (isSVG) { + if (isSVG) { createSvgRequestBuilder(context, validatedUrl, client) } else { createUrlRequestBuilder(context, client, validatedUrl) - } - .withLogging("createRequestBuilder", validatedUrl) as RequestBuilder? + }.withLogging("createRequestBuilder", validatedUrl) as RequestBuilder? } catch (e: Exception) { Log_OC.e(TAG, "exception createRequestBuilder: $e") null diff --git a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java index cff5cb42600a..379457aa3526 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java @@ -129,7 +129,6 @@ import androidx.fragment.app.Fragment; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hct.Hct; -import kotlin.Unit; /** * Base class to handle setup of the drawer implementation including user switching and avatar fetching and fallback @@ -425,14 +424,11 @@ public void updateHeader() { if (!TextUtils.isEmpty(serverLogoURL) && URLUtil.isValidUrl(serverLogoURL)) { Target target = createSVGLogoTarget(primaryColor, capability); - getClientRepository().getNextcloudClient(nextcloudClient -> { - GlideHelper.INSTANCE.loadIntoTarget(DrawerActivity.this, - nextcloudClient, - serverLogoURL, - target, - R.drawable.background); - return Unit.INSTANCE; - }); + GlideHelper.INSTANCE.loadIntoTarget(this, + accountManager.getCurrentOwnCloudAccount(), + serverLogoURL, + target, + R.drawable.background); } } } @@ -965,14 +961,11 @@ private void updateQuotaLink() { }); Target quotaTarget = createQuotaDrawableTarget(size, mQuotaTextLink); - getClientRepository().getNextcloudClient(nextcloudClient -> { - GlideHelper.INSTANCE.loadIntoTarget(this, - nextcloudClient, - firstQuota.getIconUrl(), - quotaTarget, - R.drawable.ic_link); - return Unit.INSTANCE; - }); + GlideHelper.INSTANCE.loadIntoTarget(this, + accountManager.getCurrentOwnCloudAccount(), + firstQuota.getIconUrl(), + quotaTarget, + R.drawable.ic_link); } else { mQuotaTextLink.setVisibility(View.GONE); } @@ -1097,15 +1090,11 @@ private void updateExternalLinksInDrawer() { .getItemId(); Target iconTarget = createMenuItemTarget(id, greyColor); - getClientRepository().getNextcloudClient(nextcloudClient -> { - GlideHelper.INSTANCE.loadIntoTarget( - this, - nextcloudClient, - link.getIconUrl(), - iconTarget, - R.drawable.ic_link); - return Unit.INSTANCE; - }); + GlideHelper.INSTANCE.loadIntoTarget(this, + accountManager.getCurrentOwnCloudAccount(), + link.getIconUrl(), + iconTarget, + R.drawable.ic_link); } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java index dd137e4f9581..ae7e644965b3 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java @@ -67,7 +67,6 @@ import androidx.fragment.app.FragmentManager; import androidx.lifecycle.Lifecycle; import androidx.recyclerview.widget.RecyclerView; -import kotlin.Unit; /** * This Activity presents the user information. @@ -209,14 +208,11 @@ private void setHeaderImage() { } Target backgroundImageTarget = createBackgroundImageTarget(backgroundImageView); - getClientRepository().getNextcloudClient(nextcloudClient -> { - GlideHelper.INSTANCE.loadIntoTarget(this, - nextcloudClient, - backgroundURL, - backgroundImageTarget, - R.drawable.background); - return Unit.INSTANCE; - }); + GlideHelper.INSTANCE.loadIntoTarget(this, + accountManager.getCurrentOwnCloudAccount(), + backgroundURL, + backgroundImageTarget, + R.drawable.background); } private Target createBackgroundImageTarget(ImageView backgroundImageView) { diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/DashboardWidgetListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/DashboardWidgetListAdapter.kt index 154ab99e1018..d8d6b96d285f 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/DashboardWidgetListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/DashboardWidgetListAdapter.kt @@ -17,8 +17,10 @@ import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.network.ClientFactory import com.nextcloud.client.widget.DashboardWidgetConfigurationInterface import com.owncloud.android.databinding.WidgetListItemBinding +import kotlinx.coroutines.CoroutineScope class DashboardWidgetListAdapter( + private val lifecycleScope: CoroutineScope, val accountManager: UserAccountManager, val clientFactory: ClientFactory, val context: Context, @@ -28,6 +30,7 @@ class DashboardWidgetListAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = WidgetListItemViewHolder( + lifecycleScope, WidgetListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), accountManager, clientFactory, diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/WidgetListItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/WidgetListItemViewHolder.kt index 80d8858d2cc9..c494ca40b023 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/WidgetListItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/WidgetListItemViewHolder.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class WidgetListItemViewHolder( + private val lifecycleScope: CoroutineScope, val binding: WidgetListItemBinding, val accountManager: UserAccountManager, val clientFactory: ClientFactory, @@ -34,7 +35,7 @@ class WidgetListItemViewHolder( ) { binding.layout.setOnClickListener { dashboardWidgetConfigurationInterface.onItemClicked(dashboardWidget) } - CoroutineScope(Dispatchers.IO).launch { + lifecycleScope.launch(Dispatchers.IO) { val client = OwnCloudClientManagerFactory.getDefaultSingleton() .getNextcloudClientFor(accountManager.currentOwnCloudAccount, context) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListAdapter.kt index 69d71a5872f8..553cc55908ca 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListAdapter.kt @@ -20,7 +20,6 @@ import android.view.ViewGroup import android.widget.AdapterView import android.widget.CheckedTextView import android.widget.ImageView -import androidx.lifecycle.lifecycleScope import com.afollestad.sectionedrecyclerview.SectionedRecyclerViewAdapter import com.afollestad.sectionedrecyclerview.SectionedViewHolder import com.bumptech.glide.request.target.CustomTarget @@ -33,7 +32,6 @@ import com.owncloud.android.databinding.BackupListItemHeaderBinding import com.owncloud.android.databinding.CalendarlistListItemBinding import com.owncloud.android.databinding.ContactlistListItemBinding import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.ui.TextDrawable import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment.getDisplayName @@ -41,10 +39,6 @@ import com.owncloud.android.utils.BitmapUtils import com.owncloud.android.utils.theme.ViewThemeUtils import ezvcard.VCard import ezvcard.property.Photo -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import third_parties.sufficientlysecure.AndroidCalendar @Suppress("LongParameterList", "TooManyFunctions") @@ -256,20 +250,13 @@ class BackupListAdapter( } } - backupListFragment.lifecycleScope.launch(Dispatchers.IO) { - val client = OwnCloudClientManagerFactory.getDefaultSingleton() - .getNextcloudClientFor(accountManager.currentOwnCloudAccount, context) - - withContext(Dispatchers.Main) { - GlideHelper.loadIntoTarget( - context, - client, - url, - target, - R.drawable.ic_user_outline - ) - } - } + GlideHelper.loadIntoTarget( + backupListFragment.requireActivity(), + accountManager.currentOwnCloudAccount, + url, + target, + R.drawable.ic_user_outline + ) } } From 4fd106a38b165de16a8094c46a87939dc851309f Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 27 Mar 2026 12:25:18 +0100 Subject: [PATCH 3/5] fix(glide): lifecycle aware usages Signed-off-by: alperozturk96 --- app/src/main/java/com/owncloud/android/utils/svg/SvgDecoder.kt | 2 +- .../com/owncloud/android/utils/svg/SvgDrawableTranscoder.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/utils/svg/SvgDecoder.kt b/app/src/main/java/com/owncloud/android/utils/svg/SvgDecoder.kt index 9bd0713fab05..d6c3c32c7f66 100644 --- a/app/src/main/java/com/owncloud/android/utils/svg/SvgDecoder.kt +++ b/app/src/main/java/com/owncloud/android/utils/svg/SvgDecoder.kt @@ -27,7 +27,7 @@ class SvgDecoder : ResourceDecoder { override fun decode(source: InputStream, width: Int, height: Int, options: Options): Resource { try { val svg = SVG.getFromInputStream(source) - return SimpleResource(svg) + return SimpleResource(svg) } catch (ex: SVGParseException) { throw IOException("Cannot load SVG from stream", ex) } diff --git a/app/src/main/java/com/owncloud/android/utils/svg/SvgDrawableTranscoder.kt b/app/src/main/java/com/owncloud/android/utils/svg/SvgDrawableTranscoder.kt index ec961eab890c..9ad504066ceb 100644 --- a/app/src/main/java/com/owncloud/android/utils/svg/SvgDrawableTranscoder.kt +++ b/app/src/main/java/com/owncloud/android/utils/svg/SvgDrawableTranscoder.kt @@ -26,6 +26,6 @@ class SvgDrawableTranscoder : ResourceTranscoder { val svg = toTranscode.get() val picture = svg.renderToPicture() val drawable = PictureDrawable(picture) - return SimpleResource(drawable) + return SimpleResource(drawable) } } From 8f71e573fde0e4c6ee48bf6e5ba2a7cf9e878a4b Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 27 Mar 2026 12:39:59 +0100 Subject: [PATCH 4/5] fix codacy Signed-off-by: alperozturk96 --- .../DashboardWidgetConfigurationActivity.kt | 6 +++-- .../client/widget/DashboardWidgetService.kt | 6 ++++- .../nextcloud/model/SearchResultEntryType.kt | 2 ++ .../java/com/nextcloud/utils/GlideHelper.kt | 27 +++++++------------ .../extensions/SearchResultEntryExtensions.kt | 6 +++++ .../ui/adapter/UnifiedSearchItemViewHolder.kt | 25 +++++++++++------ .../utils/glide/GlideStringStreamFetcher.kt | 3 ++- .../android/utils/glide/HttpStreamFetcher.kt | 4 +-- 8 files changed, 48 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationActivity.kt b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationActivity.kt index a4d806d9dc05..89f1621c8646 100644 --- a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationActivity.kt +++ b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationActivity.kt @@ -78,11 +78,13 @@ class DashboardWidgetConfigurationActivity : val layoutManager = LinearLayoutManager(this) // TODO follow our new architecture - mAdapter = DashboardWidgetListAdapter(lifecycleScope, + mAdapter = DashboardWidgetListAdapter( + lifecycleScope, accountManager, clientFactory, this, - this) + this + ) binding.list.apply { setHasFooter(false) setAdapter(mAdapter) 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 55e5b3f67353..9a0ed7bbf3fa 100644 --- a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt +++ b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt @@ -27,6 +27,7 @@ import com.owncloud.android.utils.BitmapUtils import dagger.android.AndroidInjection import javax.inject.Inject +@Suppress("TooGenericExceptionCaught") class DashboardWidgetService : RemoteViewsService() { @Inject lateinit var userAccountManager: UserAccountManager @@ -51,6 +52,7 @@ class DashboardWidgetService : RemoteViewsService() { ) } +@Suppress("TooGenericExceptionCaught") class StackRemoteViewsFactory( private val context: Context, val userAccountManager: UserAccountManager, @@ -175,7 +177,9 @@ class StackRemoteViewsFactory( try { val finalBitmap = if (widgetConfiguration.roundIcon) { BitmapUtils.roundBitmap(bitmap) - } else bitmap + } else { + bitmap + } remoteViews.setImageViewBitmap(R.id.icon, finalBitmap) } catch (e: Exception) { diff --git a/app/src/main/java/com/nextcloud/model/SearchResultEntryType.kt b/app/src/main/java/com/nextcloud/model/SearchResultEntryType.kt index e0343aed9efe..7eb6068fb1d0 100644 --- a/app/src/main/java/com/nextcloud/model/SearchResultEntryType.kt +++ b/app/src/main/java/com/nextcloud/model/SearchResultEntryType.kt @@ -29,9 +29,11 @@ enum class SearchResultEntryType { TextCode, Link, Font, + Avatar, Unknown; fun iconId(): Int = when (this) { + Avatar -> R.drawable.ic_user CalendarEvent -> R.drawable.file_calendar Folder -> R.drawable.folder Note -> R.drawable.ic_edit diff --git a/app/src/main/java/com/nextcloud/utils/GlideHelper.kt b/app/src/main/java/com/nextcloud/utils/GlideHelper.kt index d269bde4ee87..36847652f071 100644 --- a/app/src/main/java/com/nextcloud/utils/GlideHelper.kt +++ b/app/src/main/java/com/nextcloud/utils/GlideHelper.kt @@ -44,7 +44,7 @@ import kotlinx.coroutines.withContext * Provides methods for loading images into `ImageView`, `Target`, `Target` ... * from both URLs and URIs. */ -@Suppress("TooManyFunctions") +@Suppress("TooManyFunctions", "TooGenericExceptionCaught") object GlideHelper { private const val TAG = "GlideHelper" @@ -67,7 +67,7 @@ object GlideHelper { } } - fun loadCircularBitmapIntoImageView(context: Context, url: String?, imageView: ImageView, placeholder: Drawable) { + fun loadCircularBitmapIntoImageView(context: Context, url: String?, imageView: ImageView, placeholder: Drawable?) { val validatedUrl = validateAndGetURL(url) ?: return try { @@ -112,13 +112,11 @@ object GlideHelper { } } - fun getDrawable(context: Context, client: NextcloudClient?, urlString: String?): Drawable? { - return try { - createRequestBuilder(context, client, urlString)?.submit()?.get() - } catch (e: Exception) { - Log_OC.e(TAG, "exception getDrawable: $e") - null - } + fun getDrawable(context: Context, client: NextcloudClient?, urlString: String?): Drawable? = try { + createRequestBuilder(context, client, urlString)?.submit()?.get() + } catch (e: Exception) { + Log_OC.e(TAG, "exception getDrawable: $e") + null } fun loadIntoTarget( @@ -159,10 +157,7 @@ object GlideHelper { ) // region private methods - private class GlideLogger( - private val methodName: String, - private val identifier: String - ) : RequestListener { + private class GlideLogger(private val methodName: String, private val identifier: String) : RequestListener { override fun onLoadFailed( e: GlideException?, @@ -190,10 +185,8 @@ 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)) + private fun RequestBuilder.withLogging(methodName: String, identifier: String): RequestBuilder = + listener(GlideLogger(methodName, identifier)) @SuppressLint("CheckResult") private fun createSvgRequestBuilder( 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 f7df7f63b6de..8a42a2f869ec 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/SearchResultEntryExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/SearchResultEntryExtensions.kt @@ -13,6 +13,11 @@ import com.owncloud.android.lib.common.SearchResultEntry fun SearchResultEntry.getType(): SearchResultEntryType { val value = icon.lowercase() + fun isAvatarUrl(url: String): Boolean { + val regex = Regex("""^https?://[^/]+/avatar/[^/]+/\d+$""") + return regex.matches(url) + } + return when { value.contains("icon-folder") -> SearchResultEntryType.Folder value.contains("icon-note") -> SearchResultEntryType.Note @@ -33,6 +38,7 @@ fun SearchResultEntry.getType(): SearchResultEntryType { value.contains("text-code") -> SearchResultEntryType.TextCode value.contains("link") -> SearchResultEntryType.Link value.contains("font") -> SearchResultEntryType.Font + isAvatarUrl(thumbnailUrl) -> SearchResultEntryType.Avatar else -> SearchResultEntryType.Unknown } } 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 9d9412fd5244..174fdeff666e 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 @@ -140,14 +140,23 @@ class UnifiedSearchItemViewHolder( if (entry.thumbnailUrl.isNotBlank()) { filesAction.loadFileThumbnail(entry) { client -> - GlideHelper.loadIntoImageView( - context, - client, - entry.thumbnailUrl, - binding.thumbnail, - entryType.iconId(), - circleCrop = entry.rounded - ) + if (entryType == SearchResultEntryType.Avatar) { + GlideHelper.loadCircularBitmapIntoImageView( + context, + entry.thumbnailUrl, + binding.thumbnail, + ContextCompat.getDrawable(context, R.drawable.ic_user) + ) + } else { + GlideHelper.loadIntoImageView( + context, + client, + entry.thumbnailUrl, + binding.thumbnail, + entryType.iconId(), + circleCrop = entry.rounded + ) + } } } else { binding.thumbnail.setImageDrawable(ContextCompat.getDrawable(context, entryType.iconId())) diff --git a/app/src/main/java/com/owncloud/android/utils/glide/GlideStringStreamFetcher.kt b/app/src/main/java/com/owncloud/android/utils/glide/GlideStringStreamFetcher.kt index cddbf7f39455..e34afb909563 100644 --- a/app/src/main/java/com/owncloud/android/utils/glide/GlideStringStreamFetcher.kt +++ b/app/src/main/java/com/owncloud/android/utils/glide/GlideStringStreamFetcher.kt @@ -31,7 +31,8 @@ class GlideStringStreamFetcher(private val url: String?) : DataFetcher) { try { val ownCloudAccount = UserAccountManagerImpl.fromContext(MainApp.getAppContext()).currentOwnCloudAccount - val client = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ownCloudAccount, MainApp.getAppContext()) + val client = OwnCloudClientManagerFactory.getDefaultSingleton() + .getClientFor(ownCloudAccount, MainApp.getAppContext()) get = GetMethod(url) get?.setRequestHeader("Cookie", "nc_sameSiteCookielax=true;nc_sameSiteCookiestrict=true") diff --git a/app/src/main/java/com/owncloud/android/utils/glide/HttpStreamFetcher.kt b/app/src/main/java/com/owncloud/android/utils/glide/HttpStreamFetcher.kt index ff452915fd30..f519b4a3c701 100644 --- a/app/src/main/java/com/owncloud/android/utils/glide/HttpStreamFetcher.kt +++ b/app/src/main/java/com/owncloud/android/utils/glide/HttpStreamFetcher.kt @@ -20,7 +20,6 @@ import org.apache.commons.httpclient.methods.GetMethod import java.io.IOException import java.io.InputStream - @Suppress("TooGenericExceptionCaught") class HttpStreamFetcher internal constructor(private val url: String) : DataFetcher { @@ -30,7 +29,8 @@ class HttpStreamFetcher internal constructor(private val url: String) : DataFetc override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { try { val ownCloudAccount = UserAccountManagerImpl.fromContext(MainApp.getAppContext()).currentOwnCloudAccount - val client = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ownCloudAccount, MainApp.getAppContext()) + val client = OwnCloudClientManagerFactory.getDefaultSingleton() + .getClientFor(ownCloudAccount, MainApp.getAppContext()) if (client == null || url.isBlank()) { callback.onLoadFailed(IllegalStateException("Invalid client or URL")) From f28c5bcfa79e88f135aa796748a8bb951440565c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 27 Mar 2026 12:57:29 +0100 Subject: [PATCH 5/5] fix codacy Signed-off-by: alperozturk96 --- .../client/widget/DashboardWidgetService.kt | 86 +++++++++---------- .../client/widget/DashboardWidgetUpdater.kt | 41 ++++----- 2 files changed, 58 insertions(+), 69 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 9a0ed7bbf3fa..a55157d00ec8 100644 --- a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt +++ b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt @@ -22,12 +22,16 @@ import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.network.ClientFactory import com.nextcloud.utils.GlideHelper import com.owncloud.android.R +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 -@Suppress("TooGenericExceptionCaught") class DashboardWidgetService : RemoteViewsService() { @Inject lateinit var userAccountManager: UserAccountManager @@ -52,7 +56,6 @@ class DashboardWidgetService : RemoteViewsService() { ) } -@Suppress("TooGenericExceptionCaught") class StackRemoteViewsFactory( private val context: Context, val userAccountManager: UserAccountManager, @@ -64,7 +67,6 @@ class StackRemoteViewsFactory( private lateinit var widgetConfiguration: WidgetConfiguration private var widgetItems: List = emptyList() private var hasLoadMore = false - private val bitmaps = mutableMapOf() override fun onCreate() { Log_OC.d(TAG, "onCreate") @@ -81,39 +83,25 @@ class StackRemoteViewsFactory( } override fun onDataSetChanged() { - try { - if (!widgetConfiguration.user.isPresent) { - Log_OC.w(TAG, "User not present for widget update") - return - } + 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 - - bitmaps.clear() - - widgetItems.forEachIndexed { index, item -> - if (item.iconUrl.isNotEmpty()) { - try { - val drawable = GlideHelper.getDrawable(context, client, item.iconUrl) - val bmp = drawable?.toBitmap() - if (bmp != null) { - bitmaps[index] = bmp - } - } catch (e: Exception) { - Log_OC.e(TAG, "Bitmap preload failed", e) - } + 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) } - } catch (e: ClientFactory.CreationException) { - Log_OC.e(TAG, "Error updating widget", e) } Log_OC.d(TAG, "onDataSetChanged") @@ -156,7 +144,7 @@ class StackRemoteViewsFactory( val widgetItem = widgetItems[position] if (widgetItem.iconUrl.isNotEmpty()) { - loadIcon(position, this) + loadIcon(widgetItem, this) } updateTexts(widgetItem, this) @@ -168,23 +156,33 @@ class StackRemoteViewsFactory( } } - private fun loadIcon(position: Int, remoteViews: RemoteViews) { - val bitmap = bitmaps[position] ?: run { - remoteViews.setImageViewResource(R.id.icon, R.drawable.ic_dashboard) - return + 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 + } } + } + @Suppress("TooGenericExceptionCaught") + private fun RemoteViews.setRemoteImageView(source: Bitmap) { try { - val finalBitmap = if (widgetConfiguration.roundIcon) { - BitmapUtils.roundBitmap(bitmap) + val bitmap: Bitmap = if (widgetConfiguration.roundIcon) { + BitmapUtils.roundBitmap(source) } else { - bitmap + source } - remoteViews.setImageViewBitmap(R.id.icon, finalBitmap) + setImageViewBitmap(R.id.icon, bitmap) } catch (e: Exception) { - Log_OC.e(TAG, "Error setting bitmap", e) - remoteViews.setImageViewResource(R.id.icon, R.drawable.ic_dashboard) + Log_OC.d(TAG, "Error setting icon", e) + setImageViewResource(R.id.icon, R.drawable.ic_dashboard) } } 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 cdb34b77db61..abbff4c4c95d 100644 --- a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetUpdater.kt +++ b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetUpdater.kt @@ -11,29 +11,29 @@ import android.app.PendingIntent import android.appwidget.AppWidgetManager import android.content.Context import android.content.Intent -import android.graphics.Bitmap -import android.graphics.drawable.Drawable 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.Glide import com.bumptech.glide.request.target.AppWidgetTarget -import com.bumptech.glide.request.transition.Transition import com.nextcloud.android.lib.resources.dashboard.DashboardButton import com.nextcloud.client.account.CurrentAccountProvider -import com.nextcloud.client.network.ClientFactory import com.nextcloud.utils.GlideHelper -import com.nextcloud.utils.LinkHelper import com.owncloud.android.R import com.owncloud.android.lib.common.OwnCloudClientManagerFactory 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( private val context: Context, private val accountProvider: CurrentAccountProvider ) { + private val scope = CoroutineScope(Dispatchers.IO) fun updateAppWidget( appWidgetManager: AppWidgetManager, @@ -153,26 +153,17 @@ class DashboardWidgetUpdater @Inject constructor( // endregion private fun loadIcon(appWidgetId: Int, iconUrl: String, remoteViews: RemoteViews) { - val safeUrl = LinkHelper.validateAndGetURL(iconUrl) ?: return - val client = OwnCloudClientManagerFactory.getDefaultSingleton() - .getNextcloudClientFor(accountProvider.user.toOwnCloudAccount(), context) - val glideUrl = GlideHelper.createGlideUrl(safeUrl, client) - - val target = object : AppWidgetTarget(context, R.id.icon, remoteViews, appWidgetId) { - override fun onResourceReady(resource: Bitmap, transition: Transition?) { - val tinted = BitmapUtils.tintImage(resource, R.color.black) - super.onResourceReady(tinted, transition) - } - - override fun onLoadFailed(errorDrawable: Drawable?) { - remoteViews.setImageViewResource(R.id.icon, R.drawable.ic_dashboard) - AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, 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) } } - - Glide.with(context) - .asBitmap() - .load(glideUrl) - .into(target) } }