Improve organization of glide packages.

Generic image processing classes were scattered alongside Signal-specific Glide code across multiple packages: `org.signal.glide`, `org.thoughtcrime.securesms.glide` and `org.thoughtcrime.securesms.mms`.

This change provides a clearer separation of concerns:
- `org.signal.glide` contains generic image loading components
- `org.thoughtcrime.securesms.glide` contains Signal-specific Glide integrations
- Feature-specific loaders are moved to their respective domain packages (e.g. `.badges`, `.contacts`)
This commit is contained in:
jeffrey-signal
2025-08-15 12:44:42 -04:00
committed by Jeffrey Starke
parent cc43add7af
commit 47508495ed
48 changed files with 220 additions and 179 deletions

View File

@@ -1,61 +0,0 @@
package org.thoughtcrime.securesms.glide;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.model.GlideUrl;
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.badges.models.Badge;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import java.io.InputStream;
import okhttp3.OkHttpClient;
/**
* A loader which will load a sprite sheet for a particular badge at the correct dpi for this device.
*/
public class BadgeLoader implements ModelLoader<Badge, InputStream> {
private final OkHttpClient client;
private BadgeLoader(OkHttpClient client) {
this.client = client;
}
@Override
public @Nullable LoadData<InputStream> buildLoadData(@NonNull Badge request, int width, int height, @NonNull Options options) {
return new LoadData<>(request, new OkHttpStreamFetcher(client, new GlideUrl(request.getImageUrl().toString())));
}
@Override
public boolean handles(@NonNull Badge badgeSpriteSheetRequest) {
return true;
}
public static Factory createFactory() {
return new Factory(AppDependencies.getSignalOkHttpClient());
}
public static class Factory implements ModelLoaderFactory<Badge, InputStream> {
private final OkHttpClient client;
private Factory(@NonNull OkHttpClient client) {
this.client = client;
}
@Override
public @NonNull ModelLoader<Badge, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
return new BadgeLoader(client);
}
@Override
public void teardown() {
}
}
}

View File

@@ -1,63 +0,0 @@
package org.thoughtcrime.securesms.glide;
import android.content.Context;
import androidx.annotation.NonNull;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.data.DataFetcher;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
class ContactPhotoFetcher implements DataFetcher<InputStream> {
private final Context context;
private final ContactPhoto contactPhoto;
private InputStream inputStream;
ContactPhotoFetcher(@NonNull Context context, @NonNull ContactPhoto contactPhoto) {
this.context = context.getApplicationContext();
this.contactPhoto = contactPhoto;
}
@Override
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
try {
inputStream = contactPhoto.openInputStream(context);
callback.onDataReady(inputStream);
} catch (FileNotFoundException e) {
callback.onDataReady(null);
} catch (IOException e) {
callback.onLoadFailed(e);
}
}
@Override
public void cleanup() {
try {
if (inputStream != null) inputStream.close();
} catch (IOException e) {}
}
@Override
public void cancel() {
}
@Override
public @NonNull Class<InputStream> getDataClass() {
return InputStream.class;
}
@Override
public @NonNull DataSource getDataSource() {
return DataSource.LOCAL;
}
}

View File

@@ -1,51 +0,0 @@
package org.thoughtcrime.securesms.glide;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.load.Options;
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.contacts.avatars.ContactPhoto;
import java.io.InputStream;
public class ContactPhotoLoader implements ModelLoader<ContactPhoto, InputStream> {
private final Context context;
private ContactPhotoLoader(Context context) {
this.context = context;
}
@Override
public @Nullable LoadData<InputStream> buildLoadData(@NonNull ContactPhoto contactPhoto, int width, int height, @NonNull Options options) {
return new LoadData<>(contactPhoto, new ContactPhotoFetcher(context, contactPhoto));
}
@Override
public boolean handles(@NonNull ContactPhoto contactPhoto) {
return true;
}
public static class Factory implements ModelLoaderFactory<ContactPhoto, InputStream> {
private final Context context;
public Factory(Context context) {
this.context = context.getApplicationContext();
}
@Override
public @NonNull ModelLoader<ContactPhoto, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
return new ContactPhotoLoader(context);
}
@Override
public void teardown() {}
}
}

View File

@@ -1,97 +0,0 @@
package org.thoughtcrime.securesms.glide
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.Key
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import okhttp3.OkHttpClient
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation
import org.thoughtcrime.securesms.components.settings.app.subscription.getBadge
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.dependencies.AppDependencies
import java.io.InputStream
import java.security.MessageDigest
import java.util.Locale
/**
* Glide Model allowing the direct loading of a GiftBadge.
*
* This model will first resolve a GiftBadge into a Badge, and then it will delegate to the Badge loader.
*/
data class GiftBadgeModel(val giftBadge: GiftBadge) : Key {
class Loader(val client: OkHttpClient) : ModelLoader<GiftBadgeModel, InputStream> {
override fun buildLoadData(model: GiftBadgeModel, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream>? {
return ModelLoader.LoadData(model, Fetcher(client, model))
}
override fun handles(model: GiftBadgeModel): Boolean = true
}
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(giftBadge.encode())
}
class Fetcher(
private val client: OkHttpClient,
private val giftBadge: GiftBadgeModel
) : DataFetcher<InputStream> {
private var okHttpStreamFetcher: OkHttpStreamFetcher? = null
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
try {
val receiptCredentialPresentation = ReceiptCredentialPresentation(giftBadge.giftBadge.redemptionToken.toByteArray())
val giftBadgeResponse = AppDependencies.donationsService.getDonationsConfiguration(Locale.getDefault())
if (giftBadgeResponse.result.isPresent) {
val badge = giftBadgeResponse.result.get().getBadge(receiptCredentialPresentation.receiptLevel.toInt())
okHttpStreamFetcher = OkHttpStreamFetcher(client, GlideUrl(badge.imageUrl.toString()))
okHttpStreamFetcher?.loadData(priority, callback)
} else if (giftBadgeResponse.applicationError.isPresent) {
callback.onLoadFailed(Exception(giftBadgeResponse.applicationError.get()))
} else if (giftBadgeResponse.executionError.isPresent) {
callback.onLoadFailed(Exception(giftBadgeResponse.executionError.get()))
} else {
callback.onLoadFailed(Exception("No result or error in service response."))
}
} catch (e: Exception) {
callback.onLoadFailed(e)
}
}
override fun cleanup() {
okHttpStreamFetcher?.cleanup()
}
override fun cancel() {
okHttpStreamFetcher?.cancel()
}
override fun getDataClass(): Class<InputStream> {
return InputStream::class.java
}
override fun getDataSource(): DataSource {
return DataSource.REMOTE
}
}
class Factory(private val client: OkHttpClient) : ModelLoaderFactory<GiftBadgeModel, InputStream> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<GiftBadgeModel, InputStream> {
return Loader(client)
}
override fun teardown() {}
}
companion object {
@JvmStatic
fun createFactory(): Factory {
return Factory(AppDependencies.signalOkHttpClient)
}
}
}

View File

@@ -1,53 +0,0 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.glide
import android.app.ActivityManager
import android.content.Context
import org.signal.core.util.ByteSize
import org.signal.core.util.bytes
import org.signal.core.util.gibiBytes
import org.signal.core.util.mebiBytes
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.glide.GlideStreamConfig.MAX_MARK_LIMIT
import org.thoughtcrime.securesms.glide.GlideStreamConfig.MIN_MARK_LIMIT
object GlideStreamConfig {
private val MIN_MARK_LIMIT: ByteSize = 5.mebiBytes // Glide default
private val MAX_MARK_LIMIT: ByteSize = 8.mebiBytes
private val LOW_MEMORY_THRESHOLD: ByteSize = 4.gibiBytes
private val HIGH_MEMORY_THRESHOLD: ByteSize = 12.gibiBytes
@JvmStatic
val markReadLimitBytes: Int by lazy { calculateScaledMarkLimit(context = AppDependencies.application).inWholeBytes.toInt() }
/**
* Calculates buffer size, scaling proportionally from [MIN_MARK_LIMIT] to [MAX_MARK_LIMIT] based on how much memory the device has.
*/
private fun calculateScaledMarkLimit(context: Context): ByteSize {
val deviceMemory = getAvailableDeviceMemory(context)
return when {
deviceMemory <= LOW_MEMORY_THRESHOLD -> MIN_MARK_LIMIT
deviceMemory >= HIGH_MEMORY_THRESHOLD -> MAX_MARK_LIMIT
else -> calculateScaledSize(deviceMemory)
}
}
private fun getAvailableDeviceMemory(context: Context): ByteSize {
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memoryInfo = ActivityManager.MemoryInfo().apply {
activityManager.getMemoryInfo(this)
}
return memoryInfo.totalMem.bytes
}
private fun calculateScaledSize(deviceMemory: ByteSize): ByteSize {
val ratio: Float = (deviceMemory - LOW_MEMORY_THRESHOLD).percentageOf(HIGH_MEMORY_THRESHOLD - LOW_MEMORY_THRESHOLD)
val offsetBytes = (ratio * (MAX_MARK_LIMIT.inWholeBytes - MIN_MARK_LIMIT.inWholeBytes)).toLong()
return MIN_MARK_LIMIT + ByteSize(offsetBytes)
}
}

View File

@@ -1,351 +0,0 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*
* This file is adapted from the Glide image loading library.
* Original work Copyright 2014 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thoughtcrime.securesms.glide;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import androidx.annotation.GuardedBy;
import androidx.annotation.VisibleForTesting;
import com.bumptech.glide.util.Util;
import org.signal.core.util.logging.Log;
import java.io.File;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* State and constants for interacting with {@link android.graphics.Bitmap.Config#HARDWARE} on Android O+.
*/
public final class HardwareConfigState {
private static final String TAG = "HardwareConfig";
private static final boolean enableVerboseLogging = false;
/**
* Force the state to wait until a call to allow hardware Bitmaps to be used when they'd otherwise
* be eligible to work around a framework issue pre Q that can cause a native crash when
* allocating a hardware Bitmap in this specific circumstance. See b/126573603#comment12 for
* details.
*/
public static final boolean BLOCK_HARDWARE_BITMAPS_WHEN_GL_CONTEXT_MIGHT_NOT_BE_INITIALIZED =
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q;
/**
* Support for the hardware bitmap config was added in Android O.
*/
public static final boolean HARDWARE_BITMAPS_SUPPORTED =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
/**
* The minimum size in pixels a {@link Bitmap} must be in both dimensions to be created with the
* {@link Bitmap.Config#HARDWARE} configuration.
*
* <p>This is a quick check that lets us skip wasting FDs (see {@link #FD_SIZE_LIST}) on small
* {@link Bitmap}s with relatively low memory costs.
*
* @see #FD_SIZE_LIST
*/
@VisibleForTesting static final int MIN_HARDWARE_DIMENSION_O = 128;
private static final int MIN_HARDWARE_DIMENSION_P = 0;
/**
* Allows us to check to make sure we're not exceeding the FD limit for a process with hardware
* {@link Bitmap}s.
*
* <p>{@link Bitmap.Config#HARDWARE} {@link Bitmap}s require two FDs (depending on the driver).
* Processes have an FD limit of 1024 (at least on O). With sufficiently small {@link Bitmap}s
* and/or a sufficiently large {@link com.bumptech.glide.load.engine.cache.MemoryCache}, we can
* end up with enough {@link Bitmap}s in memory that we blow through the FD limit, which causes
* graphics errors, Binder errors, and a variety of crashes.
*
* <p>Calling list.size() should be relatively efficient (hopefully < 1ms on average) because
* /proc is an in-memory FS.
*/
private static final File FD_SIZE_LIST = new File("/proc/self/fd");
/**
* Each FD check takes 1-2ms, so to avoid overhead, only check every N decodes. 50 is more or less
* arbitrary.
*/
private static final int MINIMUM_DECODES_BETWEEN_FD_CHECKS = 50;
/**
* 700 with an error of 50 Bitmaps in between at two FDs each lets us use up to 800 FDs for
* hardware Bitmaps.
*
* <p>Prior to P, the limit per process was 1024 FDs. In P, the limit was updated to 32k FDs per
* process.
*
* <p>Access to this variable will be removed in a future version without deprecation.
*/
private static final int MAXIMUM_FDS_FOR_HARDWARE_CONFIGS_O = 700;
// 20k.
private static final int MAXIMUM_FDS_FOR_HARDWARE_CONFIGS_P = 20000;
/**
* This constant will be removed in a future version without deprecation, avoid using it.
*/
public static final int NO_MAX_FD_COUNT = -1;
private static volatile HardwareConfigState instance;
@SuppressWarnings("FieldMayBeFinal")
private static volatile int manualOverrideMaxFdCount = NO_MAX_FD_COUNT;
private final boolean isHardwareConfigAllowedByDeviceModel;
private final int sdkBasedMaxFdCount;
private final int minHardwareDimension;
@GuardedBy("this")
private int decodesSinceLastFdCheck;
@GuardedBy("this")
private boolean isFdSizeBelowHardwareLimit = true;
/**
* Only mutated on the main thread. Read by any number of background threads concurrently.
*
* <p>Defaults to {@code false} because we need to wait for the GL context to be initialized and
* it defaults to not initialized (<a href="https://b.corp.google.com/issues/126573603#comment12">https://b.corp.google.com/issues/126573603#comment12</a>).
*/
private final AtomicBoolean isHardwareConfigAllowedByAppState = new AtomicBoolean(false);
public static HardwareConfigState getInstance() {
if (instance == null) {
synchronized (HardwareConfigState.class) {
if (instance == null) {
instance = new HardwareConfigState();
}
}
}
return instance;
}
@VisibleForTesting HardwareConfigState() {
isHardwareConfigAllowedByDeviceModel = isHardwareConfigAllowedByDeviceModel();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
sdkBasedMaxFdCount = MAXIMUM_FDS_FOR_HARDWARE_CONFIGS_P;
minHardwareDimension = MIN_HARDWARE_DIMENSION_P;
} else {
sdkBasedMaxFdCount = MAXIMUM_FDS_FOR_HARDWARE_CONFIGS_O;
minHardwareDimension = MIN_HARDWARE_DIMENSION_O;
}
}
public boolean areHardwareBitmapsBlocked() {
Util.assertMainThread();
return !isHardwareConfigAllowedByAppState.get();
}
public void blockHardwareBitmaps() {
Util.assertMainThread();
isHardwareConfigAllowedByAppState.set(false);
}
public void unblockHardwareBitmaps() {
Util.assertMainThread();
isHardwareConfigAllowedByAppState.set(true);
}
public boolean isHardwareConfigAllowed(
int targetWidth,
int targetHeight,
boolean isHardwareConfigAllowed,
boolean isExifOrientationRequired)
{
if (!isHardwareConfigAllowed) {
if (enableVerboseLogging) {
Log.v(TAG, "Hardware config disallowed by caller");
}
return false;
}
if (!isHardwareConfigAllowedByDeviceModel) {
if (enableVerboseLogging) {
Log.v(TAG, "Hardware config disallowed by device model");
}
return false;
}
if (!HARDWARE_BITMAPS_SUPPORTED) {
if (enableVerboseLogging) {
Log.v(TAG, "Hardware config disallowed by sdk");
}
return false;
}
if (areHardwareBitmapsBlockedByAppState()) {
if (enableVerboseLogging) {
Log.v(TAG, "Hardware config disallowed by app state");
}
return false;
}
if (isExifOrientationRequired) {
if (enableVerboseLogging) {
Log.v(TAG, "Hardware config disallowed because exif orientation is required");
}
return false;
}
if (targetWidth < minHardwareDimension) {
if (enableVerboseLogging) {
Log.v(TAG, "Hardware config disallowed because width is too small");
}
return false;
}
if (targetHeight < minHardwareDimension) {
if (enableVerboseLogging) {
Log.v(TAG, "Hardware config disallowed because height is too small");
}
return false;
}
// Make sure to call isFdSizeBelowHardwareLimit last because it has side affects.
if (!isFdSizeBelowHardwareLimit()) {
if (enableVerboseLogging) {
Log.v(TAG, "Hardware config disallowed because there are insufficient FDs");
}
return false;
}
return true;
}
private boolean areHardwareBitmapsBlockedByAppState() {
return BLOCK_HARDWARE_BITMAPS_WHEN_GL_CONTEXT_MIGHT_NOT_BE_INITIALIZED
&& !isHardwareConfigAllowedByAppState.get();
}
@TargetApi(Build.VERSION_CODES.O)
boolean setHardwareConfigIfAllowed(
int targetWidth,
int targetHeight,
BitmapFactory.Options optionsWithScaling,
boolean isHardwareConfigAllowed,
boolean isExifOrientationRequired)
{
boolean result =
isHardwareConfigAllowed(
targetWidth, targetHeight, isHardwareConfigAllowed, isExifOrientationRequired);
if (result) {
optionsWithScaling.inPreferredConfig = Bitmap.Config.HARDWARE;
optionsWithScaling.inMutable = false;
}
return result;
}
private static boolean isHardwareConfigAllowedByDeviceModel() {
return !isHardwareConfigDisallowedByB112551574() && !isHardwareConfigDisallowedByB147430447();
}
private static boolean isHardwareConfigDisallowedByB147430447() {
if (Build.VERSION.SDK_INT != Build.VERSION_CODES.O_MR1) {
return false;
}
// This method will only be called once, so simple iteration is reasonable.
return Arrays.asList(
"LG-M250",
"LG-M320",
"LG-Q710AL",
"LG-Q710PL",
"LGM-K121K",
"LGM-K121L",
"LGM-K121S",
"LGM-X320K",
"LGM-X320L",
"LGM-X320S",
"LGM-X401L",
"LGM-X401S",
"LM-Q610.FG",
"LM-Q610.FGN",
"LM-Q617.FG",
"LM-Q617.FGN",
"LM-Q710.FG",
"LM-Q710.FGN",
"LM-X220PM",
"LM-X220QMA",
"LM-X410PM")
.contains(Build.MODEL);
}
private static boolean isHardwareConfigDisallowedByB112551574() {
if (Build.VERSION.SDK_INT != Build.VERSION_CODES.O) {
return false;
}
// This method will only be called once, so simple iteration is reasonable.
for (String prefixOrModelName :
// This is sadly a list of prefixes, not models. We no longer have the data that shows us
// all the explicit models, so we have to live with the prefixes.
Arrays.asList(
// Samsung
"SC-04J",
"SM-N935",
"SM-J720",
"SM-G570F",
"SM-G570M",
"SM-G960",
"SM-G965",
"SM-G935",
"SM-G930",
"SM-A520",
"SM-A720F",
// Moto
"moto e5",
"moto e5 play",
"moto e5 plus",
"moto e5 cruise",
"moto g(6) forge",
"moto g(6) play")) {
if (Build.MODEL.startsWith(prefixOrModelName)) {
return true;
}
}
return false;
}
private int getMaxFdCount() {
return manualOverrideMaxFdCount != NO_MAX_FD_COUNT
? manualOverrideMaxFdCount
: sdkBasedMaxFdCount;
}
@SuppressLint("LogTagInlined")
private synchronized boolean isFdSizeBelowHardwareLimit() {
if (++decodesSinceLastFdCheck >= MINIMUM_DECODES_BETWEEN_FD_CHECKS) {
decodesSinceLastFdCheck = 0;
int currentFds = Objects.requireNonNull(FD_SIZE_LIST.list()).length;
long maxFdCount = getMaxFdCount();
isFdSizeBelowHardwareLimit = currentFds < maxFdCount;
if (!isFdSizeBelowHardwareLimit && enableVerboseLogging) {
Log.w(
Downsampler.TAG,
"Excluding HARDWARE bitmap config because we're over the file descriptor limit"
+ ", file descriptors "
+ currentFds
+ ", limit "
+ maxFdCount);
}
}
return isFdSizeBelowHardwareLimit;
}
}

View File

@@ -1,189 +0,0 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.glide
import androidx.exifinterface.media.ExifInterface
import com.bumptech.glide.load.ImageHeaderParser
import com.bumptech.glide.load.data.ParcelFileDescriptorRewinder
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool
import com.bumptech.glide.load.resource.bitmap.RecyclableBufferedInputStream
import org.thoughtcrime.securesms.mms.InputStreamFactory
import org.thoughtcrime.securesms.util.BitmapUtil
import java.io.IOException
import java.io.InputStream
import java.nio.ByteBuffer
typealias GlideImageHeaderParserUtils = com.bumptech.glide.load.ImageHeaderParserUtils
object ImageHeaderParserUtils {
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getType
*/
@JvmStatic
@Throws(IOException::class)
fun getType(
parsers: List<ImageHeaderParser>,
inputStream: InputStream?,
byteArrayPool: ArrayPool
): ImageHeaderParser.ImageType {
if (inputStream == null) {
return ImageHeaderParser.ImageType.UNKNOWN
}
val markableStream = if (!inputStream.markSupported()) {
RecyclableBufferedInputStream(inputStream, byteArrayPool)
} else {
inputStream
}
markableStream.mark(GlideStreamConfig.markReadLimitBytes)
return getType(
parsers = parsers,
getTypeAndRewind = { parser ->
try {
parser.getType(markableStream)
} finally {
markableStream.reset()
}
}
)
}
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getType
*/
@JvmStatic
@Throws(IOException::class)
fun getType(
parsers: List<ImageHeaderParser>,
buffer: ByteBuffer
): ImageHeaderParser.ImageType {
return GlideImageHeaderParserUtils.getType(parsers, buffer)
}
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getType
*/
@JvmStatic
@Throws(IOException::class)
fun getType(
parsers: List<ImageHeaderParser>,
parcelFileDescriptorRewinder: ParcelFileDescriptorRewinder,
byteArrayPool: ArrayPool
): ImageHeaderParser.ImageType {
return GlideImageHeaderParserUtils.getType(parsers, parcelFileDescriptorRewinder, byteArrayPool)
}
private fun getType(
parsers: List<ImageHeaderParser>,
getTypeAndRewind: (ImageHeaderParser) -> ImageHeaderParser.ImageType
): ImageHeaderParser.ImageType {
return parsers.firstNotNullOfOrNull { parser ->
getTypeAndRewind(parser)
.takeIf { type -> type != ImageHeaderParser.ImageType.UNKNOWN }
} ?: ImageHeaderParser.ImageType.UNKNOWN
}
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getOrientation
*/
@JvmStatic
@Throws(IOException::class)
fun getOrientationWithFallbacks(
parsers: List<ImageHeaderParser>,
buffer: ByteBuffer,
arrayPool: ArrayPool
): Int {
return GlideImageHeaderParserUtils.getOrientation(parsers, buffer, arrayPool)
}
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getOrientation
*/
@JvmStatic
@Throws(IOException::class)
fun getOrientation(
parsers: List<ImageHeaderParser>,
parcelFileDescriptorRewinder: ParcelFileDescriptorRewinder,
byteArrayPool: ArrayPool
): Int {
return GlideImageHeaderParserUtils.getOrientation(parsers, parcelFileDescriptorRewinder, byteArrayPool)
}
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getOrientation
*/
@JvmStatic
@Throws(IOException::class)
fun getOrientation(
parsers: List<ImageHeaderParser>,
inputStream: InputStream?,
byteArrayPool: ArrayPool,
allowStreamRewind: Boolean
): Int {
if (inputStream == null) {
return ImageHeaderParser.UNKNOWN_ORIENTATION
}
val markableStream = if (allowStreamRewind && !inputStream.markSupported()) {
RecyclableBufferedInputStream(inputStream, byteArrayPool)
} else {
inputStream
}
if (allowStreamRewind) {
markableStream.mark(GlideStreamConfig.markReadLimitBytes)
}
return getOrientation(
parsers = parsers,
getOrientationAndRewind = { parser ->
try {
parser.getOrientation(markableStream, byteArrayPool)
} finally {
if (allowStreamRewind) {
markableStream.reset()
}
}
}
)
}
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getOrientation
*/
@JvmStatic
@Throws(IOException::class)
fun getOrientationWithFallbacks(
parsers: List<ImageHeaderParser>,
inputStreamFactory: InputStreamFactory,
byteArrayPool: ArrayPool
): Int {
val orientationFromParsers = getOrientation(
parsers = parsers,
inputStream = inputStreamFactory.createRecyclable(byteArrayPool),
byteArrayPool = byteArrayPool,
allowStreamRewind = false
)
if (orientationFromParsers != ImageHeaderParser.UNKNOWN_ORIENTATION) return orientationFromParsers
val orientationFromExif = BitmapUtil.getExifOrientation(ExifInterface(inputStreamFactory.createRecyclable(byteArrayPool)))
if (orientationFromExif != ImageHeaderParser.UNKNOWN_ORIENTATION) return orientationFromExif
return ImageHeaderParser.UNKNOWN_ORIENTATION
}
private fun getOrientation(
parsers: List<ImageHeaderParser>,
getOrientationAndRewind: (ImageHeaderParser) -> Int
): Int {
return parsers.firstNotNullOfOrNull { parser ->
getOrientationAndRewind(parser)
.takeIf { type -> type != ImageHeaderParser.UNKNOWN_ORIENTATION }
} ?: ImageHeaderParser.UNKNOWN_ORIENTATION
}
}

View File

@@ -1,261 +0,0 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.glide;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.bumptech.glide.load.ImageHeaderParser;
import com.bumptech.glide.load.ImageHeaderParser.ImageType;
import com.bumptech.glide.load.data.DataRewinder;
import com.bumptech.glide.load.data.ParcelFileDescriptorRewinder;
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import com.bumptech.glide.load.resource.bitmap.RecyclableBufferedInputStream;
import com.bumptech.glide.util.ByteBufferUtil;
import com.bumptech.glide.util.Preconditions;
import org.thoughtcrime.securesms.mms.InputStreamFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.List;
/**
* This is a helper class for {@link Downsampler} that abstracts out image operations from the input
* type wrapped into a {@link DataRewinder}.
*/
interface ImageReader {
@Nullable
Bitmap decodeBitmap(BitmapFactory.Options options) throws IOException;
ImageHeaderParser.ImageType getImageType() throws IOException;
int getImageOrientation() throws IOException;
void stopGrowingBuffers();
final class ByteArrayReader implements ImageReader {
private final byte[] bytes;
private final List<ImageHeaderParser> parsers;
private final ArrayPool byteArrayPool;
ByteArrayReader(byte[] bytes, List<ImageHeaderParser> parsers, ArrayPool byteArrayPool) {
this.bytes = bytes;
this.parsers = parsers;
this.byteArrayPool = byteArrayPool;
}
@Nullable
@Override
public Bitmap decodeBitmap(Options options) {
return BitmapFactory.decodeByteArray(bytes, /* offset= */ 0, bytes.length, options);
}
@Override
public ImageType getImageType() throws IOException {
return ImageHeaderParserUtils.getType(parsers, ByteBuffer.wrap(bytes));
}
@Override
public int getImageOrientation() throws IOException {
return ImageHeaderParserUtils.getOrientationWithFallbacks(parsers, ByteBuffer.wrap(bytes), byteArrayPool);
}
@Override
public void stopGrowingBuffers() {}
}
final class FileReader implements ImageReader {
private final File file;
private final List<ImageHeaderParser> parsers;
private final ArrayPool byteArrayPool;
FileReader(File file, List<ImageHeaderParser> parsers, ArrayPool byteArrayPool) {
this.file = file;
this.parsers = parsers;
this.byteArrayPool = byteArrayPool;
}
@Nullable
@Override
public Bitmap decodeBitmap(Options options) throws FileNotFoundException {
InputStream is = null;
try {
is = new RecyclableBufferedInputStream(new FileInputStream(file), byteArrayPool);
return BitmapFactory.decodeStream(is, /* outPadding= */ null, options);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
// Ignored.
}
}
}
}
@Override
public ImageType getImageType() throws IOException {
InputStream is = null;
try {
is = new RecyclableBufferedInputStream(new FileInputStream(file), byteArrayPool);
return ImageHeaderParserUtils.getType(parsers, is, byteArrayPool);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
// Ignored.
}
}
}
}
@Override
public int getImageOrientation() throws IOException {
return ImageHeaderParserUtils.getOrientationWithFallbacks(parsers, InputStreamFactory.build(file), byteArrayPool);
}
@Override
public void stopGrowingBuffers() {}
}
final class ByteBufferReader implements ImageReader {
private final ByteBuffer buffer;
private final List<ImageHeaderParser> parsers;
private final ArrayPool byteArrayPool;
ByteBufferReader(ByteBuffer buffer, List<ImageHeaderParser> parsers, ArrayPool byteArrayPool) {
this.buffer = buffer;
this.parsers = parsers;
this.byteArrayPool = byteArrayPool;
}
@Nullable
@Override
public Bitmap decodeBitmap(Options options) {
return BitmapFactory.decodeStream(stream(), /* outPadding= */ null, options);
}
@Override
public ImageType getImageType() throws IOException {
return ImageHeaderParserUtils.getType(parsers, ByteBufferUtil.rewind(buffer));
}
@Override
public int getImageOrientation() throws IOException {
return ImageHeaderParserUtils.getOrientationWithFallbacks(parsers, ByteBufferUtil.rewind(buffer), byteArrayPool);
}
@Override
public void stopGrowingBuffers() {}
private InputStream stream() {
return ByteBufferUtil.toStream(ByteBufferUtil.rewind(buffer));
}
}
final class InputStreamImageReader implements ImageReader {
private final InputStreamRewinder dataRewinder;
private final ArrayPool byteArrayPool;
private final List<ImageHeaderParser> parsers;
private final InputStreamFactory inputStreamFactory;
InputStreamImageReader(@NonNull InputStreamFactory inputStreamFactory, List<ImageHeaderParser> parsers, ArrayPool byteArrayPool) {
this.byteArrayPool = Preconditions.checkNotNull(byteArrayPool);
this.parsers = Preconditions.checkNotNull(parsers);
this.inputStreamFactory = inputStreamFactory;
this.dataRewinder = new InputStreamRewinder(inputStreamFactory.create(), byteArrayPool);
}
@Nullable
@Override
public Bitmap decodeBitmap(@NonNull BitmapFactory.Options options) {
try {
return BitmapFactory.decodeStream(dataRewinder.rewindAndGet(), null, options);
} catch (IOException e) {
return BitmapFactory.decodeStream(inputStreamFactory.createRecyclable(byteArrayPool), null, options);
}
}
@Override
public ImageHeaderParser.ImageType getImageType() throws IOException {
try {
return ImageHeaderParserUtils.getType(parsers, dataRewinder.rewindAndGet(), byteArrayPool);
} catch (IOException e) {
return ImageHeaderParserUtils.getType(parsers, inputStreamFactory.createRecyclable(byteArrayPool), byteArrayPool);
}
}
@Override
public int getImageOrientation() throws IOException {
try {
return ImageHeaderParserUtils.getOrientation(parsers, dataRewinder.rewindAndGet(), byteArrayPool, true);
} catch (IOException e) {
return ImageHeaderParserUtils.getOrientationWithFallbacks(parsers, inputStreamFactory, byteArrayPool);
}
}
@Override
public void stopGrowingBuffers() {
dataRewinder.fixMarkLimits();
}
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) final class ParcelFileDescriptorImageReader implements ImageReader {
private final ArrayPool byteArrayPool;
private final List<ImageHeaderParser> parsers;
private final ParcelFileDescriptorRewinder dataRewinder;
ParcelFileDescriptorImageReader(
ParcelFileDescriptor parcelFileDescriptor,
List<ImageHeaderParser> parsers,
ArrayPool byteArrayPool)
{
this.byteArrayPool = Preconditions.checkNotNull(byteArrayPool);
this.parsers = Preconditions.checkNotNull(parsers);
dataRewinder = new ParcelFileDescriptorRewinder(parcelFileDescriptor);
}
@Nullable
@Override
public Bitmap decodeBitmap(BitmapFactory.Options options) throws IOException {
return BitmapFactory.decodeFileDescriptor(
dataRewinder.rewindAndGet().getFileDescriptor(), null, options);
}
@Override
public ImageHeaderParser.ImageType getImageType() throws IOException {
return ImageHeaderParserUtils.getType(parsers, dataRewinder, byteArrayPool);
}
@Override
public int getImageOrientation() throws IOException {
return ImageHeaderParserUtils.getOrientation(parsers, dataRewinder, byteArrayPool);
}
@Override
public void stopGrowingBuffers() {
// Nothing to do here.
}
}
}

View File

@@ -1,84 +0,0 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*
* This file is adapted from the Glide image loading library.
* Original work Copyright 2014 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thoughtcrime.securesms.glide;
import androidx.annotation.NonNull;
import com.bumptech.glide.load.data.DataRewinder;
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import com.bumptech.glide.load.resource.bitmap.RecyclableBufferedInputStream;
import com.bumptech.glide.util.Synthetic;
import java.io.IOException;
import java.io.InputStream;
/**
* Implementation for {@link InputStream}s that rewinds streams by wrapping them in a buffered stream. This is a copy of
* {@link com.bumptech.glide.load.data.InputStreamRewinder that is modified to use GlideStreamConfig.markReadLimitBytes in place of a hardcoded MARK_READ_LIMIT.
*/
public final class InputStreamRewinder implements DataRewinder<InputStream> {
private final RecyclableBufferedInputStream bufferedStream;
@Synthetic
public InputStreamRewinder(InputStream is, ArrayPool byteArrayPool) {
// We don't check is.markSupported() here because RecyclableBufferedInputStream allows resetting
// after exceeding GlideStreamConfig.markReadLimitBytes, which other InputStreams don't guarantee.
bufferedStream = new RecyclableBufferedInputStream(is, byteArrayPool);
bufferedStream.mark(GlideStreamConfig.getMarkReadLimitBytes());
}
@NonNull
@Override
public InputStream rewindAndGet() throws IOException {
bufferedStream.reset();
return bufferedStream;
}
@Override
public void cleanup() {
bufferedStream.release();
}
public void fixMarkLimits() {
bufferedStream.fixMarkLimit();
}
/**
* Factory for producing {@link InputStreamRewinder}s from {@link InputStream}s.
*/
public static final class Factory implements DataRewinder.Factory<InputStream> {
private final ArrayPool byteArrayPool;
public Factory(ArrayPool byteArrayPool) {
this.byteArrayPool = byteArrayPool;
}
@NonNull
@Override
public DataRewinder<InputStream> build(@NonNull InputStream data) {
return new InputStreamRewinder(data, byteArrayPool);
}
@NonNull
@Override
public Class<InputStream> getDataClass() {
return InputStream.class;
}
}
}

View File

@@ -1,3 +1,8 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.glide;
import androidx.annotation.NonNull;
@@ -8,8 +13,6 @@ import com.bumptech.glide.load.data.DataFetcher;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.util.ContentLengthInputStream;
import org.signal.core.util.logging.Log;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
@@ -22,16 +25,14 @@ import okhttp3.ResponseBody;
/**
* Fetches an {@link InputStream} using the okhttp library.
*/
class OkHttpStreamFetcher implements DataFetcher<InputStream> {
private static final String TAG = Log.tag(OkHttpStreamFetcher.class);
public class OkHttpStreamFetcher implements DataFetcher<InputStream> {
private final OkHttpClient client;
private final GlideUrl url;
private InputStream stream;
private ResponseBody responseBody;
OkHttpStreamFetcher(OkHttpClient client, GlideUrl url) {
public OkHttpStreamFetcher(OkHttpClient client, GlideUrl url) {
this.client = client;
this.url = url;
}

View File

@@ -0,0 +1,114 @@
package org.thoughtcrime.securesms.glide;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import com.bumptech.glide.Glide;
import com.bumptech.glide.Registry;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.load.model.UnitModelLoader;
import com.bumptech.glide.load.resource.bitmap.BitmapDrawableEncoder;
import com.bumptech.glide.load.resource.gif.ByteBufferGifDecoder;
import com.bumptech.glide.load.resource.gif.GifDrawable;
import com.bumptech.glide.load.resource.gif.StreamGifDecoder;
import org.signal.glide.common.io.InputStreamFactory;
import org.signal.glide.load.resource.apng.decode.APNGDecoder;
import org.thoughtcrime.securesms.badges.load.BadgeLoader;
import org.thoughtcrime.securesms.badges.load.GiftBadgeModel;
import org.thoughtcrime.securesms.badges.models.Badge;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.blurhash.BlurHashModelLoader;
import org.thoughtcrime.securesms.blurhash.BlurHashResourceDecoder;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhotoLoader;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
import org.thoughtcrime.securesms.glide.cache.ApngFrameDrawableTranscoder;
import org.thoughtcrime.securesms.glide.cache.ByteBufferApngDecoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedApngCacheEncoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapResourceEncoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedCacheDecoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedCacheEncoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedGifDrawableResourceEncoder;
import org.thoughtcrime.securesms.glide.cache.InputStreamFactoryBitmapDecoder;
import org.thoughtcrime.securesms.glide.cache.StreamApngDecoder;
import org.thoughtcrime.securesms.glide.cache.StreamBitmapDecoder;
import org.thoughtcrime.securesms.glide.cache.StreamFactoryApngDecoder;
import org.thoughtcrime.securesms.glide.cache.StreamFactoryGifDecoder;
import org.thoughtcrime.securesms.glide.cache.WebpSanDecoder;
import org.thoughtcrime.securesms.mms.DecryptableUri;
import org.thoughtcrime.securesms.mms.DecryptableUriStreamLoader;
import org.thoughtcrime.securesms.mms.RegisterGlideComponents;
import org.thoughtcrime.securesms.mms.SignalGlideModule;
import org.thoughtcrime.securesms.stickers.StickerRemoteUri;
import org.thoughtcrime.securesms.stickers.StickerRemoteUriLoader;
import org.thoughtcrime.securesms.stories.StoryTextPostModel;
import org.thoughtcrime.securesms.util.ConversationShortcutPhoto;
import java.io.File;
import java.io.InputStream;
import java.nio.ByteBuffer;
/**
* The core logic for {@link SignalGlideModule}. This is a separate class because it uses
* dependencies defined in the main Gradle module.
*/
public class SignalGlideComponents implements RegisterGlideComponents {
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
byte[] secret = attachmentSecret.getModernKey();
registry.prepend(File.class, File.class, UnitModelLoader.Factory.getInstance());
registry.prepend(InputStream.class, Bitmap.class, new WebpSanDecoder());
registry.prepend(InputStream.class, new EncryptedCacheEncoder(secret, glide.getArrayPool()));
registry.prepend(File.class, Bitmap.class, new EncryptedCacheDecoder<>(secret, new StreamBitmapDecoder(context, glide, registry)));
StreamGifDecoder streamGifDecoder = new StreamGifDecoder(registry.getImageHeaderParsers(), new ByteBufferGifDecoder(context, registry.getImageHeaderParsers(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool());
StreamFactoryGifDecoder streamFactoryGifDecoder = new StreamFactoryGifDecoder(streamGifDecoder);
registry.prepend(InputStream.class, GifDrawable.class, streamGifDecoder);
registry.prepend(InputStreamFactory.class, GifDrawable.class, streamFactoryGifDecoder);
registry.prepend(GifDrawable.class, new EncryptedGifDrawableResourceEncoder(secret));
registry.prepend(File.class, GifDrawable.class, new EncryptedCacheDecoder<>(secret, streamGifDecoder));
EncryptedBitmapResourceEncoder encryptedBitmapResourceEncoder = new EncryptedBitmapResourceEncoder(secret);
registry.prepend(Bitmap.class, new EncryptedBitmapResourceEncoder(secret));
registry.prepend(BitmapDrawable.class, new BitmapDrawableEncoder(glide.getBitmapPool(), encryptedBitmapResourceEncoder));
ByteBufferApngDecoder byteBufferApngDecoder = new ByteBufferApngDecoder();
StreamApngDecoder streamApngDecoder = new StreamApngDecoder(byteBufferApngDecoder);
StreamFactoryApngDecoder streamFactoryApngDecoder = new StreamFactoryApngDecoder(byteBufferApngDecoder, glide, registry);
registry.prepend(InputStream.class, APNGDecoder.class, streamApngDecoder);
registry.prepend(InputStreamFactory.class, APNGDecoder.class, streamFactoryApngDecoder);
registry.prepend(ByteBuffer.class, APNGDecoder.class, byteBufferApngDecoder);
registry.prepend(APNGDecoder.class, new EncryptedApngCacheEncoder(secret));
registry.prepend(File.class, APNGDecoder.class, new EncryptedCacheDecoder<>(secret, streamApngDecoder));
registry.register(APNGDecoder.class, Drawable.class, new ApngFrameDrawableTranscoder());
registry.prepend(BlurHash.class, Bitmap.class, new BlurHashResourceDecoder());
registry.prepend(StoryTextPostModel.class, Bitmap.class, new StoryTextPostModel.Decoder());
registry.append(StoryTextPostModel.class, StoryTextPostModel.class, UnitModelLoader.Factory.getInstance());
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, InputStreamFactory.class, new DecryptableUriStreamLoader.Factory(context));
registry.append(InputStreamFactory.class, Bitmap.class, new InputStreamFactoryBitmapDecoder(context, glide, registry));
registry.append(ChunkedImageUrl.class, InputStream.class, new ChunkedImageUrlLoader.Factory());
registry.append(StickerRemoteUri.class, InputStream.class, new StickerRemoteUriLoader.Factory());
registry.append(BlurHash.class, BlurHash.class, new BlurHashModelLoader.Factory());
registry.append(Badge.class, InputStream.class, BadgeLoader.createFactory());
registry.append(GiftBadgeModel.class, InputStream.class, GiftBadgeModel.createFactory());
registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
}
}

View File

@@ -10,8 +10,8 @@ import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.drawable.DrawableResource;
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder;
import org.signal.glide.apng.APNGDrawable;
import org.signal.glide.apng.decode.APNGDecoder;
import org.signal.glide.load.resource.apng.APNGDrawable;
import org.signal.glide.load.resource.apng.decode.APNGDecoder;
public class ApngFrameDrawableTranscoder implements ResourceTranscoder<APNGDecoder, Drawable> {

View File

@@ -7,11 +7,11 @@ import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceDecoder;
import com.bumptech.glide.load.engine.Resource;
import org.signal.glide.apng.decode.APNGDecoder;
import org.signal.glide.apng.decode.APNGParser;
import org.signal.glide.common.io.ByteBufferReader;
import org.signal.glide.common.loader.ByteBufferLoader;
import org.signal.glide.common.loader.Loader;
import org.signal.glide.load.resource.apng.decode.APNGDecoder;
import org.signal.glide.load.resource.apng.decode.APNGParser;
import java.io.IOException;
import java.nio.ByteBuffer;

View File

@@ -9,8 +9,8 @@ import com.bumptech.glide.load.engine.Resource;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.logging.Log;
import org.signal.glide.apng.decode.APNGDecoder;
import org.signal.glide.common.loader.Loader;
import org.signal.glide.load.resource.apng.decode.APNGDecoder;
import java.io.File;
import java.io.IOException;

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.glide.cache
import android.content.Context
import android.graphics.Bitmap
import com.bumptech.glide.Glide
import com.bumptech.glide.Registry
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.ResourceDecoder
import com.bumptech.glide.load.engine.Resource
import org.signal.glide.common.io.InputStreamFactory
import org.signal.glide.load.resource.bitmap.Downsampler
/**
* A Glide [ResourceDecoder] that decodes [Bitmap]s from a [InputStreamFactory] instances.
*/
class InputStreamFactoryBitmapDecoder(
private val downsampler: Downsampler
) : ResourceDecoder<InputStreamFactory, Bitmap> {
constructor(
context: Context,
glide: Glide,
registry: Registry
) : this(
downsampler = Downsampler(registry.imageHeaderParsers, context.resources.displayMetrics, glide.bitmapPool, glide.arrayPool)
)
override fun handles(source: InputStreamFactory, options: Options): Boolean = true
override fun decode(source: InputStreamFactory, width: Int, height: Int, options: Options): Resource<Bitmap?>? {
return downsampler.decode(source, width, height, options)
}
}

View File

@@ -8,9 +8,9 @@ import com.bumptech.glide.load.ResourceDecoder;
import com.bumptech.glide.load.engine.Resource;
import org.signal.core.util.StreamUtil;
import org.signal.glide.apng.decode.APNGDecoder;
import org.signal.glide.apng.decode.APNGParser;
import org.signal.glide.common.io.StreamReader;
import org.signal.glide.load.resource.apng.decode.APNGDecoder;
import org.signal.glide.load.resource.apng.decode.APNGParser;
import java.io.IOException;
import java.io.InputStream;

View File

@@ -12,9 +12,9 @@ import com.bumptech.glide.load.Options
import com.bumptech.glide.load.ResourceDecoder
import com.bumptech.glide.load.engine.Resource
import org.signal.core.util.StreamUtil
import org.signal.glide.apng.decode.APNGDecoder
import org.thoughtcrime.securesms.glide.ImageHeaderParserUtils
import org.thoughtcrime.securesms.mms.InputStreamFactory
import org.signal.glide.common.io.InputStreamFactory
import org.signal.glide.load.ImageHeaderParserUtils
import org.signal.glide.load.resource.apng.decode.APNGDecoder
import java.nio.ByteBuffer
/**

View File

@@ -10,7 +10,7 @@ import com.bumptech.glide.load.ResourceDecoder
import com.bumptech.glide.load.engine.Resource
import com.bumptech.glide.load.resource.gif.GifDrawable
import com.bumptech.glide.load.resource.gif.StreamGifDecoder
import org.thoughtcrime.securesms.mms.InputStreamFactory
import org.signal.glide.common.io.InputStreamFactory
/**
* A variant of [StreamGifDecoder] that decodes animated PNGs from [InputStreamFactory] sources.

View File

@@ -0,0 +1,34 @@
package org.thoughtcrime.securesms.glide.targets;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.request.target.BitmapImageViewTarget;
import org.signal.core.util.concurrent.SettableFuture;
public class GlideBitmapListeningTarget extends BitmapImageViewTarget {
private final SettableFuture<Boolean> loaded;
public GlideBitmapListeningTarget(@NonNull ImageView view, @NonNull SettableFuture<Boolean> loaded) {
super(view);
this.loaded = loaded;
}
@Override
protected void setResource(@Nullable Bitmap resource) {
super.setResource(resource);
loaded.set(true);
}
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
super.onLoadFailed(errorDrawable);
loaded.set(true);
}
}

View File

@@ -0,0 +1,42 @@
package org.thoughtcrime.securesms.glide.targets;
import android.graphics.drawable.Drawable;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.request.target.DrawableImageViewTarget;
import org.signal.core.util.concurrent.SettableFuture;
import org.signal.core.util.logging.Log;
public class GlideDrawableListeningTarget extends DrawableImageViewTarget {
private static final String TAG = Log.tag(GlideDrawableListeningTarget.class);
private final SettableFuture<Boolean> loaded;
public GlideDrawableListeningTarget(@NonNull ImageView view, @NonNull SettableFuture<Boolean> loaded) {
super(view);
this.loaded = loaded;
}
@Override
protected void setResource(@Nullable Drawable resource) {
if (resource == null) {
Log.d(TAG, "Loaded null resource");
} else {
Log.d(TAG, "Loaded resource of w " + resource.getIntrinsicWidth() + " by h " + resource.getIntrinsicHeight());
}
super.setResource(resource);
loaded.set(true);
}
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
super.onLoadFailed(errorDrawable);
loaded.set(true);
}
}

View File

@@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.glide;
package org.thoughtcrime.securesms.glide.targets;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;