mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-24 19:00:26 +01:00
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:
committed by
Jeffrey Starke
parent
cc43add7af
commit
47508495ed
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.bumptech.glide.load.data.StreamLocalUriFetcher;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
class DecryptableStreamLocalUriFetcher extends StreamLocalUriFetcher {
|
||||
|
||||
private static final String TAG = Log.tag(DecryptableStreamLocalUriFetcher.class);
|
||||
|
||||
private static final long TOTAL_PIXEL_SIZE_LIMIT = 200_000_000L; // 200 megapixels
|
||||
|
||||
private final Context context;
|
||||
|
||||
DecryptableStreamLocalUriFetcher(Context context, Uri uri) {
|
||||
super(context.getContentResolver(), uri);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream loadResource(Uri uri, ContentResolver contentResolver) throws FileNotFoundException {
|
||||
if (MediaUtil.hasVideoThumbnail(context, uri)) {
|
||||
Bitmap thumbnail = MediaUtil.getVideoThumbnail(context, uri, 1000);
|
||||
|
||||
if (thumbnail != null) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, baos);
|
||||
ByteArrayInputStream thumbnailStream = new ByteArrayInputStream(baos.toByteArray());
|
||||
thumbnail.recycle();
|
||||
return thumbnailStream;
|
||||
}
|
||||
if (PartAuthority.isAttachmentUri(uri) && MediaUtil.isVideoType(PartAuthority.getAttachmentContentType(context, uri))) {
|
||||
try {
|
||||
AttachmentId attachmentId = PartAuthority.requireAttachmentId(uri);
|
||||
Uri thumbnailUri = PartAuthority.getAttachmentThumbnailUri(attachmentId);
|
||||
InputStream thumbStream = PartAuthority.getAttachmentThumbnailStream(context, thumbnailUri);
|
||||
if (thumbStream != null) {
|
||||
return thumbStream;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.i(TAG, "Failed to fetch thumbnail", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (PartAuthority.isBlobUri(uri) && BlobProvider.isSingleUseMemoryBlob(uri)) {
|
||||
return PartAuthority.getAttachmentThumbnailStream(context, uri);
|
||||
} else if (isSafeSize(PartAuthority.getAttachmentThumbnailStream(context, uri))) {
|
||||
return PartAuthority.getAttachmentThumbnailStream(context, uri);
|
||||
} else {
|
||||
throw new IOException("File dimensions are too large!");
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
throw new FileNotFoundException("PartAuthority couldn't load Uri resource.");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSafeSize(InputStream stream) {
|
||||
try {
|
||||
Pair<Integer, Integer> dimensions = BitmapUtil.getDimensions(stream);
|
||||
long totalPixels = (long) dimensions.first * dimensions.second;
|
||||
return totalPixels < TOTAL_PIXEL_SIZE_LIMIT;
|
||||
} catch (BitmapDecodingException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io
|
||||
|
||||
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.signal.glide.common.io.GlideStreamConfig.MAX_MARK_LIMIT
|
||||
import org.signal.glide.common.io.GlideStreamConfig.MIN_MARK_LIMIT
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool
|
||||
import com.bumptech.glide.load.resource.bitmap.RecyclableBufferedInputStream
|
||||
import org.signal.core.util.logging.Log
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
interface InputStreamFactory {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun build(context: Context, uri: Uri): InputStreamFactory = UriInputStreamFactory(context, uri)
|
||||
|
||||
@JvmStatic
|
||||
fun build(file: File): InputStreamFactory = FileInputStreamFactory(file)
|
||||
}
|
||||
|
||||
fun create(): InputStream
|
||||
fun createRecyclable(byteArrayPool: ArrayPool): InputStream = RecyclableBufferedInputStream(create(), byteArrayPool)
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory that creates a new [InputStream] for the given [Uri] each time [create] is called.
|
||||
*/
|
||||
class UriInputStreamFactory(
|
||||
private val context: Context,
|
||||
private val uri: Uri
|
||||
) : InputStreamFactory {
|
||||
companion object {
|
||||
private val TAG = Log.tag(UriInputStreamFactory::class)
|
||||
}
|
||||
|
||||
override fun create(): InputStream {
|
||||
return try {
|
||||
DecryptableStreamLocalUriFetcher(context, uri).loadResource(uri, context.contentResolver)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error creating input stream for URI.", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory that creates a new [InputStream] for the given [File] each time [create] is called.
|
||||
*/
|
||||
class FileInputStreamFactory(
|
||||
private val file: File
|
||||
) : InputStreamFactory {
|
||||
companion object {
|
||||
private val TAG = Log.tag(FileInputStreamFactory::class)
|
||||
}
|
||||
|
||||
override fun create(): InputStream {
|
||||
return try {
|
||||
FileInputStream(file)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error creating input stream for File.", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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.signal.glide.common.io;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.glide.load
|
||||
|
||||
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.signal.glide.common.io.GlideStreamConfig
|
||||
import org.signal.glide.common.io.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
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.signal.glide.transforms
|
||||
package org.signal.glide.load
|
||||
|
||||
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
|
||||
import kotlin.math.max
|
||||
@@ -3,17 +3,17 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng;
|
||||
package org.signal.glide.load.resource.apng;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.signal.glide.apng.decode.APNGDecoder;
|
||||
import org.signal.glide.common.FrameAnimationDrawable;
|
||||
import org.signal.glide.common.decode.FrameSeqDecoder;
|
||||
import org.signal.glide.common.loader.AssetStreamLoader;
|
||||
import org.signal.glide.common.loader.FileLoader;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
import org.signal.glide.common.loader.ResourceStreamLoader;
|
||||
import org.signal.glide.load.resource.apng.decode.APNGDecoder;
|
||||
|
||||
/**
|
||||
* @Description: APNGDrawable
|
||||
@@ -3,9 +3,9 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
package org.signal.glide.load.resource.apng.decode;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.load.resource.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
package org.signal.glide.load.resource.apng.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
@@ -13,12 +13,12 @@ import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.apng.io.APNGWriter;
|
||||
import org.signal.glide.common.decode.Frame;
|
||||
import org.signal.glide.common.decode.FrameSeqDecoder;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
import org.signal.glide.load.resource.apng.io.APNGReader;
|
||||
import org.signal.glide.load.resource.apng.io.APNGWriter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -3,16 +3,16 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
package org.signal.glide.load.resource.apng.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.apng.io.APNGWriter;
|
||||
import org.signal.glide.common.decode.Frame;
|
||||
import org.signal.glide.load.resource.apng.io.APNGReader;
|
||||
import org.signal.glide.load.resource.apng.io.APNGWriter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
@@ -3,13 +3,13 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
package org.signal.glide.load.resource.apng.decode;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.io.StreamReader;
|
||||
import org.signal.glide.load.resource.apng.io.APNGReader;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
@@ -3,11 +3,11 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
package org.signal.glide.load.resource.apng.decode;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.load.resource.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
package org.signal.glide.load.resource.apng.decode;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.load.resource.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
package org.signal.glide.load.resource.apng.decode;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.load.resource.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
package org.signal.glide.load.resource.apng.decode;
|
||||
|
||||
/**
|
||||
* @Description: 作用描述
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
package org.signal.glide.load.resource.apng.decode;
|
||||
|
||||
/**
|
||||
* @Description: 作用描述
|
||||
@@ -3,9 +3,9 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
package org.signal.glide.load.resource.apng.decode;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.load.resource.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
package org.signal.glide.load.resource.apng.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.apng.io.APNGWriter;
|
||||
import org.signal.glide.common.decode.Frame;
|
||||
import org.signal.glide.load.resource.apng.io.APNGReader;
|
||||
import org.signal.glide.load.resource.apng.io.APNGWriter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.io;
|
||||
package org.signal.glide.load.resource.apng.io;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.io;
|
||||
package org.signal.glide.load.resource.apng.io;
|
||||
|
||||
import org.signal.glide.common.io.ByteBufferWriter;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,351 @@
|
||||
/*
|
||||
* 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.signal.glide.load.resource.bitmap;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.glide.load.resource.bitmap;
|
||||
|
||||
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.signal.glide.common.io.InputStreamFactory;
|
||||
import org.signal.glide.common.io.InputStreamRewinder;
|
||||
import org.signal.glide.load.ImageHeaderParserUtils;
|
||||
|
||||
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.
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user