Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -77,7 +78,13 @@ 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ 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.client.network.ClientFactory
import com.nextcloud.utils.GlideHelper
import com.owncloud.android.R
import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
Expand All @@ -32,9 +31,9 @@ import javax.inject.Inject

class DashboardWidgetUpdater @Inject constructor(
private val context: Context,
private val clientFactory: ClientFactory,
private val accountProvider: CurrentAccountProvider
) {
private val scope = CoroutineScope(Dispatchers.IO)

fun updateAppWidget(
appWidgetManager: AppWidgetManager,
Expand Down Expand Up @@ -155,7 +154,7 @@ class DashboardWidgetUpdater @Inject constructor(

private fun loadIcon(appWidgetId: Int, iconUrl: String, remoteViews: RemoteViews) {
val target = AppWidgetTarget(context, R.id.icon, remoteViews, appWidgetId)
CoroutineScope(Dispatchers.IO).launch {
scope.launch {
val client = OwnCloudClientManagerFactory.getDefaultSingleton()
.getNextcloudClientFor(accountProvider.user.toOwnCloudAccount(), context)
val drawable = GlideHelper.getDrawable(context, client, iconUrl)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
228 changes: 141 additions & 87 deletions app/src/main/java/com/nextcloud/utils/GlideHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,43 +30,161 @@ 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.
*
* Provides methods for loading images into `ImageView`, `Target<Drawable>`, `Target<Bitmap>` ...
* from both URLs and URIs.
*/
@Suppress("TooManyFunctions")
@Suppress("TooManyFunctions", "TooGenericExceptionCaught")
object GlideHelper {
private const val TAG = "GlideHelper"

private class GlideLogger<T>(private val methodName: String, private val identifier: String) : RequestListener<T> {
override fun onLoadFailed(p0: GlideException?, p1: Any?, p2: Target<T>, p3: Boolean): Boolean {
Log_OC.e(TAG, "$methodName: Load failed for $identifier")
Log_OC.e(TAG, "$methodName: Error: ${p0?.message}")
p0?.logRootCauses(TAG)
return false
@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
}
}

override fun onResourceReady(p0: T & Any, p1: Any, p2: Target<T?>?, p3: DataSource, p4: Boolean): Boolean {
Log_OC.i(TAG, "Glide load completed: $p0")
return false
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)
}
}

private fun isSVG(url: String): Boolean = (url.toUri().encodedPath?.endsWith(".svg") == true)
@SuppressLint("CheckResult")
fun loadIntoImageView(
context: Context,
client: NextcloudClient?,
url: String?,
imageView: ImageView,
@DrawableRes placeholder: Int,
circleCrop: Boolean = false
) {
try {
createRequestBuilder<Drawable>(context, client, url)
?.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")
imageView.setImageResource(placeholder)
}
}

fun getDrawable(context: Context, client: NextcloudClient?, urlString: String?): Drawable? = try {
createRequestBuilder<Drawable>(context, client, urlString)?.submit()?.get()
} catch (e: Exception) {
Log_OC.e(TAG, "exception getDrawable: $e")
null
}

fun <T> loadIntoTarget(
activity: ComponentActivity,
account: OwnCloudAccount?,
url: String,
target: Target<T>,
@DrawableRes placeholder: Int
) {
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<T>(activity, client, url)
?.placeholder(placeholder)
?.error(placeholder)
?.withLogging("loadIntoTarget", url)
?.into(target)
} catch (e: Exception) {
Log_OC.e(TAG, "exception loadIntoTarget: $e")
}
}
}
}

private fun createGlideUrl(url: String, client: NextcloudClient) = GlideUrl(
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<T>(private val methodName: String, private val identifier: String) : RequestListener<T> {

override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<T>,
isFirstResource: Boolean
): Boolean {
Log_OC.e(TAG, "$methodName: Load failed for $identifier")
Log_OC.e(TAG, "$methodName: Error: ${e?.message}")
e?.logRootCauses(TAG)
return false
}

override fun onResourceReady(
resource: T & Any,
model: Any?,
target: Target<T?>?,
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 <T> RequestBuilder<T>.withLogging(methodName: String, identifier: String): RequestBuilder<T> =
listener(GlideLogger(methodName, identifier))

Expand All @@ -81,8 +201,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())
}
Expand All @@ -94,47 +216,11 @@ object GlideHelper {
): RequestBuilder<Drawable> {
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 <T> createRequestBuilder(context: Context, client: NextcloudClient?, url: String?): RequestBuilder<T>? {
if (client == null) {
Expand All @@ -147,47 +233,15 @@ 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<T>?
}.withLogging("createRequestBuilder", validatedUrl) as RequestBuilder<T>?
} 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<Drawable>(context, client, url)
?.placeholder(placeholder)
?.error(placeholder)
?.apply { if (circleCrop) circleCrop() }
?.into(imageView)
}

fun getDrawable(context: Context, client: NextcloudClient?, urlString: String?): Drawable? =
createRequestBuilder<Drawable>(context, client, urlString)?.submit()?.get()

fun <T> loadIntoTarget(
context: Context,
client: NextcloudClient?,
url: String,
target: Target<T>,
@DrawableRes placeholder: Int
) {
createRequestBuilder<T>(context, client, url)
?.placeholder(placeholder)
?.error(placeholder)
?.into(target)
}
// endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
}
Loading
Loading