diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java index 6a8d1ed380..2aa13792ba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java @@ -15,22 +15,16 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.bumptech.glide.Priority; -import com.bumptech.glide.load.DataSource; -import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.load.engine.GlideException; -import com.bumptech.glide.request.RequestListener; -import com.bumptech.glide.request.RequestOptions; -import com.bumptech.glide.request.target.Target; - import org.signal.core.util.ThreadUtil; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.components.emoji.parsing.EmojiDrawInfo; import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser; -import org.thoughtcrime.securesms.emoji.EmojiPage; +import org.thoughtcrime.securesms.emoji.EmojiPageCache; import org.thoughtcrime.securesms.emoji.EmojiSource; -import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.util.DeviceProperties; +import org.thoughtcrime.securesms.util.FutureTaskListener; + +import java.util.concurrent.ExecutionException; class EmojiProvider { @@ -96,27 +90,20 @@ class EmojiProvider { final int lowMemoryDecodeScale = DeviceProperties.isLowMemoryDevice(context) ? 2 : 1; final EmojiSource source = EmojiSource.getLatest(); final EmojiDrawable drawable = new EmojiDrawable(source, drawInfo, lowMemoryDecodeScale); - GlideApp.with(context) - .asBitmap() - .diskCacheStrategy(DiskCacheStrategy.NONE) - .load(drawInfo.getPage()) - .priority(Priority.HIGH) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .apply(new RequestOptions().set(EmojiPage.IN_SAMPLE_SIZE, lowMemoryDecodeScale)) - .addListener(new RequestListener() { - @Override - public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { - Log.d(TAG, "Failed to load emoji bitmap resource", e); - return false; - } - @Override - public boolean onResourceReady(Bitmap resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { - ThreadUtil.runOnMain(() -> drawable.setBitmap(resource)); - return true; - } - }) - .submit(); + EmojiPageCache.INSTANCE + .load(context, drawInfo.getPage(), lowMemoryDecodeScale) + .addListener(new FutureTaskListener() { + @Override + public void onSuccess(Bitmap result) { + ThreadUtil.runOnMain(() -> drawable.setBitmap(result)); + } + + @Override + public void onFailure(ExecutionException exception) { + Log.d(TAG, "Failed to load emoji bitmap resource", exception); + } + }); return drawable; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiPage.kt b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiPage.kt index fca927cf7b..1d3341c914 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiPage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiPage.kt @@ -1,85 +1,17 @@ package org.thoughtcrime.securesms.emoji -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory import android.net.Uri -import com.bumptech.glide.Priority -import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.Key -import com.bumptech.glide.load.Option -import com.bumptech.glide.load.Options -import com.bumptech.glide.load.data.DataFetcher -import com.bumptech.glide.load.model.ModelLoader -import com.bumptech.glide.load.model.ModelLoaderFactory -import com.bumptech.glide.load.model.MultiModelLoaderFactory -import org.thoughtcrime.securesms.mms.PartAuthority -import java.io.InputStream import java.security.MessageDigest typealias EmojiPageFactory = (Uri) -> EmojiPage -sealed class EmojiPage(private val uri: Uri) : Key { +sealed class EmojiPage(open val uri: Uri) : Key { override fun updateDiskCacheKey(messageDigest: MessageDigest) { messageDigest.update("EmojiPage".encodeToByteArray()) messageDigest.update(uri.toString().encodeToByteArray()) } - data class Asset(private val uri: Uri) : EmojiPage(uri) - data class Disk(private val uri: Uri) : EmojiPage(uri) - - class Loader(private val context: Context) : ModelLoader { - override fun buildLoadData( - model: EmojiPage, - width: Int, - height: Int, - options: Options - ): ModelLoader.LoadData { - return ModelLoader.LoadData(model, Fetcher(context, model, options.get(IN_SAMPLE_SIZE) ?: 1)) - } - - override fun handles(model: EmojiPage): Boolean = true - - class Factory(private val context: Context) : ModelLoaderFactory { - override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { - return Loader(context) - } - - override fun teardown() = Unit - } - } - - class Fetcher(private val context: Context, private val model: EmojiPage, private val inSampleSize: Int) : DataFetcher { - override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { - try { - val inputStream: InputStream = when (model) { - is Asset -> context.assets.open(model.uri.toString().replace("file:///android_asset/", "")) - is Disk -> EmojiFiles.openForReading(context, PartAuthority.getEmojiFilename(model.uri)) - } - - val bitmapOptions = BitmapFactory.Options() - bitmapOptions.inSampleSize = inSampleSize - - callback.onDataReady(BitmapFactory.decodeStream(inputStream, null, bitmapOptions)) - } catch (e: Exception) { - callback.onLoadFailed(e) - } - } - - override fun cleanup() = Unit - override fun cancel() = Unit - - override fun getDataClass(): Class { - return Bitmap::class.java - } - - override fun getDataSource(): DataSource { - return DataSource.LOCAL - } - } - - companion object { - @JvmField - val IN_SAMPLE_SIZE: Option = Option.memory("emoji_page_in_sample_size", 1) - } + data class Asset(override val uri: Uri) : EmojiPage(uri) + data class Disk(override val uri: Uri) : EmojiPage(uri) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiPageCache.kt b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiPageCache.kt new file mode 100644 index 0000000000..b29b1a098b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiPageCache.kt @@ -0,0 +1,73 @@ +package org.thoughtcrime.securesms.emoji + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import androidx.annotation.MainThread +import androidx.annotation.WorkerThread +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.mms.PartAuthority +import org.thoughtcrime.securesms.util.ListenableFutureTask +import org.thoughtcrime.securesms.util.SoftHashMap +import org.thoughtcrime.securesms.util.concurrent.SimpleTask +import java.io.IOException +import java.io.InputStream + +object EmojiPageCache { + + private val TAG = Log.tag(EmojiPageCache::class.java) + + private val cache: SoftHashMap = SoftHashMap() + private val tasks: HashMap> = hashMapOf() + + @MainThread + fun load(context: Context, emojiPage: EmojiPage, inSampleSize: Int): ListenableFutureTask { + val applicationContext = context.applicationContext + val emojiPageRequest = EmojiPageRequest(emojiPage, inSampleSize) + val bitmap: Bitmap? = cache[emojiPageRequest] + val task: ListenableFutureTask? = tasks[emojiPageRequest] + + return when { + bitmap != null -> ListenableFutureTask(bitmap) + task != null -> task + else -> { + val newTask = ListenableFutureTask { + try { + Log.i(TAG, "Loading page $emojiPageRequest") + loadInternal(applicationContext, emojiPageRequest) + } catch (e: IOException) { + Log.w(TAG, e) + null + } + } + + tasks[emojiPageRequest] = newTask + + SimpleTask.run(newTask::run) { + try { + cache[emojiPageRequest] = newTask.get() + } finally { + tasks.remove(emojiPageRequest) + } + } + + newTask + } + } + } + + @WorkerThread + private fun loadInternal(context: Context, emojiPageRequest: EmojiPageRequest): Bitmap? { + val inputStream: InputStream = when (emojiPageRequest.emojiPage) { + is EmojiPage.Asset -> context.assets.open(emojiPageRequest.emojiPage.uri.toString().replace("file:///android_asset/", "")) + is EmojiPage.Disk -> EmojiFiles.openForReading(context, PartAuthority.getEmojiFilename(emojiPageRequest.emojiPage.uri)) + } + + val bitmapOptions = BitmapFactory.Options() + bitmapOptions.inSampleSize = emojiPageRequest.inSampleSize + + return BitmapFactory.decodeStream(inputStream, null, bitmapOptions) + } + + private data class EmojiPageRequest(val emojiPage: EmojiPage, val inSampleSize: Int) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java b/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java index f3e717fc08..16872a6243 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java @@ -29,7 +29,6 @@ import org.thoughtcrime.securesms.blurhash.BlurHashResourceDecoder; import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; import org.thoughtcrime.securesms.crypto.AttachmentSecret; import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; -import org.thoughtcrime.securesms.emoji.EmojiPage; import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl; import org.thoughtcrime.securesms.glide.ChunkedImageUrlLoader; import org.thoughtcrime.securesms.glide.ContactPhotoLoader; @@ -91,7 +90,6 @@ public class SignalGlideModule extends AppGlideModule { registry.prepend(BlurHash.class, Bitmap.class, new BlurHashResourceDecoder()); - registry.append(EmojiPage.class, Bitmap.class, new EmojiPage.Loader.Factory(context)); registry.append(ConversationShortcutPhoto.class, Bitmap.class, new ConversationShortcutPhoto.Loader.Factory(context)); registry.append(ContactPhoto.class, InputStream.class, new ContactPhotoLoader.Factory(context)); registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context));