Repository: thundernest/k-9
Issue: Contact photos flickering when e-mails are downloaded

### Expected behavior
E-mail list refreshing smoothly

### Actual behavior
Avatars / contact photos are flickering when client is downloading new messages.

### Steps to reproduce
0. fresh install
1. open mail box, headers are being downloaded
2. scroll down and load more
3. it starts downloading more email headers
4. observer images flickering while list is updated

### Environment
K-9 Mail version:
master

Android version:
OnePlus 6T, Android 9.0

Account type (IMAP, POP3, WebDAV/Exchange):
IMAP


Changed files:
app/ui/src/main/java/com/fsck/k9/contacts/ContactPictureLoader.kt

diff --git a/app/ui/src/main/java/com/fsck/k9/contacts/ContactPictureLoader.kt b/app/ui/src/main/java/com/fsck/k9/contacts/ContactPictureLoader.kt
index 6240f2161..6836dded7 100644
--- a/app/ui/src/main/java/com/fsck/k9/contacts/ContactPictureLoader.kt
+++ b/app/ui/src/main/java/com/fsck/k9/contacts/ContactPictureLoader.kt
@@ -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()