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 516e23c9cc..98dfa8b9c8 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt @@ -1042,11 +1042,12 @@ 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..2ff99bfdfb 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,390 @@ 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]) + addView(imageView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)) + imageView.loadProgressCallback = { loading -> + when (loading) { + true -> showSkeleton() + false -> showOriginal() } - 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 } } - 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 +439,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" />