@ -2,10 +2,13 @@ package org.thoughtcrime.securesms.conversation.v2.utilities
import android.content.Context
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Bitmap
import android.graphics.Outline
import android.graphics.drawable.Drawable
import android.graphics.drawable.Drawable
import android.net.Uri
import android.net.Uri
import android.util.AttributeSet
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
import android.view.View
import android.view.ViewOutlineProvider
import android.widget.FrameLayout
import android.widget.FrameLayout
import androidx.core.view.isVisible
import androidx.core.view.isVisible
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.DiskCacheStrategy
@ -21,18 +24,17 @@ import org.session.libsignal.utilities.ListenableFuture
import org.session.libsignal.utilities.SettableFuture
import org.session.libsignal.utilities.SettableFuture
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
import org.thoughtcrime.securesms.mms.GlideRequest
import org.thoughtcrime.securesms.mms.GlideRequest
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.mms.Slide
import kotlin.Boolean
import kotlin.Int
import kotlin.getValue
import kotlin.lazy
import kotlin.let
open class ThumbnailView : FrameLayout {
open class ThumbnailView @JvmOverloads constructor (
context : Context ,
attrs : AttributeSet ? = null ,
defStyleAttr : Int = 0
) : FrameLayout ( context , attrs , defStyleAttr ) {
companion object {
companion object {
private const val WIDTH = 0
private const val WIDTH = 0
private const val HEIGHT = 1
private const val HEIGHT = 1
@ -41,30 +43,29 @@ open class ThumbnailView: FrameLayout {
private val binding : ThumbnailViewBinding by lazy { ThumbnailViewBinding . bind ( this ) }
private val binding : ThumbnailViewBinding by lazy { ThumbnailViewBinding . bind ( this ) }
// region Lifecycle
// region Lifecycle
constructor ( context : Context ) : super ( context ) { initialize ( null ) }
constructor ( context : Context , attrs : AttributeSet ) : super ( context , attrs ) { initialize ( attrs ) }
constructor ( context : Context , attrs : AttributeSet , defStyleAttr : Int ) : super ( context , attrs , defStyleAttr ) { initialize ( attrs ) }
val loadIndicator : View by lazy { binding . thumbnailLoadIndicator }
val loadIndicator : View by lazy { binding . thumbnailLoadIndicator }
private val dimensDelegate = ThumbnailDimensDelegate ( )
private val dimensDelegate = ThumbnailDimensDelegate ( )
private var slide : Slide ? = null
private var slide : Slide ? = null
var radius : Int = 0
private fun initialize ( attrs : AttributeSet ? ) {
if ( attrs != null ) {
val typedArray = context . theme . obtainStyledAttributes ( attrs , R . styleable . ThumbnailView , 0 , 0 )
dimensDelegate . setBounds ( typedArray . getDimensionPixelSize ( R . styleable . ThumbnailView _minWidth , 0 ) ,
typedArray . getDimensionPixelSize ( R . styleable . ThumbnailView _minHeight , 0 ) ,
typedArray . getDimensionPixelSize ( R . styleable . ThumbnailView _maxWidth , 0 ) ,
typedArray . getDimensionPixelSize ( R . styleable . ThumbnailView _maxHeight , 0 ) )
radius = typedArray . getDimensionPixelSize ( R . styleable . ThumbnailView _thumbnail _radius , 0 )
init {
attrs ?. let { context . theme . obtainStyledAttributes ( it , R . styleable . ThumbnailView , 0 , 0 ) }
typedArray . recycle ( )
?. apply {
}
dimensDelegate . setBounds (
getDimensionPixelSize ( R . styleable . ThumbnailView _minWidth , 0 ) ,
getDimensionPixelSize ( R . styleable . ThumbnailView _minHeight , 0 ) ,
getDimensionPixelSize ( R . styleable . ThumbnailView _maxWidth , 0 ) ,
getDimensionPixelSize ( R . styleable . ThumbnailView _maxHeight , 0 )
)
setRoundedCorners (
getDimensionPixelSize ( R . styleable . ThumbnailView _thumbnail _radius , 0 )
)
recycle ( )
}
}
}
override fun onMeasure ( widthMeasureSpec : Int , heightMeasureSpec : Int ) {
override fun onMeasure ( widthMeasureSpec : Int , heightMeasureSpec : Int ) {
@ -84,114 +85,118 @@ open class ThumbnailView: FrameLayout {
private fun getDefaultWidth ( ) = maxOf ( layoutParams ?. width ?: 0 , 0 )
private fun getDefaultWidth ( ) = maxOf ( layoutParams ?. width ?: 0 , 0 )
private fun getDefaultHeight ( ) = maxOf ( layoutParams ?. height ?: 0 , 0 )
private fun getDefaultHeight ( ) = maxOf ( layoutParams ?. height ?: 0 , 0 )
// endregion
// endregion
// region Interaction
// region Interaction
fun setImageResource ( glide : GlideRequests , slide : Slide , isPreview : Boolean , mms : MmsMessageRecord ? ) : ListenableFuture < Boolean > {
fun setRoundedCorners ( radius : Int ) {
return setImageResource ( glide , slide , isPreview , 0 , 0 , mms )
// create an outline provider and clip the whole view to that shape
}
// that way we can round the image and the background ( and any other artifacts that the view may contain )
val mOutlineProvider = object : ViewOutlineProvider ( ) {
fun setImageResource ( glide : GlideRequests , slide : Slide ,
override fun getOutline ( view : View , outline : Outline ) {
isPreview : Boolean , naturalWidth : Int ,
// all corners
naturalHeight : Int , mms : MmsMessageRecord ? ) : ListenableFuture < Boolean > {
outline . setRoundRect ( 0 , 0 , view . width , view . height , radius . toFloat ( ) )
}
}
val currentSlide = this . slide
outlineProvider = mOutlineProvider
clipToOutline = true
}
fun setImageResource (
glide : GlideRequests ,
slide : Slide ,
isPreview : Boolean
) : ListenableFuture < Boolean > = setImageResource ( glide , slide , isPreview , 0 , 0 )
fun setImageResource (
glide : GlideRequests , slide : Slide ,
isPreview : Boolean , naturalWidth : Int ,
naturalHeight : Int
) : ListenableFuture < Boolean > {
binding . playOverlay . isVisible = ( slide . thumbnailUri != null && slide . hasPlayOverlay ( ) &&
binding . playOverlay . isVisible = ( slide . thumbnailUri != null && slide . hasPlayOverlay ( ) &&
( slide . transferState == AttachmentTransferProgress . TRANSFER _PROGRESS _DONE || isPreview ) )
( slide . transferState == AttachmentTransferProgress . TRANSFER _PROGRESS _DONE || isPreview ) )
if ( equals ( currentSlide , slide ) ) {
if ( equals ( this . s lide, slide ) ) {
// don't re-load slide
// don't re-load slide
return SettableFuture ( false )
return SettableFuture ( false )
}
}
if ( currentSlide != null && currentSlide . fastPreflightId != null && currentSlide . fastPreflightId == slide . fastPreflightId ) {
// not reloading slide for fast preflight
this . slide = slide
}
this . slide = slide
this . slide = slide
binding . thumbnailLoadIndicator . isVisible = slide . isInProgress
binding . thumbnailLoadIndicator . isVisible = slide . isInProgress
binding . thumbnailDownloadIcon . isVisible = slide . transferState == AttachmentTransferProgress . TRANSFER _PROGRESS _FAILED
binding . thumbnailDownloadIcon . isVisible =
slide . transferState == AttachmentTransferProgress . TRANSFER _PROGRESS _FAILED
dimensDelegate . setDimens ( naturalWidth , naturalHeight )
dimensDelegate . setDimens ( naturalWidth , naturalHeight )
invalidate ( )
invalidate ( )
val result = SettableFuture < Boolean > ( )
return SettableFuture < Boolean > ( ) . also {
when {
when {
slide . thumbnailUri != null -> {
slide . thumbnailUri != null -> {
buildThumbnailGlideRequest ( glide , slide ) . into (
buildThumbnailGlideRequest ( glide , slide ) . into ( GlideDrawableListeningTarget ( binding . thumbnailImage , binding . thumbnailLoadIndicator , result ) )
GlideDrawableListeningTarget ( binding . thumbnailImage , binding . thumbnailLoadIndicator , it )
}
)
slide . hasPlaceholder ( ) -> {
}
buildPlaceholderGlideRequest ( glide , slide ) . into ( GlideBitmapListeningTarget ( binding . thumbnailImage , null , result ) )
slide . hasPlaceholder ( ) -> {
}
buildPlaceholderGlideRequest ( glide , slide ) . into (
else -> {
GlideBitmapListeningTarget ( binding . thumbnailImage , null , it )
glide . clear ( binding . thumbnailImage )
)
result . set ( false )
}
else -> {
glide . clear ( binding . thumbnailImage )
it . set ( false )
}
}
}
}
}
return result
}
}
fun buildThumbnailGlideRequest ( glide : GlideRequests , slide : Slide ) : GlideRequest < Drawable > {
private fun buildThumbnailGlideRequest (
glide : GlideRequests ,
val dimens = dimensDelegate . resourceSize ( )
slide : Slide
) : GlideRequest < Drawable > = glide . load ( DecryptableUri ( slide . thumbnailUri !! ) )
val request = glide . load ( DecryptableUri ( slide . thumbnailUri !! ) )
. diskCacheStrategy ( DiskCacheStrategy . NONE )
. diskCacheStrategy ( DiskCacheStrategy . NONE )
. overrideDimensions ( )
. let { request ->
. transition ( DrawableTransitionOptions . withCrossFade ( ) )
if ( dimens [ WIDTH ] == 0 || dimens [ HEIGHT ] == 0 ) {
. transform ( CenterCrop ( ) )
request . override ( getDefaultWidth ( ) , getDefaultHeight ( ) )
. missingThumbnailPicture ( slide . isInProgress )
} else {
request . override ( dimens [ WIDTH ] , dimens [ HEIGHT ] )
private fun buildPlaceholderGlideRequest (
}
glide : GlideRequests ,
}
slide : Slide
. transition ( DrawableTransitionOptions . withCrossFade ( ) )
) : GlideRequest < Bitmap > = glide . asBitmap ( )
. centerCrop ( )
. load ( slide . getPlaceholderRes ( context . theme ) )
. diskCacheStrategy ( DiskCacheStrategy . NONE )
return if ( slide . isInProgress ) request else request . apply ( RequestOptions . errorOf ( R . drawable . ic _missing _thumbnail _picture ) )
. overrideDimensions ( )
}
. fitCenter ( )
fun buildPlaceholderGlideRequest ( glide : GlideRequests , slide : Slide ) : GlideRequest < Bitmap > {
val dimens = dimensDelegate . resourceSize ( )
return glide . asBitmap ( )
. load ( slide . getPlaceholderRes ( context . theme ) )
. diskCacheStrategy ( DiskCacheStrategy . NONE )
. let { request ->
if ( dimens [ WIDTH ] == 0 || dimens [ HEIGHT ] == 0 ) {
request . override ( getDefaultWidth ( ) , getDefaultHeight ( ) )
} else {
request . override ( dimens [ WIDTH ] , dimens [ HEIGHT ] )
}
}
. fitCenter ( )
}
open fun clear ( glideRequests : GlideRequests ) {
open fun clear ( glideRequests : GlideRequests ) {
glideRequests . clear ( binding . thumbnailImage )
glideRequests . clear ( binding . thumbnailImage )
slide = null
slide = null
}
}
fun setImageResource ( glideRequests : GlideRequests , uri : Uri ) : ListenableFuture < Boolean > {
fun setImageResource (
val future = SettableFuture < Boolean > ( )
glideRequests : GlideRequests ,
uri : Uri
var request : GlideRequest < Drawable > = glideRequests . load ( DecryptableUri ( uri ) )
) : ListenableFuture < Boolean > = glideRequests . load ( DecryptableUri ( uri ) )
. diskCacheStrategy ( DiskCacheStrategy . NONE )
. diskCacheStrategy ( DiskCacheStrategy . NONE )
. transition ( DrawableTransitionOptions . withCrossFade ( ) )
. transition ( DrawableTransitionOptions . withCrossFade ( ) )
. transform ( CenterCrop ( ) )
request = if ( radius > 0 ) {
. intoDrawableTargetAsFuture ( )
request . transforms ( CenterCrop ( ) , RoundedCorners ( radius ) )
} else {
private fun GlideRequest < Drawable > . intoDrawableTargetAsFuture ( ) =
request . transforms ( CenterCrop ( ) )
SettableFuture < Boolean > ( ) . also {
binding . run {
GlideDrawableListeningTarget ( thumbnailImage , thumbnailLoadIndicator , it )
} . let { into ( it ) }
}
}
request . into ( GlideDrawableListeningTarget ( binding . thumbnailImage , binding . thumbnailLoadIndicator , future ) )
private fun < T > GlideRequest < T > . overrideDimensions ( ) =
dimensDelegate . resourceSize ( ) . takeIf { 0 !in it }
?. let { override ( it [ WIDTH ] , it [ HEIGHT ] ) }
?: override ( getDefaultWidth ( ) , getDefaultHeight ( ) )
}
return future
private fun < T > GlideRequest < T > . missingThumbnailPicture (
}
inProgress : Boolean
}
) = takeIf { inProgress } ?: apply ( RequestOptions . errorOf ( R . drawable . ic _missing _thumbnail _picture ) )