Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e7d8cda0 authored by cketti's avatar cketti
Browse files

Fix contact picture flickering in message list

Change the way we try to load contact pictures and generate contact
letter fallback images afterwards, so Glide caches the result.
parent 7dadab73
Loading
Loading
Loading
Loading
+46 −94
Original line number Diff line number Diff line
@@ -3,33 +3,26 @@ package com.fsck.k9.contacts

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Bitmap.CompressFormat
import android.graphics.BitmapFactory
import android.net.Uri
import androidx.annotation.WorkerThread
import android.widget.ImageView
import androidx.annotation.WorkerThread
import com.bumptech.glide.Glide
import com.bumptech.glide.Priority
import com.bumptech.glide.load.ResourceDecoder
import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.data.StreamLocalUriFetcher
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.Resource
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.stream.StreamModelLoader
import com.bumptech.glide.load.resource.bitmap.BitmapEncoder
import com.bumptech.glide.load.resource.bitmap.BitmapResource
import com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder
import com.bumptech.glide.load.resource.drawable.GlideDrawable
import com.bumptech.glide.load.resource.file.FileToStreamDecoder
import com.bumptech.glide.load.resource.transcode.BitmapToGlideDrawableTranscoder
import com.bumptech.glide.request.FutureTarget
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.fsck.k9.helper.Contacts
import com.fsck.k9.mail.Address
import com.fsck.k9.ui.R
import com.fsck.k9.view.RecipientSelectView.Recipient
import java.io.InputStream

import timber.log.Timber
import kotlin.math.max

class ContactPictureLoader(
        private val context: Context,
@@ -44,53 +37,45 @@ class ContactPictureLoader(

    fun setContactPicture(imageView: ImageView, address: Address) {
        Glide.with(imageView.context)
                .using(ContactPictureModelLoader())
                .using(AddressModelLoader(backgroundCacheId), Address::class.java)
                .from(Address::class.java)
                .load(address)
                .`as`(Bitmap::class.java)
                .decoder(ContactImageBitmapDecoder())
                .signature(contactLetterBitmapCreator.signatureOf(address))
                .diskCacheStrategy(DiskCacheStrategy.NONE)
                .listener(FallbackImageRequestListener(address, imageView))
                // for some reason, following 2 lines fix loading issues.
                .load(address)
                .dontAnimate()
                .override(pictureSizeInPx, pictureSizeInPx)
                .into(imageView)
    }

    fun setContactPicture(imageView: ImageView, recipient: Recipient) {
        val contactPictureUri = recipient.photoThumbnailUri
        if (contactPictureUri != null) {
            setContactPicture(imageView, contactPictureUri, recipient.address)
            setContactPicture(imageView, contactPictureUri)
        } else {
            setFallbackPicture(imageView, recipient.address)
        }
    }

    private fun setContactPicture(imageView: ImageView, contactPictureUri: Uri, address: Address) {
    private fun setContactPicture(imageView: ImageView, contactPictureUri: Uri) {
        Glide.with(imageView.context)
                .load(contactPictureUri)
                .error(R.drawable.ic_contact_picture)
                .diskCacheStrategy(DiskCacheStrategy.NONE)
                .listener(FallbackImageRequestListener(address, imageView))
                // for some reason, following 2 lines fix loading issues.
                .dontAnimate()
                .override(pictureSizeInPx, pictureSizeInPx)
                .into(imageView)
    }

    private fun setFallbackPicture(imageView: ImageView, address: Address) {
        val context = imageView.context
        Glide.with(context)
        Glide.with(imageView.context)
                .using(AddressModelLoader(backgroundCacheId), Address::class.java)
                .from(Address::class.java)
                .`as`(Bitmap::class.java)
                .transcode(BitmapToGlideDrawableTranscoder(context), GlideDrawable::class.java)
                .decoder(ContactLetterBitmapDecoder())
                .decoder(ContactImageBitmapDecoder(contactLetterOnly = true))
                .signature(contactLetterBitmapCreator.signatureOf(address))
                .encoder(BitmapEncoder(CompressFormat.PNG, 0))
                .cacheDecoder(FileToStreamDecoder(StreamBitmapDecoder(context)))
                .diskCacheStrategy(DiskCacheStrategy.NONE)
                .load(address)
                // for some reason, following 2 lines fix loading issues.
                .dontAnimate()
                .override(pictureSizeInPx, pictureSizeInPx)
                .into(imageView)
    }

@@ -110,6 +95,7 @@ class ContactPictureLoader(
        return Glide.with(context)
                .load(contactPictureUri)
                .asBitmap()
                .error(R.drawable.ic_contact_picture)
                .diskCacheStrategy(DiskCacheStrategy.NONE)
                .dontAnimate()
                .into(pictureSizeInPx, pictureSizeInPx)
@@ -121,9 +107,7 @@ class ContactPictureLoader(
                .using(AddressModelLoader(backgroundCacheId), Address::class.java)
                .from(Address::class.java)
                .`as`(Bitmap::class.java)
                .decoder(ContactLetterBitmapDecoder())
                .encoder(BitmapEncoder(CompressFormat.PNG, 0))
                .cacheDecoder(FileToStreamDecoder(StreamBitmapDecoder(context)))
                .decoder(ContactImageBitmapDecoder(contactLetterOnly = true))
                .diskCacheStrategy(DiskCacheStrategy.NONE)
                .load(address)
                .dontAnimate()
@@ -131,18 +115,41 @@ class ContactPictureLoader(
                .getOrNull()
    }

    private inner class ContactLetterBitmapDecoder : ResourceDecoder<Address, Bitmap> {
    private inner class ContactImageBitmapDecoder(
        private val contactLetterOnly: Boolean = false
    ) : ResourceDecoder<Address, Bitmap> {

        override fun decode(address: Address, width: Int, height: Int): Resource<Bitmap> {
            val pool = Glide.get(context).bitmapPool
            val bitmap: Bitmap =
                    pool.getDirty(pictureSizeInPx, pictureSizeInPx, Bitmap.Config.ARGB_8888) ?:
                    Bitmap.createBitmap(pictureSizeInPx, pictureSizeInPx, Bitmap.Config.ARGB_8888)

            contactLetterBitmapCreator.drawBitmap(bitmap, pictureSizeInPx, address)
            val size = max(width, height)

            val bitmap = loadContactPicture(address) ?: createContactLetterBitmap(address, size, pool)

            return BitmapResource.obtain(bitmap, pool)
        }

        private fun loadContactPicture(address: Address): Bitmap? {
            if (contactLetterOnly) return null

            val photoUri = contactsHelper.getPhotoUri(address.address) ?: return null
            return try {
                context.contentResolver.openInputStream(photoUri).use { inputStream ->
                    BitmapFactory.decodeStream(inputStream)
                }
            } catch (e: Exception) {
                Timber.e(e, "Couldn't load contact picture: $photoUri")
                null
            }
        }

        private fun createContactLetterBitmap(address: Address, size: Int, pool: BitmapPool): Bitmap {
            val bitmap = pool.getDirty(size, size, Bitmap.Config.ARGB_8888)
                ?: Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)

            return contactLetterBitmapCreator.drawBitmap(bitmap, size, address)
        }

        override fun getId(): String {
            return "fallback-photo"
        }
@@ -161,61 +168,6 @@ class ContactPictureLoader(
        }
    }

    private inner class ContactPictureModelLoader : StreamModelLoader<Address> {
        override fun getResourceFetcher(address: Address, width: Int, height: Int): DataFetcher<InputStream>? {
            return ContactPictureDataFetcher(address)
        }
    }

    private inner class ContactPictureDataFetcher(val address: Address) : DataFetcher<InputStream> {
        var streamLocalUriFetcher: StreamLocalUriFetcher? = null

        override fun loadData(priority: Priority?): InputStream? {
            val photoUri = contactsHelper.getPhotoUri(address.address)

            return photoUri?.let {
                StreamLocalUriFetcher(context, photoUri).also {
                    streamLocalUriFetcher = it
                }.loadData(priority)
            }
        }

        override fun cancel() = Unit

        override fun cleanup() {
            streamLocalUriFetcher?.cleanup()
        }

        override fun getId() = "contact:${address.address}"
    }


    private inner class FallbackImageRequestListener<T>(
            val address: Address,
            val imageView: ImageView
    ) : RequestListener<T, GlideDrawable> {

        override fun onException(
                e: Exception?,
                model: T,
                target: Target<GlideDrawable>,
                isFirstResource: Boolean
        ): Boolean {
            setFallbackPicture(imageView, address)
            return true
        }

        override fun onResourceReady(
                resource: GlideDrawable,
                model: T,
                target: Target<GlideDrawable>,
                isFromMemoryCache: Boolean,
                isFirstResource: Boolean
        ): Boolean {
            return false
        }
    }

    private fun <T> FutureTarget<T>.getOrNull(): T? {
        return try {
            get()