diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryCache.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryCache.kt index e9c72f1bf..8a55871cb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryCache.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryCache.kt @@ -29,15 +29,17 @@ class StoryCache( val prefetchableAttachments: List = attachments .asSequence() .filter { it.uri != null && it.uri !in cache } - .filter { MediaUtil.isImage(it) } + .filter { MediaUtil.isImage(it) || it.blurHash != null } .filter { it.transferState == AttachmentDatabase.TRANSFER_PROGRESS_DONE } .toList() val newMappings: Map = prefetchableAttachments.associateWith { attachment -> - val imageTarget = glideRequests - .load(DecryptableStreamUriLoader.DecryptableUri(attachment.uri!!)) - .priority(Priority.HIGH) - .into(StoryCacheTarget(attachment.uri!!, storySize)) + val imageTarget = if (MediaUtil.isImage(attachment)) { + glideRequests + .load(DecryptableStreamUriLoader.DecryptableUri(attachment.uri!!)) + .priority(Priority.HIGH) + .into(StoryCacheTarget(attachment.uri!!, storySize)) + } else null val blurTarget = if (attachment.blurHash != null) { glideRequests @@ -79,7 +81,7 @@ class StoryCache( /** * Represents the load targets for an image and blur. */ - data class StoryCacheValue(val imageTarget: StoryCacheTarget, val blurTarget: StoryCacheTarget?) + data class StoryCacheValue(val imageTarget: StoryCacheTarget?, val blurTarget: StoryCacheTarget?) /** * A custom glide target for loading a drawable. Placeholder immediately clears, and we don't want to do that, so we use this instead. diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryBlurLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryBlurLoader.kt new file mode 100644 index 000000000..5c978c90e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryBlurLoader.kt @@ -0,0 +1,110 @@ +package org.thoughtcrime.securesms.stories.viewer.post + +import android.graphics.drawable.Drawable +import android.net.Uri +import android.widget.ImageView +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.blurhash.BlurHash +import org.thoughtcrime.securesms.mms.GlideApp +import org.thoughtcrime.securesms.stories.viewer.page.StoryCache +import org.thoughtcrime.securesms.stories.viewer.page.StoryDisplay + +/** + * Responsible for managing the lifecycle around loading a BlurHash + */ +class StoryBlurLoader( + private val lifecycle: Lifecycle, + private val blurHash: BlurHash?, + private val cacheKey: Uri, + private val storyCache: StoryCache, + private val storySize: StoryDisplay.Size, + private val blurImage: ImageView, + private val callback: Callback = NO_OP +) { + companion object { + private val TAG = Log.tag(StoryBlurLoader::class.java) + + private val NO_OP = object : Callback { + override fun onBlurLoaded() = Unit + override fun onBlurFailed() = Unit + } + } + + private val blurListener = object : StoryCache.Listener { + override fun onResourceReady(resource: Drawable) { + blurImage.setImageDrawable(resource) + callback.onBlurLoaded() + } + + override fun onLoadFailed() { + callback.onBlurFailed() + } + } + + fun load() { + val cacheValue = storyCache.getFromCache(cacheKey) + if (cacheValue != null) { + loadViaCache(cacheValue) + } else { + loadViaGlide(blurHash, storySize) + } + } + + fun clear() { + GlideApp.with(blurImage).clear(blurImage) + + blurImage.setImageDrawable(null) + } + + private fun loadViaCache(cacheValue: StoryCache.StoryCacheValue) { + Log.d(TAG, "Blur in cache. Loading via cache...") + + val blurTarget = cacheValue.blurTarget + if (blurTarget != null) { + blurTarget.addListener(blurListener) + lifecycle.addObserver(OnDestroy { blurTarget.removeListener(blurListener) }) + } else { + callback.onBlurFailed() + } + } + + private fun loadViaGlide(blurHash: BlurHash?, storySize: StoryDisplay.Size) { + if (blurHash != null) { + GlideApp.with(blurImage) + .load(blurHash) + .override(storySize.width, storySize.height) + .addListener(object : RequestListener { + override fun onLoadFailed(e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean): Boolean { + callback.onBlurFailed() + return false + } + + override fun onResourceReady(resource: Drawable?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean): Boolean { + callback.onBlurLoaded() + return false + } + }) + .into(blurImage) + } else { + callback.onBlurFailed() + } + } + + interface Callback { + fun onBlurLoaded() + fun onBlurFailed() + } + + private inner class OnDestroy(private val onDestroy: () -> Unit) : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + onDestroy() + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryImageLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryImageLoader.kt index cda477220..023ba1901 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryImageLoader.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryImageLoader.kt @@ -9,7 +9,6 @@ import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.Target import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.blurhash.BlurHash import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.stories.viewer.page.StoryCache @@ -24,9 +23,19 @@ class StoryImageLoader( private val storyCache: StoryCache, private val storySize: StoryDisplay.Size, private val postImage: ImageView, - private val blurImage: ImageView, + blurImage: ImageView, private val callback: StoryPostFragment.Callback -) { +) : StoryBlurLoader.Callback { + + private val blurLoader = StoryBlurLoader( + fragment.viewLifecycleOwner.lifecycle, + imagePost.blurHash, + imagePost.imageUri, + storyCache, + storySize, + blurImage, + this + ) companion object { private val TAG = Log.tag(StoryImageLoader::class.java) @@ -48,77 +57,35 @@ class StoryImageLoader( } } - private val blurListener = object : StoryCache.Listener { - override fun onResourceReady(resource: Drawable) { - blurImage.setImageDrawable(resource) - blurState = LoadState.READY - notifyListeners() - } - - override fun onLoadFailed() { - blurState = LoadState.FAILED - notifyListeners() - } - } - fun load() { val cacheValue = storyCache.getFromCache(imagePost.imageUri) if (cacheValue != null) { loadViaCache(cacheValue) } else { - loadViaGlide(imagePost.blurHash, storySize) + loadViaGlide(storySize) } + + blurLoader.load() } fun clear() { GlideApp.with(postImage).clear(postImage) - GlideApp.with(blurImage).clear(blurImage) postImage.setImageDrawable(null) - blurImage.setImageDrawable(null) + + blurLoader.clear() } private fun loadViaCache(cacheValue: StoryCache.StoryCacheValue) { - Log.d(TAG, "Attachment in cache. Loading via cache...") - val blurTarget = cacheValue.blurTarget - if (blurTarget != null) { - blurTarget.addListener(blurListener) - fragment.viewLifecycleOwner.lifecycle.addObserver(OnDestroy { blurTarget.removeListener(blurListener) }) - } else { - blurState = LoadState.FAILED - notifyListeners() - } + Log.d(TAG, "Image in cache. Loading via cache...") - val imageTarget = cacheValue.imageTarget + val imageTarget = cacheValue.imageTarget!! imageTarget.addListener(imageListener) - fragment.viewLifecycleOwner.lifecycle.addObserver(OnDestroy { imageTarget.removeListener(blurListener) }) + fragment.viewLifecycleOwner.lifecycle.addObserver(OnDestroy { imageTarget.removeListener(imageListener) }) } - private fun loadViaGlide(blurHash: BlurHash?, storySize: StoryDisplay.Size) { - Log.d(TAG, "Attachment not in cache. Loading via glide...") - if (blurHash != null) { - GlideApp.with(blurImage) - .load(blurHash) - .override(storySize.width, storySize.height) - .addListener(object : RequestListener { - override fun onLoadFailed(e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean): Boolean { - blurState = LoadState.FAILED - notifyListeners() - return false - } - - override fun onResourceReady(resource: Drawable?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean): Boolean { - blurState = LoadState.READY - notifyListeners() - return false - } - }) - .into(blurImage) - } else { - blurState = LoadState.FAILED - notifyListeners() - } - + private fun loadViaGlide(storySize: StoryDisplay.Size) { + Log.d(TAG, "Image not in cache. Loading via glide...") GlideApp.with(postImage) .load(DecryptableStreamUriLoader.DecryptableUri(imagePost.imageUri)) .override(storySize.width, storySize.height) @@ -138,6 +105,16 @@ class StoryImageLoader( .into(postImage) } + override fun onBlurLoaded() { + blurState = LoadState.READY + notifyListeners() + } + + override fun onBlurFailed() { + blurState = LoadState.FAILED + notifyListeners() + } + private fun notifyListeners() { if (fragment.isDetached) { Log.w(TAG, "Fragment is detached, dropping notify call.") @@ -153,15 +130,15 @@ class StoryImageLoader( } } - private inner class OnDestroy(private val onDestroy: () -> Unit) : DefaultLifecycleObserver { - override fun onDestroy(owner: LifecycleOwner) { - onDestroy() - } - } - private enum class LoadState { INIT, READY, FAILED } + + private inner class OnDestroy(private val onDestroy: () -> Unit) : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + onDestroy() + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryPostFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryPostFragment.kt index d79013798..e82233f51 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryPostFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryPostFragment.kt @@ -110,12 +110,23 @@ class StoryPostFragment : Fragment(R.layout.stories_post_fragment) { presentNone() binding.video.visible = true + binding.blur.visible = true + + val storyBlurLoader = StoryBlurLoader( + viewLifecycleOwner.lifecycle, + state.blurHash, + state.videoUri, + pageViewModel.storyCache, + StoryDisplay.getStorySize(resources), + binding.blur + ) storyVideoLoader = StoryVideoLoader( this, state, binding.video, - requireCallback() + requireCallback(), + storyBlurLoader ) storyVideoLoader?.load() diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryPostState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryPostState.kt index 920146814..5bc394bf2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryPostState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryPostState.kt @@ -24,7 +24,8 @@ sealed class StoryPostState { val videoUri: Uri, val size: Long, val clipStart: Duration, - val clipEnd: Duration + val clipEnd: Duration, + val blurHash: BlurHash? ) : StoryPostState() data class None(private val ts: Long = System.currentTimeMillis()) : StoryPostState() diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryPostViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryPostViewModel.kt index 93d10ccb5..dec60fa48 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryPostViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryPostViewModel.kt @@ -44,7 +44,8 @@ class StoryPostViewModel(private val repository: StoryTextPostRepository) : View videoUri = storyPostContent.uri, size = storyPostContent.attachment.size, clipStart = storyPostContent.attachment.transformProperties.videoTrimStartTimeUs.microseconds, - clipEnd = storyPostContent.attachment.transformProperties.videoTrimEndTimeUs.microseconds + clipEnd = storyPostContent.attachment.transformProperties.videoTrimEndTimeUs.microseconds, + blurHash = storyPostContent.attachment.blurHash ) } } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryVideoLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryVideoLoader.kt index 3df2f7ec4..5c082c5c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryVideoLoader.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryVideoLoader.kt @@ -13,7 +13,8 @@ class StoryVideoLoader( private val fragment: StoryPostFragment, private val videoPost: StoryPostState.VideoPost, private val videoPlayer: VideoPlayer, - private val callback: StoryPostFragment.Callback + private val callback: StoryPostFragment.Callback, + private val blurLoader: StoryBlurLoader ) : DefaultLifecycleObserver { companion object { @@ -25,11 +26,13 @@ class StoryVideoLoader( videoPlayer.setVideoSource(VideoSlide(fragment.requireContext(), videoPost.videoUri, videoPost.size, false), false, TAG, videoPost.clipStart.inWholeMilliseconds, videoPost.clipEnd.inWholeMilliseconds) videoPlayer.hideControls() videoPlayer.setKeepContentOnPlayerReset(false) + blurLoader.load() } fun clear() { fragment.viewLifecycleOwner.lifecycle.removeObserver(this) videoPlayer.stop() + blurLoader.clear() } override fun onResume(lifecycleOwner: LifecycleOwner) {