From 3b3a575daeea34bca59bd13d8bd5ebd95692dc26 Mon Sep 17 00:00:00 2001 From: Amine Date: Mon, 13 Jan 2025 07:39:13 +0000 Subject: [PATCH] feat: basic auth and images loading. Fixes #172. (#175) ## Types of changes - [ ] I have read the **CONTRIBUTING** document. - [ ] My code follows the code style of this project. - [ ] I have updated the documentation accordingly. - [ ] I have added tests to cover my changes. - [ ] All new and existing tests passed. - [ ] This is **NOT** translation related. This closes issue #XXX This is implements feature #YYY This finishes chore #ZZZ Reviewed-on: https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/175 Co-authored-by: Amine Co-committed-by: Amine --- .../android/adapters/ItemCardAdapter.kt | 4 +- .../android/adapters/ItemListAdapter.kt | 4 +- .../android/adapters/SourcesListAdapter.kt | 4 +- .../android/background/LoadingWorker.kt | 2 +- .../android/fragments/ArticleFragment.kt | 111 +++++------------- .../android/fragments/FilterSheetFragment.kt | 35 +++--- .../android/fragments/ImageFragment.kt | 23 ++-- .../android/model/AndroidIModelUtils.kt | 21 ++-- .../android/utils/glide/GlideUtils.kt | 90 +++++++++++++- 9 files changed, 163 insertions(+), 131 deletions(-) diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemCardAdapter.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemCardAdapter.kt index 894af9e..bb3fab0 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemCardAdapter.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemCardAdapter.kt @@ -118,13 +118,13 @@ class ItemCardAdapter( binding.itemImage.setImageDrawable(null) } else { binding.itemImage.visibility = View.VISIBLE - c.bitmapCenterCrop(itm.getThumbnail(repository.baseUrl), binding.itemImage) + c.bitmapCenterCrop(itm.getThumbnail(repository.baseUrl), binding.itemImage, appSettingsService) } if (itm.getIcon(repository.baseUrl).isEmpty()) { binding.sourceImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded()) } else { - c.circularDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage) + c.circularDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage, appSettingsService) } } } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemListAdapter.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemListAdapter.kt index a8e0256..d096e34 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemListAdapter.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemListAdapter.kt @@ -65,10 +65,10 @@ class ItemListAdapter( if (itm.getIcon(repository.baseUrl).isEmpty()) { binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded()) } else { - c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage) + c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage, appSettingsService) } } else { - c.circularDrawable(itm.getThumbnail(repository.baseUrl), binding.itemImage) + c.circularDrawable(itm.getThumbnail(repository.baseUrl), binding.itemImage, appSettingsService) } } } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/SourcesListAdapter.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/SourcesListAdapter.kt index 1998cab..0aa6d1a 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/SourcesListAdapter.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/SourcesListAdapter.kt @@ -16,6 +16,7 @@ import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBindi import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.repository.Repository +import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded import bou.amine.apps.readerforselfossv2.utils.getIcon import kotlinx.coroutines.CoroutineScope @@ -36,6 +37,7 @@ class SourcesListAdapter( override val di: DI by closestDI(app) private val repository: Repository by instance() + private val appSettingsService: AppSettingsService by instance() override fun onCreateViewHolder( parent: ViewGroup, @@ -82,7 +84,7 @@ class SourcesListAdapter( if (itm.getIcon(repository.baseUrl).isEmpty()) { binding.itemImage.setBackgroundAndText(itm.title.getHtmlDecoded()) } else { - c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage) + c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage, appSettingsService) } if (!itm.error.isNullOrBlank()) { diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/background/LoadingWorker.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/background/LoadingWorker.kt index 0d73f83..54e0e49 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/background/LoadingWorker.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/background/LoadingWorker.kt @@ -63,7 +63,7 @@ class LoadingWorker( handleNewItemsNotification(apiItems, notificationManager) } } - apiItems.map { it.preloadImages(context) } + apiItems.map { it.preloadImages(context, appSettingsService) } } } return Result.success() diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ArticleFragment.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ArticleFragment.kt index 705e7f1..dba460a 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ArticleFragment.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ArticleFragment.kt @@ -32,7 +32,9 @@ import bou.amine.apps.readerforselfossv2.android.model.ParecelableItem import bou.amine.apps.readerforselfossv2.android.model.toModel import bou.amine.apps.readerforselfossv2.android.model.toParcelable import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName +import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapFitCenter import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream +import bou.amine.apps.readerforselfossv2.android.utils.glide.getGlideImageForResource import bou.amine.apps.readerforselfossv2.android.utils.isUrlValid import bou.amine.apps.readerforselfossv2.android.utils.openItemUrlInBrowserAsNewTask import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser @@ -46,9 +48,6 @@ import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded import bou.amine.apps.readerforselfossv2.utils.getImages import bou.amine.apps.readerforselfossv2.utils.getThumbnail import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString -import com.bumptech.glide.Glide -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.request.RequestOptions import com.github.rubensousa.floatingtoolbar.FloatingToolbar import com.google.android.material.floatingactionbutton.FloatingActionButton import kotlinx.coroutines.CoroutineScope @@ -65,6 +64,8 @@ import java.util.Locale import java.util.concurrent.ExecutionException private const val IMAGE_JPG = "image/jpg" +private const val IMAGE_PNG = "image/png" +private const val IMAGE_WEBP = "image/webp" private const val WHITE_COLOR_HEX = 0xFFFFFF @@ -208,12 +209,7 @@ class ArticleFragment : if (!contentImage.isEmptyOrNullOrNullString() && context != null) { binding.imageView.visibility = View.VISIBLE - Glide - .with(requireContext()) - .asBitmap() - .load(contentImage) - .apply(RequestOptions.fitCenterTransform()) - .into(binding.imageView) + requireContext().bitmapFitCenter(contentImage, binding.imageView, appSettingsService) } else { binding.imageView.visibility = View.GONE } @@ -327,13 +323,7 @@ class ArticleFragment : private fun handleLeadImage(leadImageUrl: String?) { if (!leadImageUrl.isNullOrEmpty() && context != null) { binding.imageView.visibility = View.VISIBLE - Glide - .with(requireContext()) - .asBitmap() - .load( - leadImageUrl, - ).apply(RequestOptions.fitCenterTransform()) - .into(binding.imageView) + requireContext().bitmapFitCenter(leadImageUrl, binding.imageView, appSettingsService) } else { binding.imageView.visibility = View.GONE } @@ -357,78 +347,37 @@ class ArticleFragment : false } - @Suppress("detekt:LongMethod", "detekt:SwallowedException") + @Suppress("detekt:SwallowedException", "detekt:ReturnCount") @Deprecated("Deprecated in Java") override fun shouldInterceptRequest( view: WebView, url: String, ): WebResourceResponse? { - val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL) - var glideResource: WebResourceResponse? = null - if (url.lowercase(Locale.US).contains(".jpg") || - url - .lowercase(Locale.US) - .contains(".jpeg") - ) { - try { - val image = - Glide - .with(view) - .asBitmap() - .apply(glideOptions) - .load(url) - .submit() - .get() - glideResource = - WebResourceResponse( - IMAGE_JPG, - "UTF-8", - getBitmapInputStream(image, Bitmap.CompressFormat.JPEG), - ) - } catch (e: ExecutionException) { - // Do nothing + val (mime: String?, compression: Bitmap.CompressFormat) = + if (url + .lowercase(Locale.US) + .contains(".jpg") || + url.lowercase(Locale.US).contains(".jpeg") + ) { + Pair(IMAGE_JPG, Bitmap.CompressFormat.JPEG) + } else if (url.lowercase(Locale.US).contains(".png")) { + Pair(IMAGE_PNG, Bitmap.CompressFormat.PNG) + } else if (url.lowercase(Locale.US).contains(".webp")) { + Pair(IMAGE_WEBP, Bitmap.CompressFormat.WEBP) + } else { + return super.shouldInterceptRequest(view, url) } - } else if (url.lowercase(Locale.US).contains(".png")) { - try { - val image = - Glide - .with(view) - .asBitmap() - .apply(glideOptions) - .load(url) - .submit() - .get() - glideResource = - WebResourceResponse( - IMAGE_JPG, - "UTF-8", - getBitmapInputStream(image, Bitmap.CompressFormat.PNG), - ) - } catch (e: ExecutionException) { - // Do nothing - } - } else if (url.lowercase(Locale.US).contains(".webp")) { - try { - val image = - Glide - .with(view) - .asBitmap() - .apply(glideOptions) - .load(url) - .submit() - .get() - glideResource = - WebResourceResponse( - IMAGE_JPG, - "UTF-8", - getBitmapInputStream(image, Bitmap.CompressFormat.WEBP), - ) - } catch (e: ExecutionException) { - // Do nothing - } - } - return glideResource ?: super.shouldInterceptRequest(view, url) + try { + val image = view.getGlideImageForResource(url, appSettingsService) + return WebResourceResponse( + mime, + "UTF-8", + getBitmapInputStream(image, compression), + ) + } catch (e: ExecutionException) { + return super.shouldInterceptRequest(view, url) + } } } } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/FilterSheetFragment.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/FilterSheetFragment.kt index bab0d24..067affb 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/FilterSheetFragment.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/FilterSheetFragment.kt @@ -16,11 +16,12 @@ import bou.amine.apps.readerforselfossv2.android.HomeActivity import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName +import bou.amine.apps.readerforselfossv2.android.utils.glide.imageIntoViewTarget import bou.amine.apps.readerforselfossv2.repository.Repository +import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.utils.getColorHexCode import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded import bou.amine.apps.readerforselfossv2.utils.getIcon -import com.bumptech.glide.Glide import com.bumptech.glide.request.target.ViewTarget import com.bumptech.glide.request.transition.Transition import com.google.android.material.bottomsheet.BottomSheetDialogFragment @@ -41,6 +42,7 @@ class FilterSheetFragment : private lateinit var binding: FilterFragmentBinding override val di: DI by closestDI() private val repository: Repository by instance() + private val appSettingsService: AppSettingsService by instance() private var selectedChip: Chip? = null @@ -84,23 +86,22 @@ class FilterSheetFragment : val c = Chip(context) c.ellipsize = TextUtils.TruncateAt.END - Glide - .with(context) - .load(source.getIcon(repository.baseUrl)) - .into( - object : ViewTarget(c) { - override fun onResourceReady( - resource: Drawable, - transition: Transition?, - ) { - try { - c.chipIcon = resource - } catch (e: Exception) { - e.sendSilentlyWithAcraWithName("sources > onResourceReady") - } + context.imageIntoViewTarget( + source.getIcon(repository.baseUrl), + object : ViewTarget(c) { + override fun onResourceReady( + resource: Drawable, + transition: Transition?, + ) { + try { + c.chipIcon = resource + } catch (e: Exception) { + e.sendSilentlyWithAcraWithName("sources > onResourceReady") } - }, - ) + } + }, + appSettingsService, + ) c.text = source.title.getHtmlDecoded() diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ImageFragment.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ImageFragment.kt index 3a4e023..9300c3f 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ImageFragment.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ImageFragment.kt @@ -6,13 +6,19 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import bou.amine.apps.readerforselfossv2.android.databinding.FragmentImageBinding -import com.bumptech.glide.Glide -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.request.RequestOptions +import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapWithCache +import bou.amine.apps.readerforselfossv2.service.AppSettingsService +import org.kodein.di.DI +import org.kodein.di.DIAware +import org.kodein.di.android.x.closestDI +import org.kodein.di.instance -class ImageFragment : Fragment() { +class ImageFragment : + Fragment(), + DIAware { + override val di: DI by closestDI() + private val appSettingsService: AppSettingsService by instance() private lateinit var imageUrl: String - private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL) private var _binding: FragmentImageBinding? = null val binding get() = _binding @@ -31,12 +37,7 @@ class ImageFragment : Fragment() { val view = binding?.root binding!!.photoView.visibility = View.VISIBLE - Glide - .with(requireActivity()) - .asBitmap() - .apply(glideOptions) - .load(imageUrl) - .into(binding!!.photoView) + requireActivity().bitmapWithCache(imageUrl, binding!!.photoView, appSettingsService) return view } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/model/AndroidIModelUtils.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/model/AndroidIModelUtils.kt index 426f537..c5fd6a8 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/model/AndroidIModelUtils.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/model/AndroidIModelUtils.kt @@ -3,28 +3,21 @@ package bou.amine.apps.readerforselfossv2.android.model import android.content.Context import android.webkit.URLUtil import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName +import bou.amine.apps.readerforselfossv2.android.utils.glide.preloadImage import bou.amine.apps.readerforselfossv2.model.SelfossModel +import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.utils.getImages -import com.bumptech.glide.Glide -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.request.RequestOptions -private const val PRELOAD_IMAGE_TIMEOUT = 10000 - -fun SelfossModel.Item.preloadImages(context: Context): Boolean { +fun SelfossModel.Item.preloadImages( + context: Context, + appSettingsService: AppSettingsService, +): Boolean { val imageUrls = this.getImages() - val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(PRELOAD_IMAGE_TIMEOUT) - try { for (url in imageUrls) { if (URLUtil.isValidUrl(url)) { - Glide - .with(context) - .asBitmap() - .apply(glideOptions) - .load(url) - .submit() + context.preloadImage(url, appSettingsService) } } } catch (e: Error) { diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/glide/GlideUtils.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/glide/GlideUtils.kt index bdb56bf..10b4954 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/glide/GlideUtils.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/glide/GlideUtils.kt @@ -2,33 +2,119 @@ package bou.amine.apps.readerforselfossv2.android.utils.glide import android.content.Context import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.webkit.WebView import android.widget.ImageView import bou.amine.apps.readerforselfossv2.android.utils.CircleImageView +import bou.amine.apps.readerforselfossv2.service.AppSettingsService import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.model.GlideUrl +import com.bumptech.glide.load.model.LazyHeaders import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.request.target.ViewTarget +import com.google.android.material.chip.Chip import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.InputStream +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +private const val PRELOAD_IMAGE_TIMEOUT = 10000 + +@OptIn(ExperimentalEncodingApi::class) +fun String.toGlideUrl(appSettingsService: AppSettingsService): GlideUrl { + if (appSettingsService.getBasicUserName().isNotEmpty()) { + val authString = "${appSettingsService.getBasicUserName()}:${appSettingsService.getBasicPassword()}" + val authBuf = Base64.encode(authString.toByteArray(Charsets.UTF_8)) + + return GlideUrl( + this, + LazyHeaders + .Builder() + .addHeader("Authorization", "Basic $authBuf") + .build(), + ) + } else { + return GlideUrl( + this, + ) + } +} + +fun WebView.getGlideImageForResource( + url: String, + appSettingsService: AppSettingsService, +) = Glide + .with(this) + .asBitmap() + .apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)) + .load(url.toGlideUrl(appSettingsService)) + .submit() + .get() + +fun Context.preloadImage( + url: String, + appSettingsService: AppSettingsService, +) = Glide + .with(this) + .asBitmap() + .apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(PRELOAD_IMAGE_TIMEOUT)) + .load(url.toGlideUrl(appSettingsService)) + .submit() + +fun Context.imageIntoViewTarget( + url: String, + target: ViewTarget, + appSettingsService: AppSettingsService, +) = Glide + .with(this) + .load(url.toGlideUrl(appSettingsService)) + .into(target) + +fun Context.bitmapWithCache( + url: String, + iv: ImageView, + appSettingsService: AppSettingsService, +) = Glide + .with(this) + .asBitmap() + .apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)) + .load(url.toGlideUrl(appSettingsService)) + .into(iv) fun Context.bitmapCenterCrop( url: String, iv: ImageView, + appSettingsService: AppSettingsService, ) = Glide .with(this) .asBitmap() - .load(url) + .load(url.toGlideUrl(appSettingsService)) .apply(RequestOptions.centerCropTransform()) .into(iv) +fun Context.bitmapFitCenter( + url: String, + iv: ImageView, + appSettingsService: AppSettingsService, +) = Glide + .with(this) + .asBitmap() + .load(url.toGlideUrl(appSettingsService)) + .apply(RequestOptions.fitCenterTransform()) + .into(iv) + fun Context.circularDrawable( url: String, view: CircleImageView, + appSettingsService: AppSettingsService, ) { view.textView.text = "" Glide .with(this) - .load(url) + .load(url.toGlideUrl(appSettingsService)) .into(view.imageView) }