kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add blur hashes behind videos.
rodzic
fb8e81cf50
commit
3e2ecdaaa9
|
@ -29,15 +29,17 @@ class StoryCache(
|
||||||
val prefetchableAttachments: List<Attachment> = attachments
|
val prefetchableAttachments: List<Attachment> = attachments
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.filter { it.uri != null && it.uri !in cache }
|
.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 }
|
.filter { it.transferState == AttachmentDatabase.TRANSFER_PROGRESS_DONE }
|
||||||
.toList()
|
.toList()
|
||||||
|
|
||||||
val newMappings: Map<Uri, StoryCacheValue> = prefetchableAttachments.associateWith { attachment ->
|
val newMappings: Map<Uri, StoryCacheValue> = prefetchableAttachments.associateWith { attachment ->
|
||||||
val imageTarget = glideRequests
|
val imageTarget = if (MediaUtil.isImage(attachment)) {
|
||||||
.load(DecryptableStreamUriLoader.DecryptableUri(attachment.uri!!))
|
glideRequests
|
||||||
.priority(Priority.HIGH)
|
.load(DecryptableStreamUriLoader.DecryptableUri(attachment.uri!!))
|
||||||
.into(StoryCacheTarget(attachment.uri!!, storySize))
|
.priority(Priority.HIGH)
|
||||||
|
.into(StoryCacheTarget(attachment.uri!!, storySize))
|
||||||
|
} else null
|
||||||
|
|
||||||
val blurTarget = if (attachment.blurHash != null) {
|
val blurTarget = if (attachment.blurHash != null) {
|
||||||
glideRequests
|
glideRequests
|
||||||
|
@ -79,7 +81,7 @@ class StoryCache(
|
||||||
/**
|
/**
|
||||||
* Represents the load targets for an image and blur.
|
* 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.
|
* A custom glide target for loading a drawable. Placeholder immediately clears, and we don't want to do that, so we use this instead.
|
||||||
|
|
|
@ -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<Drawable> {
|
||||||
|
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
|
||||||
|
callback.onBlurFailed()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,6 @@ import com.bumptech.glide.load.engine.GlideException
|
||||||
import com.bumptech.glide.request.RequestListener
|
import com.bumptech.glide.request.RequestListener
|
||||||
import com.bumptech.glide.request.target.Target
|
import com.bumptech.glide.request.target.Target
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.blurhash.BlurHash
|
|
||||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader
|
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
import org.thoughtcrime.securesms.stories.viewer.page.StoryCache
|
import org.thoughtcrime.securesms.stories.viewer.page.StoryCache
|
||||||
|
@ -24,9 +23,19 @@ class StoryImageLoader(
|
||||||
private val storyCache: StoryCache,
|
private val storyCache: StoryCache,
|
||||||
private val storySize: StoryDisplay.Size,
|
private val storySize: StoryDisplay.Size,
|
||||||
private val postImage: ImageView,
|
private val postImage: ImageView,
|
||||||
private val blurImage: ImageView,
|
blurImage: ImageView,
|
||||||
private val callback: StoryPostFragment.Callback
|
private val callback: StoryPostFragment.Callback
|
||||||
) {
|
) : StoryBlurLoader.Callback {
|
||||||
|
|
||||||
|
private val blurLoader = StoryBlurLoader(
|
||||||
|
fragment.viewLifecycleOwner.lifecycle,
|
||||||
|
imagePost.blurHash,
|
||||||
|
imagePost.imageUri,
|
||||||
|
storyCache,
|
||||||
|
storySize,
|
||||||
|
blurImage,
|
||||||
|
this
|
||||||
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = Log.tag(StoryImageLoader::class.java)
|
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() {
|
fun load() {
|
||||||
val cacheValue = storyCache.getFromCache(imagePost.imageUri)
|
val cacheValue = storyCache.getFromCache(imagePost.imageUri)
|
||||||
if (cacheValue != null) {
|
if (cacheValue != null) {
|
||||||
loadViaCache(cacheValue)
|
loadViaCache(cacheValue)
|
||||||
} else {
|
} else {
|
||||||
loadViaGlide(imagePost.blurHash, storySize)
|
loadViaGlide(storySize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
blurLoader.load()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
GlideApp.with(postImage).clear(postImage)
|
GlideApp.with(postImage).clear(postImage)
|
||||||
GlideApp.with(blurImage).clear(blurImage)
|
|
||||||
|
|
||||||
postImage.setImageDrawable(null)
|
postImage.setImageDrawable(null)
|
||||||
blurImage.setImageDrawable(null)
|
|
||||||
|
blurLoader.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadViaCache(cacheValue: StoryCache.StoryCacheValue) {
|
private fun loadViaCache(cacheValue: StoryCache.StoryCacheValue) {
|
||||||
Log.d(TAG, "Attachment in cache. Loading via cache...")
|
Log.d(TAG, "Image 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
val imageTarget = cacheValue.imageTarget
|
val imageTarget = cacheValue.imageTarget!!
|
||||||
imageTarget.addListener(imageListener)
|
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) {
|
private fun loadViaGlide(storySize: StoryDisplay.Size) {
|
||||||
Log.d(TAG, "Attachment not in cache. Loading via glide...")
|
Log.d(TAG, "Image not in cache. Loading via glide...")
|
||||||
if (blurHash != null) {
|
|
||||||
GlideApp.with(blurImage)
|
|
||||||
.load(blurHash)
|
|
||||||
.override(storySize.width, storySize.height)
|
|
||||||
.addListener(object : RequestListener<Drawable> {
|
|
||||||
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
|
|
||||||
blurState = LoadState.FAILED
|
|
||||||
notifyListeners()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
|
|
||||||
blurState = LoadState.READY
|
|
||||||
notifyListeners()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.into(blurImage)
|
|
||||||
} else {
|
|
||||||
blurState = LoadState.FAILED
|
|
||||||
notifyListeners()
|
|
||||||
}
|
|
||||||
|
|
||||||
GlideApp.with(postImage)
|
GlideApp.with(postImage)
|
||||||
.load(DecryptableStreamUriLoader.DecryptableUri(imagePost.imageUri))
|
.load(DecryptableStreamUriLoader.DecryptableUri(imagePost.imageUri))
|
||||||
.override(storySize.width, storySize.height)
|
.override(storySize.width, storySize.height)
|
||||||
|
@ -138,6 +105,16 @@ class StoryImageLoader(
|
||||||
.into(postImage)
|
.into(postImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onBlurLoaded() {
|
||||||
|
blurState = LoadState.READY
|
||||||
|
notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBlurFailed() {
|
||||||
|
blurState = LoadState.FAILED
|
||||||
|
notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
private fun notifyListeners() {
|
private fun notifyListeners() {
|
||||||
if (fragment.isDetached) {
|
if (fragment.isDetached) {
|
||||||
Log.w(TAG, "Fragment is detached, dropping notify call.")
|
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 {
|
private enum class LoadState {
|
||||||
INIT,
|
INIT,
|
||||||
READY,
|
READY,
|
||||||
FAILED
|
FAILED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inner class OnDestroy(private val onDestroy: () -> Unit) : DefaultLifecycleObserver {
|
||||||
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
|
onDestroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,12 +110,23 @@ class StoryPostFragment : Fragment(R.layout.stories_post_fragment) {
|
||||||
presentNone()
|
presentNone()
|
||||||
|
|
||||||
binding.video.visible = true
|
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(
|
storyVideoLoader = StoryVideoLoader(
|
||||||
this,
|
this,
|
||||||
state,
|
state,
|
||||||
binding.video,
|
binding.video,
|
||||||
requireCallback()
|
requireCallback(),
|
||||||
|
storyBlurLoader
|
||||||
)
|
)
|
||||||
|
|
||||||
storyVideoLoader?.load()
|
storyVideoLoader?.load()
|
||||||
|
|
|
@ -24,7 +24,8 @@ sealed class StoryPostState {
|
||||||
val videoUri: Uri,
|
val videoUri: Uri,
|
||||||
val size: Long,
|
val size: Long,
|
||||||
val clipStart: Duration,
|
val clipStart: Duration,
|
||||||
val clipEnd: Duration
|
val clipEnd: Duration,
|
||||||
|
val blurHash: BlurHash?
|
||||||
) : StoryPostState()
|
) : StoryPostState()
|
||||||
|
|
||||||
data class None(private val ts: Long = System.currentTimeMillis()) : StoryPostState()
|
data class None(private val ts: Long = System.currentTimeMillis()) : StoryPostState()
|
||||||
|
|
|
@ -44,7 +44,8 @@ class StoryPostViewModel(private val repository: StoryTextPostRepository) : View
|
||||||
videoUri = storyPostContent.uri,
|
videoUri = storyPostContent.uri,
|
||||||
size = storyPostContent.attachment.size,
|
size = storyPostContent.attachment.size,
|
||||||
clipStart = storyPostContent.attachment.transformProperties.videoTrimStartTimeUs.microseconds,
|
clipStart = storyPostContent.attachment.transformProperties.videoTrimStartTimeUs.microseconds,
|
||||||
clipEnd = storyPostContent.attachment.transformProperties.videoTrimEndTimeUs.microseconds
|
clipEnd = storyPostContent.attachment.transformProperties.videoTrimEndTimeUs.microseconds,
|
||||||
|
blurHash = storyPostContent.attachment.blurHash
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -13,7 +13,8 @@ class StoryVideoLoader(
|
||||||
private val fragment: StoryPostFragment,
|
private val fragment: StoryPostFragment,
|
||||||
private val videoPost: StoryPostState.VideoPost,
|
private val videoPost: StoryPostState.VideoPost,
|
||||||
private val videoPlayer: VideoPlayer,
|
private val videoPlayer: VideoPlayer,
|
||||||
private val callback: StoryPostFragment.Callback
|
private val callback: StoryPostFragment.Callback,
|
||||||
|
private val blurLoader: StoryBlurLoader
|
||||||
) : DefaultLifecycleObserver {
|
) : DefaultLifecycleObserver {
|
||||||
|
|
||||||
companion object {
|
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.setVideoSource(VideoSlide(fragment.requireContext(), videoPost.videoUri, videoPost.size, false), false, TAG, videoPost.clipStart.inWholeMilliseconds, videoPost.clipEnd.inWholeMilliseconds)
|
||||||
videoPlayer.hideControls()
|
videoPlayer.hideControls()
|
||||||
videoPlayer.setKeepContentOnPlayerReset(false)
|
videoPlayer.setKeepContentOnPlayerReset(false)
|
||||||
|
blurLoader.load()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
fragment.viewLifecycleOwner.lifecycle.removeObserver(this)
|
fragment.viewLifecycleOwner.lifecycle.removeObserver(this)
|
||||||
videoPlayer.stop()
|
videoPlayer.stop()
|
||||||
|
blurLoader.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume(lifecycleOwner: LifecycleOwner) {
|
override fun onResume(lifecycleOwner: LifecycleOwner) {
|
||||||
|
|
Ładowanie…
Reference in New Issue