From 201d4924a714efc851c770a446193b9cd5eddb29 Mon Sep 17 00:00:00 2001 From: Danny Baumann Date: Tue, 30 Jul 2024 10:52:15 +0200 Subject: [PATCH] Fix issues with WidgetImageView skeleton placeholder The 'automagic' skeleton addition caused two issues: - Misplaced icon in section switches (#3773) This was caused by constraints in the widget's ConstraintLayout referencing the WidgetImageView's ID, which was no longer present after replacing it by the SkeletonLayout - Skeleton and image shown at the same time (mentioned in #3786) This was caused by WidgetAdapter and SkeletonLayout both simultaneously modifying the visibility flag of the WidgetImageView, again caused by silent replacement of WidgetImageView by SkeletonLayout Fix both issues by changing the approach: Instead of silently replacing the view, make WidgetImageView inherit from SkeletonLayout and make it redirect external calls to an internal ImageView instance. Fixes #3773, #3786 Signed-off-by: Danny Baumann --- .../org/openhab/habdroid/ui/WidgetAdapter.kt | 9 +- .../habdroid/ui/widget/WidgetImageView.kt | 648 +++++++++--------- mobile/src/main/res/layout/activity_chart.xml | 16 +- 3 files changed, 351 insertions(+), 322 deletions(-) diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt index 1b179ecbc9..d71beec566 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt @@ -1037,11 +1037,10 @@ class WidgetAdapter( // Make sure images fit into the content frame by scaling // them at max 90% of the available height - if (initData.parent.height > 0) { - imageView.maxHeight = (0.9f * initData.parent.height).roundToInt() - } else { - imageView.maxHeight = Integer.MAX_VALUE - } + imageView.setMaxHeight(when { + initData.parent.height > 0 -> (0.9f * initData.parent.height).roundToInt() + else -> Integer.MAX_VALUE + }) imageView.setImageScalingType(prefs.getImageWidgetScalingType()) if (value != null && value.matches("data:image/.*;base64,.*".toRegex())) { diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/widget/WidgetImageView.kt b/mobile/src/main/java/org/openhab/habdroid/ui/widget/WidgetImageView.kt index 09bc702246..1286d3be1a 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/widget/WidgetImageView.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/widget/WidgetImageView.kt @@ -13,6 +13,7 @@ package org.openhab.habdroid.ui.widget +import android.annotation.SuppressLint import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory @@ -22,10 +23,8 @@ import android.util.AttributeSet import android.util.Base64 import android.util.Log import androidx.appcompat.widget.AppCompatImageView -import com.faltenreich.skeletonlayout.Skeleton import com.faltenreich.skeletonlayout.SkeletonConfig import com.faltenreich.skeletonlayout.SkeletonLayout -import com.faltenreich.skeletonlayout.createSkeleton import kotlin.random.Random import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -36,6 +35,7 @@ import kotlinx.coroutines.launch import okhttp3.HttpUrl import org.openhab.habdroid.R import org.openhab.habdroid.core.connection.Connection +import org.openhab.habdroid.ui.widget.WidgetImageView.ImageScalingType import org.openhab.habdroid.util.CacheManager import org.openhab.habdroid.util.HttpClient import org.openhab.habdroid.util.IconBackground @@ -45,377 +45,389 @@ import org.openhab.habdroid.util.getPrefs import org.openhab.habdroid.util.isDebugModeEnabled import org.openhab.habdroid.util.resolveThemedColor -class WidgetImageView(context: Context, attrs: AttributeSet?) : AppCompatImageView(context, attrs) { - private var scope: CoroutineScope? = null - private val fallback: Drawable? - - private var skeleton: Skeleton? = null - private var originalScaleType: ScaleType? = null - private var originalAdjustViewBounds: Boolean = false - private val emptyHeightToWidthRatio: Float - private val addRandomnessToUrl: Boolean - private var imageScalingType = ImageScalingType.NoScaling - private var internalLoad: Boolean = false - private var lastRequest: HttpImageRequest? = null - - private var refreshInterval: Long = 0 - private var lastRefreshTimestamp: Long = 0 - private var refreshJob: Job? = null - private var refreshActive = false - private var pendingRequest: PendingRequest? = null - private var pendingLoadJob: Job? = null - private var targetImageSize: Int = 0 +class WidgetImageView(context: Context, attrs: AttributeSet?, private val imageView: InternalImageView) : + SkeletonLayout(context, attrs, config = createConfig(context)), + WidgetImageViewIntf by imageView +{ + constructor(context: Context, attrs: AttributeSet?) : this( + context, + attrs, + InternalImageView(context, attrs) + ) init { - context.obtainStyledAttributes(attrs, R.styleable.WidgetImageView).apply { - fallback = getDrawable(R.styleable.WidgetImageView_fallback) - emptyHeightToWidthRatio = getFraction(R.styleable.WidgetImageView_emptyHeightToWidthRatio, 1, 1, 0f) - addRandomnessToUrl = getBoolean(R.styleable.WidgetImageView_addRandomnessToUrl, false) - val imageScalingType = getInt(R.styleable.WidgetImageView_imageScalingType, 0) - if (imageScalingType < ImageScalingType.entries.size) { - setImageScalingType(ImageScalingType.entries[imageScalingType]) - } - recycle() - } - // In some cases it's required to add the skeleton manually to XML. - // In these cases, set the parent as skeleton here, otherwise create it in applyProgressDrawable() - val parent = parent - if (parent is SkeletonLayout) { - skeleton = parent - } + addView(imageView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)) + imageView.loadProgressCallback = { loading -> when (loading) { + true -> showSkeleton() + false -> showOriginal() + } } } - fun setImageUrl( - connection: Connection, - url: String, - refreshDelayInMs: Int = 0, - timeoutMillis: Long = HttpClient.DEFAULT_TIMEOUT_MS, - forceLoad: Boolean = false - ) { - val client = connection.httpClient - val actualUrl = client.buildUrl(url) - - pendingLoadJob?.cancel() - refreshInterval = refreshDelayInMs.toLong() - - if (actualUrl == lastRequest?.url) { - if (lastRequest?.isActive() == true) { - // We're already in the process of loading this image, thus there's nothing to do - return + @SuppressLint("CustomViewStyleable") + class InternalImageView( + context: Context, + attrs: AttributeSet? + ) : AppCompatImageView(context, attrs), WidgetImageViewIntf { + private var scope: CoroutineScope? = null + var loadProgressCallback: (loading: Boolean) -> Unit = {} + private val fallback: Drawable? + + private var originalScaleType: ScaleType? = null + private var originalAdjustViewBounds: Boolean = false + private val emptyHeightToWidthRatio: Float + private val addRandomnessToUrl: Boolean + private var imageScalingType = ImageScalingType.NoScaling + private var internalLoad: Boolean = false + private var lastRequest: HttpImageRequest? = null + + private var refreshInterval: Long = 0 + private var lastRefreshTimestamp: Long = 0 + private var refreshJob: Job? = null + private var refreshActive = false + private var pendingRequest: PendingRequest? = null + private var pendingLoadJob: Job? = null + private var targetImageSize: Int = 0 + + init { + context.obtainStyledAttributes(attrs, R.styleable.WidgetImageView).apply { + fallback = getDrawable(R.styleable.WidgetImageView_fallback) + emptyHeightToWidthRatio = getFraction( + R.styleable.WidgetImageView_emptyHeightToWidthRatio, + 1, + 1, + 0f + ) + addRandomnessToUrl = getBoolean(R.styleable.WidgetImageView_addRandomnessToUrl, false) + val imageScalingType = getInt(R.styleable.WidgetImageView_imageScalingType, 0) + if (imageScalingType < ImageScalingType.entries.size) { + setImageScalingType(ImageScalingType.entries[imageScalingType]) + } + recycle() } - } else if (pendingRequest == null) { - lastRefreshTimestamp = 0 } - if (targetImageSize == 0) { - pendingRequest = PendingHttpRequest(client, actualUrl, timeoutMillis, forceLoad) - } else { - doLoad(client, actualUrl, timeoutMillis, forceLoad) + override fun setImageUrl( + connection: Connection, + url: String, + refreshDelayInMs: Int, + timeoutMillis: Long, + forceLoad: Boolean + ) { + val client = connection.httpClient + val actualUrl = client.buildUrl(url) + + pendingLoadJob?.cancel() + refreshInterval = refreshDelayInMs.toLong() + + if (actualUrl == lastRequest?.url) { + if (lastRequest?.isActive() == true) { + // We're already in the process of loading this image, thus there's nothing to do + return + } + } else if (pendingRequest == null) { + lastRefreshTimestamp = 0 + } + + if (targetImageSize == 0) { + pendingRequest = PendingHttpRequest(client, actualUrl, timeoutMillis, forceLoad) + } else { + doLoad(client, actualUrl, timeoutMillis, forceLoad) + } } - } - fun setBase64EncodedImage(base64: String) { - prepareForNonHttpImage() - val data = Base64.decode(base64, Base64.DEFAULT) - val bitmap: Bitmap? = BitmapFactory.decodeByteArray(data, 0, data.size) + override fun setBase64EncodedImage(base64: String) { + prepareForNonHttpImage() + val data = Base64.decode(base64, Base64.DEFAULT) + val bitmap: Bitmap? = BitmapFactory.decodeByteArray(data, 0, data.size) - if (bitmap == null) { - applyFallbackDrawable() - return - } + if (bitmap == null) { + applyFallbackDrawable() + return + } - if (targetImageSize == 0) { - pendingRequest = PendingBase64Request(bitmap) - } else { - applyLoadedBitmap(bitmap) + if (targetImageSize == 0) { + pendingRequest = PendingBase64Request(bitmap) + } else { + applyLoadedBitmap(bitmap) + } } - } - override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { - super.onLayout(changed, left, top, right, bottom) - targetImageSize = right - left - paddingLeft - paddingRight - pendingRequest?.let { r -> - when (r) { - is PendingHttpRequest -> { - pendingLoadJob = scope?.launch { - doLoad(r.client, r.url, r.timeoutMillis, r.forceLoad) + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + targetImageSize = right - left - paddingLeft - paddingRight + pendingRequest?.let { r -> + when (r) { + is PendingHttpRequest -> { + pendingLoadJob = scope?.launch { + doLoad(r.client, r.url, r.timeoutMillis, r.forceLoad) + } } - } - is PendingBase64Request -> { - pendingLoadJob = scope?.launch { - applyLoadedBitmap(r.bitmap) + is PendingBase64Request -> { + pendingLoadJob = scope?.launch { + applyLoadedBitmap(r.bitmap) + } } } } + pendingRequest = null } - pendingRequest = null - } - override fun setImageResource(resId: Int) { - prepareForNonHttpImage() - super.setImageResource(resId) - } - - override fun setImageDrawable(drawable: Drawable?) { - if (!internalLoad) { + override fun setImageResource(resId: Int) { prepareForNonHttpImage() + super.setImageResource(resId) } - super.setImageDrawable(drawable) - } - override fun setImageBitmap(bm: Bitmap?) { - prepareForNonHttpImage() - super.setImageBitmap(bm) - } + override fun setImageDrawable(d: Drawable?) { + if (!internalLoad) { + prepareForNonHttpImage() + } + super.setImageDrawable(d) + } - override fun setAdjustViewBounds(adjustViewBounds: Boolean) { - super.setAdjustViewBounds(adjustViewBounds) - originalAdjustViewBounds = adjustViewBounds - } + override fun setImageBitmap(bitmap: Bitmap?) { + prepareForNonHttpImage() + super.setImageBitmap(bitmap) + } - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - val d = drawable - if (d == null && emptyHeightToWidthRatio > 0) { - val specWidth = MeasureSpec.getSize(widthMeasureSpec) - val specMode = MeasureSpec.getMode(widthMeasureSpec) - if (specMode == MeasureSpec.AT_MOST || specMode == MeasureSpec.EXACTLY) { - setMeasuredDimension(specWidth, (emptyHeightToWidthRatio * specWidth).toInt()) - } + override fun setAdjustViewBounds(adjustViewBounds: Boolean) { + super.setAdjustViewBounds(adjustViewBounds) + originalAdjustViewBounds = adjustViewBounds } - } - override fun onAttachedToWindow() { - super.onAttachedToWindow() - scope = CoroutineScope(Dispatchers.Main + Job()) - lastRequest?.let { request -> - if (!request.hasCompleted()) { - // Make sure to have an up-to-date image if refresh is enabled by avoiding cache in that case - // (when not doing so, we'd always load a stale image from cache until first refresh) - request.execute(refreshInterval != 0L) - } else { - scheduleNextRefreshIfNeeded() + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val d = drawable + if (d == null && emptyHeightToWidthRatio > 0) { + val specWidth = MeasureSpec.getSize(widthMeasureSpec) + val specMode = MeasureSpec.getMode(widthMeasureSpec) + if (specMode == MeasureSpec.AT_MOST || specMode == MeasureSpec.EXACTLY) { + setMeasuredDimension(specWidth, (emptyHeightToWidthRatio * specWidth).toInt()) + } } } - } - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - scope?.cancel() - scope = null - } + override fun onAttachedToWindow() { + super.onAttachedToWindow() + scope = CoroutineScope(Dispatchers.Main + Job()) + lastRequest?.let { request -> + if (!request.hasCompleted()) { + // Make sure to have an up-to-date image if refresh is enabled by avoiding cache in that case + // (when not doing so, we'd always load a stale image from cache until first refresh) + request.execute(refreshInterval != 0L) + } else { + scheduleNextRefreshIfNeeded() + } + } + } - fun setImageScalingType(type: ImageScalingType) { - if (imageScalingType == type) { - return + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + scope?.cancel() + scope = null } - imageScalingType = type - when (type) { - ImageScalingType.NoScaling -> { - adjustViewBounds = false - scaleType = ScaleType.CENTER_INSIDE - } - ImageScalingType.ScaleToFit -> { - adjustViewBounds = false - scaleType = ScaleType.FIT_CENTER + + override fun setImageScalingType(type: ImageScalingType) { + if (imageScalingType == type) { + return } - ImageScalingType.ScaleToFitWithViewAdjustment, - ImageScalingType.ScaleToFitWithViewAdjustmentDownscaleOnly -> { - adjustViewBounds = true - scaleType = ScaleType.FIT_CENTER + imageScalingType = type + when (type) { + ImageScalingType.NoScaling -> { + adjustViewBounds = false + scaleType = ScaleType.CENTER_INSIDE + } + ImageScalingType.ScaleToFit -> { + adjustViewBounds = false + scaleType = ScaleType.FIT_CENTER + } + ImageScalingType.ScaleToFitWithViewAdjustment, + ImageScalingType.ScaleToFitWithViewAdjustmentDownscaleOnly -> { + adjustViewBounds = true + scaleType = ScaleType.FIT_CENTER + } } } - } - fun startRefreshingIfNeeded() { - refreshJob?.cancel() - refreshJob = null - refreshActive = true - if (lastRequest?.isActive() != true) { - scheduleNextRefreshIfNeeded() + override fun startRefreshingIfNeeded() { + refreshJob?.cancel() + refreshJob = null + refreshActive = true + if (lastRequest?.isActive() != true) { + scheduleNextRefreshIfNeeded() + } } - } - - fun cancelRefresh() { - refreshJob?.cancel() - refreshJob = null - lastRefreshTimestamp = 0 - refreshActive = false - } - - private fun prepareForNonHttpImage() { - cancelCurrentLoad() - cancelRefresh() - lastRequest = null - refreshInterval = 0 - removeSkeleton() - } - - private fun doLoad(client: HttpClient, url: HttpUrl, timeoutMillis: Long, forceLoad: Boolean) { - cancelCurrentLoad() - val cached = CacheManager.getInstance(context).getCachedBitmap( - url, - context.getIconFallbackColor(IconBackground.APP_THEME) - ) - val request = HttpImageRequest(client, url, targetImageSize, timeoutMillis) - - if (cached != null) { - applyLoadedBitmap(cached) - } else if (lastRequest?.statelessUrlEquals(url) != true) { - applySkeleton() + override fun cancelRefresh() { + refreshJob?.cancel() + refreshJob = null + lastRefreshTimestamp = 0 + refreshActive = false } - if (cached == null || forceLoad) { - request.execute(forceLoad) - } else { - scheduleNextRefreshIfNeeded() + private fun prepareForNonHttpImage() { + cancelCurrentLoad() + cancelRefresh() + lastRequest = null + refreshInterval = 0 + loadProgressCallback.invoke(false) } - lastRequest = request - } - private fun scheduleNextRefreshIfNeeded() { - if (refreshInterval == 0L || !refreshActive) { - return - } - val timeToNextRefresh = refreshInterval + lastRefreshTimestamp - SystemClock.uptimeMillis() - Log.d(TAG, "Scheduling next refresh for ${lastRequest?.url} in $timeToNextRefresh ms") - refreshJob = scope?.launch { - delay(timeToNextRefresh) - lastRequest?.execute(true) - } - } + private fun doLoad(client: HttpClient, url: HttpUrl, timeoutMillis: Long, forceLoad: Boolean) { + cancelCurrentLoad() - private fun cancelCurrentLoad() { - refreshJob?.cancel() - refreshJob = null - lastRequest?.cancel() - pendingLoadJob?.cancel() - pendingLoadJob = null - } + val cached = CacheManager.getInstance(context).getCachedBitmap( + url, + context.getIconFallbackColor(IconBackground.APP_THEME) + ) + val request = HttpImageRequest(client, url, targetImageSize, timeoutMillis) - private fun applyLoadedBitmap(bitmap: Bitmap) { - removeSkeleton() - if (imageScalingType == ImageScalingType.ScaleToFitWithViewAdjustmentDownscaleOnly) { - // Make sure that view only shrinks to accommodate bitmap size, but doesn't enlarge ... that is, - // adjust view bounds only if width is larger than target size or height is larger than the maximum height - adjustViewBounds = bitmap.width > targetImageSize || maxHeight < bitmap.height - } - // Mark this call as being triggered by ourselves, as setImageBitmap() - // ultimately calls through to setImageDrawable(). - internalLoad = true - super.setImageBitmap(bitmap) - internalLoad = false - } + if (cached != null) { + applyLoadedBitmap(cached) + } else if (lastRequest?.statelessUrlEquals(url) != true) { + loadProgressCallback.invoke(true) + } - fun applyFallbackDrawable() { - if (originalScaleType == null) { - originalScaleType = scaleType - super.setScaleType(ScaleType.CENTER) - super.setAdjustViewBounds(false) + if (cached == null || forceLoad) { + request.execute(forceLoad) + } else { + scheduleNextRefreshIfNeeded() + } + lastRequest = request } - super.setImageDrawable(fallback) - } - private fun applySkeleton() { - if (skeleton == null) { - val config = SkeletonConfig.default(context) - config.maskColor = context.resolveThemedColor(R.attr.skeletonBackground, config.maskColor) - config.shimmerColor = context.resolveThemedColor(R.attr.skeletonShimmer, config.shimmerColor) - skeleton = createSkeleton(config) + private fun scheduleNextRefreshIfNeeded() { + if (refreshInterval == 0L || !refreshActive) { + return + } + val timeToNextRefresh = refreshInterval + lastRefreshTimestamp - SystemClock.uptimeMillis() + Log.d(TAG, "Scheduling next refresh for ${lastRequest?.url} in $timeToNextRefresh ms") + refreshJob = scope?.launch { + delay(timeToNextRefresh) + lastRequest?.execute(true) + } } - skeleton?.showSkeleton() - } - private fun removeSkeleton() { - skeleton?.showOriginal() - } + private fun cancelCurrentLoad() { + refreshJob?.cancel() + refreshJob = null + lastRequest?.cancel() + pendingLoadJob?.cancel() + pendingLoadJob = null + } - private inner class HttpImageRequest( - private val client: HttpClient, - val url: HttpUrl, - private val size: Int, - private val timeoutMillis: Long - ) { - private var job: Job? = null - private var lastRandomness = Random.Default.nextInt() - - fun execute(avoidCache: Boolean) { - if (job?.isActive == true) { - // Nothing to do, we're still in the process of downloading - return + private fun applyLoadedBitmap(bitmap: Bitmap) { + loadProgressCallback.invoke(false) + if (imageScalingType == ImageScalingType.ScaleToFitWithViewAdjustmentDownscaleOnly) { + // Make sure that view only shrinks to accommodate bitmap size, but doesn't enlarge ... that is, + // adjust view bounds only if width is larger than target size or height is larger than the maximum height + adjustViewBounds = bitmap.width > targetImageSize || maxHeight < bitmap.height } + // Mark this call as being triggered by ourselves, as setImageBitmap() + // ultimately calls through to setImageDrawable(). + internalLoad = true + super.setImageBitmap(bitmap) + internalLoad = false + } - Log.i(TAG, "Refreshing image at $url, avoidCache $avoidCache") - val cachingMode = if (avoidCache) { - HttpClient.CachingMode.AVOID_CACHE - } else { - HttpClient.CachingMode.FORCE_CACHE_IF_POSSIBLE + override fun applyFallbackDrawable() { + if (originalScaleType == null) { + originalScaleType = scaleType + super.setScaleType(ScaleType.CENTER) + super.setAdjustViewBounds(false) } + super.setImageDrawable(fallback) + } - val actualUrl = if (addRandomnessToUrl) { - if (avoidCache) { - lastRandomness = Random.Default.nextInt() + private inner class HttpImageRequest( + private val client: HttpClient, + val url: HttpUrl, + private val size: Int, + private val timeoutMillis: Long + ) { + private var job: Job? = null + private var lastRandomness = Random.Default.nextInt() + + fun execute(avoidCache: Boolean) { + if (job?.isActive == true) { + // Nothing to do, we're still in the process of downloading + return + } + + Log.i(TAG, "Refreshing image at $url, avoidCache $avoidCache") + val cachingMode = if (avoidCache) { + HttpClient.CachingMode.AVOID_CACHE + } else { + HttpClient.CachingMode.FORCE_CACHE_IF_POSSIBLE } - url.newBuilder().setQueryParameter("random", lastRandomness.toString()).build() - } else { - url - } - job = scope?.launch(Dispatchers.Main) { - try { - val conversionPolicy = when (originalScaleType ?: scaleType) { - ScaleType.FIT_CENTER, ScaleType.FIT_START, - ScaleType.FIT_END, ScaleType.FIT_XY -> ImageConversionPolicy.PreferTargetSize - else -> ImageConversionPolicy.PreferSourceSize + val actualUrl = if (addRandomnessToUrl) { + if (avoidCache) { + lastRandomness = Random.Default.nextInt() } - val fallbackColor = context.getIconFallbackColor(IconBackground.APP_THEME) - val bitmap = client - .get(actualUrl.toString(), timeoutMillis = timeoutMillis, caching = cachingMode) - .asBitmap(size, fallbackColor, conversionPolicy) - .response - CacheManager.getInstance(context).cacheBitmap(url, bitmap, fallbackColor) - applyLoadedBitmap(bitmap) - lastRefreshTimestamp = SystemClock.uptimeMillis() - scheduleNextRefreshIfNeeded() - } catch (e: HttpClient.HttpException) { - if (context.getPrefs().isDebugModeEnabled()) { - Log.d(TAG, "Failed to load image '$url', HTTP code ${e.statusCode}", e) + url.newBuilder().setQueryParameter("random", lastRandomness.toString()).build() + } else { + url + } + + job = scope?.launch(Dispatchers.Main) { + try { + val conversionPolicy = when (originalScaleType ?: scaleType) { + ScaleType.FIT_CENTER, ScaleType.FIT_START, + ScaleType.FIT_END, ScaleType.FIT_XY -> ImageConversionPolicy.PreferTargetSize + else -> ImageConversionPolicy.PreferSourceSize + } + val fallbackColor = context.getIconFallbackColor(IconBackground.APP_THEME) + val bitmap = client + .get(actualUrl.toString(), timeoutMillis = timeoutMillis, caching = cachingMode) + .asBitmap(size, fallbackColor, conversionPolicy) + .response + CacheManager.getInstance(context).cacheBitmap(url, bitmap, fallbackColor) + applyLoadedBitmap(bitmap) + lastRefreshTimestamp = SystemClock.uptimeMillis() + scheduleNextRefreshIfNeeded() + } catch (e: HttpClient.HttpException) { + if (context.getPrefs().isDebugModeEnabled()) { + Log.d(TAG, "Failed to load image '$url', HTTP code ${e.statusCode}", e) + } + loadProgressCallback.invoke(false) + applyFallbackDrawable() } - removeSkeleton() - applyFallbackDrawable() } } - } - fun cancel() { - job?.cancel() - } + fun cancel() { + job?.cancel() + } - fun hasCompleted(): Boolean { - return job?.isCompleted == true - } + fun hasCompleted(): Boolean { + return job?.isCompleted == true + } - fun isActive(): Boolean { - return job?.isActive == true - } + fun isActive(): Boolean { + return job?.isActive == true + } - fun statelessUrlEquals(url: HttpUrl): Boolean { - return this.url.newBuilder().removeAllQueryParameters("state").build() == - url.newBuilder().removeAllQueryParameters("state").build() + fun statelessUrlEquals(url: HttpUrl): Boolean { + return this.url.newBuilder().removeAllQueryParameters("state").build() == + url.newBuilder().removeAllQueryParameters("state").build() + } + + override fun toString(): String { + return "HttpImageRequest(url=$url, job=$job)" + } } - } - abstract class PendingRequest + abstract class PendingRequest - data class PendingHttpRequest( - val client: HttpClient, - val url: HttpUrl, - val timeoutMillis: Long, - val forceLoad: Boolean - ) : PendingRequest() + data class PendingHttpRequest( + val client: HttpClient, + val url: HttpUrl, + val timeoutMillis: Long, + val forceLoad: Boolean + ) : PendingRequest() - data class PendingBase64Request(val bitmap: Bitmap) : PendingRequest() + data class PendingBase64Request(val bitmap: Bitmap) : PendingRequest() + } enum class ImageScalingType { NoScaling, @@ -426,5 +438,29 @@ class WidgetImageView(context: Context, attrs: AttributeSet?) : AppCompatImageVi companion object { private val TAG = WidgetImageView::class.java.simpleName + private fun createConfig(context: Context) = SkeletonConfig.default(context).apply { + maskColor = context.resolveThemedColor(R.attr.skeletonBackground, maskColor) + shimmerColor = context.resolveThemedColor(R.attr.skeletonShimmer, shimmerColor) + } } } + +internal interface WidgetImageViewIntf { + fun setImageUrl( + connection: Connection, + url: String, + refreshDelayInMs: Int = 0, + timeoutMillis: Long = HttpClient.DEFAULT_TIMEOUT_MS, + forceLoad: Boolean = false + ) + fun setBase64EncodedImage(base64: String) + fun setImageScalingType(type: ImageScalingType) + fun startRefreshingIfNeeded() + fun cancelRefresh() + fun applyFallbackDrawable() + fun setImageBitmap(bitmap: Bitmap?) + fun setImageDrawable(d: Drawable?) + fun setMaxHeight(maxHeight: Int) + fun setColorFilter(color: Int) + fun clearColorFilter() +} diff --git a/mobile/src/main/res/layout/activity_chart.xml b/mobile/src/main/res/layout/activity_chart.xml index c0bad332d1..c3593d8373 100644 --- a/mobile/src/main/res/layout/activity_chart.xml +++ b/mobile/src/main/res/layout/activity_chart.xml @@ -15,19 +15,13 @@ android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"> - - - + app:imageScalingType="scaleToFitWithViewAdjustment" + app:addRandomnessToUrl="true" + tools:src="@drawable/day_dream_preview" />