Skip to content

Commit fe2c76d

Browse files
Merge pull request #16760 from nextcloud/fix/glide
fix(glide): crashes, adds avatar support
2 parents 6ba97c7 + f28c5bc commit fe2c76d

17 files changed

Lines changed: 252 additions & 218 deletions

app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationActivity.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import android.os.Bundle
1414
import android.view.View
1515
import androidx.appcompat.app.AppCompatActivity
1616
import androidx.appcompat.content.res.AppCompatResources
17+
import androidx.lifecycle.lifecycleScope
1718
import androidx.recyclerview.widget.LinearLayoutManager
1819
import com.nextcloud.android.lib.resources.dashboard.DashBoardButtonType
1920
import com.nextcloud.android.lib.resources.dashboard.DashboardListWidgetsRemoteOperation
@@ -77,7 +78,13 @@ class DashboardWidgetConfigurationActivity :
7778

7879
val layoutManager = LinearLayoutManager(this)
7980
// TODO follow our new architecture
80-
mAdapter = DashboardWidgetListAdapter(accountManager, clientFactory, this, this)
81+
mAdapter = DashboardWidgetListAdapter(
82+
lifecycleScope,
83+
accountManager,
84+
clientFactory,
85+
this,
86+
this
87+
)
8188
binding.list.apply {
8289
setHasFooter(false)
8390
setAdapter(mAdapter)

app/src/main/java/com/nextcloud/client/widget/DashboardWidgetUpdater.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import androidx.core.net.toUri
1919
import com.bumptech.glide.request.target.AppWidgetTarget
2020
import com.nextcloud.android.lib.resources.dashboard.DashboardButton
2121
import com.nextcloud.client.account.CurrentAccountProvider
22-
import com.nextcloud.client.network.ClientFactory
2322
import com.nextcloud.utils.GlideHelper
2423
import com.owncloud.android.R
2524
import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
@@ -32,9 +31,9 @@ import javax.inject.Inject
3231

3332
class DashboardWidgetUpdater @Inject constructor(
3433
private val context: Context,
35-
private val clientFactory: ClientFactory,
3634
private val accountProvider: CurrentAccountProvider
3735
) {
36+
private val scope = CoroutineScope(Dispatchers.IO)
3837

3938
fun updateAppWidget(
4039
appWidgetManager: AppWidgetManager,
@@ -155,7 +154,7 @@ class DashboardWidgetUpdater @Inject constructor(
155154

156155
private fun loadIcon(appWidgetId: Int, iconUrl: String, remoteViews: RemoteViews) {
157156
val target = AppWidgetTarget(context, R.id.icon, remoteViews, appWidgetId)
158-
CoroutineScope(Dispatchers.IO).launch {
157+
scope.launch {
159158
val client = OwnCloudClientManagerFactory.getDefaultSingleton()
160159
.getNextcloudClientFor(accountProvider.user.toOwnCloudAccount(), context)
161160
val drawable = GlideHelper.getDrawable(context, client, iconUrl)

app/src/main/java/com/nextcloud/model/SearchResultEntryType.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@ enum class SearchResultEntryType {
2929
TextCode,
3030
Link,
3131
Font,
32+
Avatar,
3233
Unknown;
3334

3435
fun iconId(): Int = when (this) {
36+
Avatar -> R.drawable.ic_user
3537
CalendarEvent -> R.drawable.file_calendar
3638
Folder -> R.drawable.folder
3739
Note -> R.drawable.ic_edit

app/src/main/java/com/nextcloud/utils/GlideHelper.kt

Lines changed: 141 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import android.graphics.Bitmap
1313
import android.graphics.drawable.Drawable
1414
import android.graphics.drawable.PictureDrawable
1515
import android.widget.ImageView
16+
import androidx.activity.ComponentActivity
1617
import androidx.annotation.DrawableRes
1718
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
1819
import androidx.core.net.toUri
20+
import androidx.lifecycle.lifecycleScope
1921
import com.bumptech.glide.Glide
2022
import com.bumptech.glide.RequestBuilder
2123
import com.bumptech.glide.load.DataSource
@@ -28,43 +30,161 @@ import com.bumptech.glide.request.target.BitmapImageViewTarget
2830
import com.bumptech.glide.request.target.Target
2931
import com.nextcloud.common.NextcloudClient
3032
import com.nextcloud.utils.LinkHelper.validateAndGetURL
33+
import com.owncloud.android.lib.common.OwnCloudAccount
34+
import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
3135
import com.owncloud.android.lib.common.utils.Log_OC
3236
import com.owncloud.android.utils.svg.SvgSoftwareLayerSetter
37+
import kotlinx.coroutines.Dispatchers
38+
import kotlinx.coroutines.launch
39+
import kotlinx.coroutines.withContext
3340

3441
/**
3542
* Utility object for loading images (including SVGs) using Glide.
3643
*
3744
* Provides methods for loading images into `ImageView`, `Target<Drawable>`, `Target<Bitmap>` ...
3845
* from both URLs and URIs.
3946
*/
40-
@Suppress("TooManyFunctions")
47+
@Suppress("TooManyFunctions", "TooGenericExceptionCaught")
4148
object GlideHelper {
4249
private const val TAG = "GlideHelper"
4350

44-
private class GlideLogger<T>(private val methodName: String, private val identifier: String) : RequestListener<T> {
45-
override fun onLoadFailed(p0: GlideException?, p1: Any?, p2: Target<T>, p3: Boolean): Boolean {
46-
Log_OC.e(TAG, "$methodName: Load failed for $identifier")
47-
Log_OC.e(TAG, "$methodName: Error: ${p0?.message}")
48-
p0?.logRootCauses(TAG)
49-
return false
51+
@Suppress("TooGenericExceptionCaught")
52+
fun getBitmap(context: Context, url: String?): Bitmap? {
53+
val validatedUrl = validateAndGetURL(url) ?: return null
54+
55+
return try {
56+
Glide.with(context)
57+
.asBitmap()
58+
.load(validatedUrl)
59+
.diskCacheStrategy(DiskCacheStrategy.NONE)
60+
.skipMemoryCache(true)
61+
.withLogging("downloadImageSynchronous", validatedUrl)
62+
.submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
63+
.get()
64+
} catch (e: Exception) {
65+
Log_OC.e(TAG, "exception getBitmap: $e")
66+
null
5067
}
68+
}
5169

52-
override fun onResourceReady(p0: T & Any, p1: Any, p2: Target<T?>?, p3: DataSource, p4: Boolean): Boolean {
53-
Log_OC.i(TAG, "Glide load completed: $p0")
54-
return false
70+
fun loadCircularBitmapIntoImageView(context: Context, url: String?, imageView: ImageView, placeholder: Drawable?) {
71+
val validatedUrl = validateAndGetURL(url) ?: return
72+
73+
try {
74+
Glide.with(context)
75+
.asBitmap()
76+
.load(validatedUrl)
77+
.placeholder(placeholder)
78+
.error(placeholder)
79+
.withLogging("loadCircularBitmapIntoImageView", validatedUrl)
80+
.into(object : BitmapImageViewTarget(imageView) {
81+
override fun setResource(resource: Bitmap?) {
82+
val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(context.resources, resource)
83+
circularBitmapDrawable.isCircular = true
84+
imageView.setImageDrawable(circularBitmapDrawable)
85+
}
86+
})
87+
} catch (e: Exception) {
88+
Log_OC.e(TAG, "exception loadCircularBitmapIntoImageView: $e")
89+
imageView.setImageDrawable(placeholder)
5590
}
5691
}
5792

58-
private fun isSVG(url: String): Boolean = (url.toUri().encodedPath?.endsWith(".svg") == true)
93+
@SuppressLint("CheckResult")
94+
fun loadIntoImageView(
95+
context: Context,
96+
client: NextcloudClient?,
97+
url: String?,
98+
imageView: ImageView,
99+
@DrawableRes placeholder: Int,
100+
circleCrop: Boolean = false
101+
) {
102+
try {
103+
createRequestBuilder<Drawable>(context, client, url)
104+
?.placeholder(placeholder)
105+
?.error(placeholder)
106+
?.apply { if (circleCrop) circleCrop() }
107+
?.withLogging("loadIntoImageView", url ?: "null")
108+
?.into(imageView) ?: imageView.setImageResource(placeholder)
109+
} catch (e: Exception) {
110+
Log_OC.e(TAG, "exception loadIntoImageView: $e")
111+
imageView.setImageResource(placeholder)
112+
}
113+
}
114+
115+
fun getDrawable(context: Context, client: NextcloudClient?, urlString: String?): Drawable? = try {
116+
createRequestBuilder<Drawable>(context, client, urlString)?.submit()?.get()
117+
} catch (e: Exception) {
118+
Log_OC.e(TAG, "exception getDrawable: $e")
119+
null
120+
}
121+
122+
fun <T> loadIntoTarget(
123+
activity: ComponentActivity,
124+
account: OwnCloudAccount?,
125+
url: String,
126+
target: Target<T>,
127+
@DrawableRes placeholder: Int
128+
) {
129+
if (account == null) {
130+
Log_OC.e(TAG, "loadIntoTargetWithActivity: account cannot be null")
131+
return
132+
}
133+
134+
activity.lifecycleScope.launch(Dispatchers.IO) {
135+
val clientFactory = OwnCloudClientManagerFactory.getDefaultSingleton()
136+
val client = clientFactory.getNextcloudClientFor(account, activity)
137+
withContext(Dispatchers.Main) {
138+
try {
139+
createRequestBuilder<T>(activity, client, url)
140+
?.placeholder(placeholder)
141+
?.error(placeholder)
142+
?.withLogging("loadIntoTarget", url)
143+
?.into(target)
144+
} catch (e: Exception) {
145+
Log_OC.e(TAG, "exception loadIntoTarget: $e")
146+
}
147+
}
148+
}
149+
}
59150

60-
private fun createGlideUrl(url: String, client: NextcloudClient) = GlideUrl(
151+
fun createGlideUrl(url: String, client: NextcloudClient) = GlideUrl(
61152
url,
62153
LazyHeaders.Builder()
63154
.addHeader("Authorization", client.credentials)
64155
.addHeader("User-Agent", "Mozilla/5.0 (Android) Nextcloud-android")
65156
.build()
66157
)
67158

159+
// region private methods
160+
private class GlideLogger<T>(private val methodName: String, private val identifier: String) : RequestListener<T> {
161+
162+
override fun onLoadFailed(
163+
e: GlideException?,
164+
model: Any?,
165+
target: Target<T>,
166+
isFirstResource: Boolean
167+
): Boolean {
168+
Log_OC.e(TAG, "$methodName: Load failed for $identifier")
169+
Log_OC.e(TAG, "$methodName: Error: ${e?.message}")
170+
e?.logRootCauses(TAG)
171+
return false
172+
}
173+
174+
override fun onResourceReady(
175+
resource: T & Any,
176+
model: Any?,
177+
target: Target<T?>?,
178+
dataSource: DataSource,
179+
isFirstResource: Boolean
180+
): Boolean {
181+
Log_OC.i(TAG, "$methodName: Successfully loaded $identifier from $dataSource")
182+
return false
183+
}
184+
}
185+
186+
private fun isSVG(url: String): Boolean = (url.toUri().encodedPath?.endsWith(".svg") == true)
187+
68188
private fun <T> RequestBuilder<T>.withLogging(methodName: String, identifier: String): RequestBuilder<T> =
69189
listener(GlideLogger(methodName, identifier))
70190

@@ -81,8 +201,10 @@ object GlideHelper {
81201
.`as`(PictureDrawable::class.java)
82202
.load(glideUrl)
83203
.apply {
84-
placeholder?.let { placeholder(it) }
85-
placeholder?.let { error(it) }
204+
placeholder?.let {
205+
placeholder(it)
206+
error(it)
207+
}
86208
}
87209
.listener(SvgSoftwareLayerSetter())
88210
}
@@ -94,47 +216,11 @@ object GlideHelper {
94216
): RequestBuilder<Drawable> {
95217
val glideUrl = createGlideUrl(url, client)
96218
return Glide.with(context)
219+
.asDrawable()
97220
.load(glideUrl)
98221
.centerCrop()
99222
}
100223

101-
@Suppress("TooGenericExceptionCaught")
102-
fun getBitmap(context: Context, url: String?): Bitmap? {
103-
val validatedUrl = validateAndGetURL(url) ?: return null
104-
105-
return try {
106-
Glide.with(context)
107-
.asBitmap()
108-
.load(validatedUrl)
109-
.diskCacheStrategy(DiskCacheStrategy.NONE)
110-
.skipMemoryCache(true)
111-
.withLogging("downloadImageSynchronous", validatedUrl)
112-
.submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
113-
.get()
114-
} catch (e: Exception) {
115-
Log_OC.e(TAG, "Could not download image $e")
116-
null
117-
}
118-
}
119-
120-
fun loadCircularBitmapIntoImageView(context: Context, url: String?, imageView: ImageView, placeholder: Drawable) {
121-
val validatedUrl = validateAndGetURL(url) ?: return
122-
123-
Glide.with(context)
124-
.asBitmap()
125-
.load(validatedUrl)
126-
.placeholder(placeholder)
127-
.error(placeholder)
128-
.withLogging("loadCircularBitmapIntoImageView", validatedUrl)
129-
.into(object : BitmapImageViewTarget(imageView) {
130-
override fun setResource(resource: Bitmap?) {
131-
val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(context.resources, resource)
132-
circularBitmapDrawable.isCircular = true
133-
imageView.setImageDrawable(circularBitmapDrawable)
134-
}
135-
})
136-
}
137-
138224
@Suppress("UNCHECKED_CAST", "TooGenericExceptionCaught", "ReturnCount")
139225
private fun <T> createRequestBuilder(context: Context, client: NextcloudClient?, url: String?): RequestBuilder<T>? {
140226
if (client == null) {
@@ -147,47 +233,15 @@ object GlideHelper {
147233
return try {
148234
val isSVG = isSVG(validatedUrl)
149235

150-
return if (isSVG) {
236+
if (isSVG) {
151237
createSvgRequestBuilder(context, validatedUrl, client)
152238
} else {
153239
createUrlRequestBuilder(context, client, validatedUrl)
154-
}
155-
.withLogging("createRequestBuilder", validatedUrl) as RequestBuilder<T>?
240+
}.withLogging("createRequestBuilder", validatedUrl) as RequestBuilder<T>?
156241
} catch (e: Exception) {
157-
Log_OC.e(TAG, "Error createRequestBuilder: $e")
242+
Log_OC.e(TAG, "exception createRequestBuilder: $e")
158243
null
159244
}
160245
}
161-
162-
@SuppressLint("CheckResult")
163-
fun loadIntoImageView(
164-
context: Context,
165-
client: NextcloudClient?,
166-
url: String?,
167-
imageView: ImageView,
168-
@DrawableRes placeholder: Int,
169-
circleCrop: Boolean = false
170-
) {
171-
createRequestBuilder<Drawable>(context, client, url)
172-
?.placeholder(placeholder)
173-
?.error(placeholder)
174-
?.apply { if (circleCrop) circleCrop() }
175-
?.into(imageView)
176-
}
177-
178-
fun getDrawable(context: Context, client: NextcloudClient?, urlString: String?): Drawable? =
179-
createRequestBuilder<Drawable>(context, client, urlString)?.submit()?.get()
180-
181-
fun <T> loadIntoTarget(
182-
context: Context,
183-
client: NextcloudClient?,
184-
url: String,
185-
target: Target<T>,
186-
@DrawableRes placeholder: Int
187-
) {
188-
createRequestBuilder<T>(context, client, url)
189-
?.placeholder(placeholder)
190-
?.error(placeholder)
191-
?.into(target)
192-
}
246+
// endregion
193247
}

app/src/main/java/com/nextcloud/utils/extensions/SearchResultEntryExtensions.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ import com.owncloud.android.lib.common.SearchResultEntry
1313
fun SearchResultEntry.getType(): SearchResultEntryType {
1414
val value = icon.lowercase()
1515

16+
fun isAvatarUrl(url: String): Boolean {
17+
val regex = Regex("""^https?://[^/]+/avatar/[^/]+/\d+$""")
18+
return regex.matches(url)
19+
}
20+
1621
return when {
1722
value.contains("icon-folder") -> SearchResultEntryType.Folder
1823
value.contains("icon-note") -> SearchResultEntryType.Note
@@ -33,6 +38,7 @@ fun SearchResultEntry.getType(): SearchResultEntryType {
3338
value.contains("text-code") -> SearchResultEntryType.TextCode
3439
value.contains("link") -> SearchResultEntryType.Link
3540
value.contains("font") -> SearchResultEntryType.Font
41+
isAvatarUrl(thumbnailUrl) -> SearchResultEntryType.Avatar
3642
else -> SearchResultEntryType.Unknown
3743
}
3844
}

0 commit comments

Comments
 (0)