Move GlideImage into the glide module.

This commit is contained in:
Greyson Parrelli
2026-01-22 16:12:04 -05:00
committed by Alex Hart
parent 2f6baf8743
commit 986923ea6c
21 changed files with 42 additions and 28 deletions

1
lib/glide/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,24 @@
plugins {
id("signal-library")
id("com.google.devtools.ksp")
alias(libs.plugins.compose.compiler)
}
android {
namespace = "org.signal.glide"
buildFeatures {
compose = true
}
}
dependencies {
implementation(project(":core:util"))
implementation(libs.glide.glide)
ksp(libs.glide.ksp)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.material3)
implementation(libs.accompanist.drawablepainter)
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,20 @@
package org.signal.glide.apng;
import com.bumptech.glide.load.Option;
import org.signal.core.util.Conversions;
/**
* Holds options that can be used to alter how APNGs are decoded in Glide.
*/
public final class ApngOptions {
private static final String KEY = "org.signal.skip_apng";
public static Option<Boolean> ANIMATE = Option.disk(KEY, true, (keyBytes, value, messageDigest) -> {
messageDigest.update(keyBytes);
messageDigest.update(Conversions.intToByteArray(value ? 1 : 0));
});
private ApngOptions() {}
}

View File

@@ -0,0 +1,175 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide.compose
import android.graphics.drawable.Drawable
import android.widget.ImageView
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.DisposableEffectResult
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.viewinterop.AndroidView
import com.bumptech.glide.Glide
import com.bumptech.glide.TransitionOptions
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import org.signal.glide.apng.ApngOptions
/**
* Our very own GlideImage. The GlideImage composable provided by the bumptech library is not suitable because it was is using our encrypted cache decoder/encoder.
*/
@Composable
fun <T> GlideImage(
modifier: Modifier = Modifier,
model: T?,
imageSize: DpSize? = null,
scaleType: GlideImageScaleType = GlideImageScaleType.FIT_CENTER,
fallback: Drawable? = null,
error: Drawable? = fallback,
transition: TransitionOptions<*, Drawable>? = null,
diskCacheStrategy: DiskCacheStrategy = DiskCacheStrategy.ALL,
enableApngAnimation: Boolean = false
) {
if (enableApngAnimation) {
val density = LocalDensity.current
AndroidView(
factory = { context -> ImageView(context) },
update = { imageView ->
Glide.with(imageView.context)
.load(model)
.fallback(fallback)
.error(error)
.diskCacheStrategy(diskCacheStrategy)
.set(ApngOptions.ANIMATE, enableApngAnimation)
.apply {
scaleType.applyTo(this)
transition?.let(this::transition)
if (imageSize != null) {
with(density) {
this@apply.override(imageSize.width.toPx().toInt(), imageSize.height.toPx().toInt())
}
}
}
.into(imageView)
},
modifier = modifier
)
} else {
GlideImage(
model = model,
imageSize = imageSize,
scaleType = scaleType,
fallback = fallback,
error = error,
transition = transition,
diskCacheStrategy = diskCacheStrategy,
modifier = modifier
)
}
}
@Composable
private fun <T> GlideImage(
modifier: Modifier = Modifier,
model: T?,
imageSize: DpSize? = null,
scaleType: GlideImageScaleType = GlideImageScaleType.FIT_CENTER,
fallback: Drawable? = null,
error: Drawable? = fallback,
transition: TransitionOptions<*, Drawable>? = null,
diskCacheStrategy: DiskCacheStrategy = DiskCacheStrategy.ALL
) {
var drawable by remember {
mutableStateOf<Drawable?>(null)
}
val target = remember {
object : CustomTarget<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
drawable = resource
}
override fun onLoadCleared(placeholder: Drawable?) {
drawable = null
}
}
}
val density = LocalDensity.current
val context = LocalContext.current
DisposableEffect(model, fallback, error, diskCacheStrategy, density, imageSize) {
val requestManager = Glide.with(context)
val builder = requestManager
.load(model)
.fallback(fallback)
.error(error)
.diskCacheStrategy(diskCacheStrategy)
.apply {
scaleType.applyTo(this)
transition?.let(this::transition)
}
if (imageSize != null) {
with(density) {
builder.override(imageSize.width.toPx().toInt(), imageSize.height.toPx().toInt()).into(target)
}
} else {
builder.into(target)
}
object : DisposableEffectResult {
override fun dispose() {
requestManager.clear(target)
drawable = null
}
}
}
if (drawable != null) {
Image(
painter = rememberDrawablePainter(drawable),
contentDescription = null,
contentScale = if (model == null) ContentScale.Inside else ContentScale.Crop,
modifier = modifier
)
}
}
enum class GlideImageScaleType {
/** @see [com.bumptech.glide.request.RequestOptions.fitCenter] */
FIT_CENTER,
/** @see [com.bumptech.glide.request.RequestOptions.centerInside] */
CENTER_INSIDE,
/** @see [com.bumptech.glide.request.RequestOptions.centerCrop] */
CENTER_CROP,
/** @see [com.bumptech.glide.request.RequestOptions.circleCrop] */
CIRCLE_CROP;
fun <TranscodeT> applyTo(builder: com.bumptech.glide.RequestBuilder<TranscodeT>): com.bumptech.glide.RequestBuilder<TranscodeT> {
return when (this) {
FIT_CENTER -> builder.fitCenter()
CENTER_INSIDE -> builder.centerInside()
CENTER_CROP -> builder.centerCrop()
CIRCLE_CROP -> builder.circleCrop()
}
}
}

View File

@@ -0,0 +1,40 @@
package org.thoughtcrime.securesms.mms
import android.content.Context
import android.util.Log
import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.module.AppGlideModule
/**
* A [GlideModule] to configure Glide for the app. This class is discovered by Glide's annotation
* processor, and delegates its logic to a [RegisterGlideComponents]. It exists outside of the main
* Gradle module to reduce the scope of classes that KAPT needs to look at.
*/
@GlideModule
class SignalGlideModule : AppGlideModule() {
override fun isManifestParsingEnabled(): Boolean {
return false
}
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
registerGlideComponents.registerComponents(context, glide, registry)
}
override fun applyOptions(context: Context, builder: GlideBuilder) {
builder.setLogLevel(Log.ERROR)
}
companion object {
@JvmStatic
lateinit var registerGlideComponents: RegisterGlideComponents
}
}
interface RegisterGlideComponents {
fun registerComponents(context: Context, glide: Glide, registry: Registry)
}