diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/SignalGlideComponents.java b/app/src/main/java/org/thoughtcrime/securesms/glide/SignalGlideComponents.java index 0b8f80fde9..0c10cc5410 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/SignalGlideComponents.java +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/SignalGlideComponents.java @@ -33,7 +33,7 @@ import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl; import org.thoughtcrime.securesms.glide.cache.ApngDrawableTranscoder; import org.thoughtcrime.securesms.glide.cache.ApngFrameDrawableTranscoder; import org.thoughtcrime.securesms.glide.cache.ApngInputStreamFactoryResourceDecoder; -import org.thoughtcrime.securesms.glide.cache.ApngInputStreamResourceDecoder; +import org.thoughtcrime.securesms.glide.cache.EncryptedApngCacheDecoder; import org.thoughtcrime.securesms.glide.cache.ByteBufferApngDecoder; import org.thoughtcrime.securesms.glide.cache.EncryptedApngCacheEncoder; import org.thoughtcrime.securesms.glide.cache.EncryptedApngResourceEncoder; @@ -93,10 +93,9 @@ public class SignalGlideComponents implements RegisterGlideComponents { if (SignalStore.labs().getNewApngRenderer()) { - registry.prepend(InputStream.class, ApngDecoder.class, new ApngInputStreamResourceDecoder()); registry.prepend(InputStreamFactory.class, ApngDecoder.class, new ApngInputStreamFactoryResourceDecoder()); registry.prepend(ApngDecoder.class, new EncryptedApngResourceEncoder(secret)); - registry.prepend(File.class, ApngDecoder.class, new EncryptedCacheDecoder<>(secret, new ApngInputStreamResourceDecoder())); + registry.prepend(File.class, ApngDecoder.class, new EncryptedApngCacheDecoder(secret)); registry.register(ApngDecoder.class, Drawable.class, new ApngDrawableTranscoder()); } else { ByteBufferApngDecoder byteBufferApngDecoder = new ByteBufferApngDecoder(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngInputStreamFactoryResourceDecoder.kt b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngInputStreamFactoryResourceDecoder.kt index 52ee9d90c4..4e519fd2a4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngInputStreamFactoryResourceDecoder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngInputStreamFactoryResourceDecoder.kt @@ -9,10 +9,8 @@ import com.bumptech.glide.load.Options import com.bumptech.glide.load.ResourceDecoder import com.bumptech.glide.load.engine.Resource import org.signal.apng.ApngDecoder -import org.signal.core.util.readFully import org.signal.glide.apng.ApngOptions import org.signal.glide.common.io.InputStreamFactory -import java.io.ByteArrayInputStream import java.io.IOException class ApngInputStreamFactoryResourceDecoder : ResourceDecoder { @@ -27,8 +25,7 @@ class ApngInputStreamFactoryResourceDecoder : ResourceDecoder? { - val data: ByteArray = source.create().readFully() - val decoder = ApngDecoder(ByteArrayInputStream(data)) - return ApngResource(decoder, data.size) + val decoder = ApngDecoder.create { source.create() } + return ApngResource(decoder) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngInputStreamResourceDecoder.kt b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngInputStreamResourceDecoder.kt deleted file mode 100644 index 0a80526155..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngInputStreamResourceDecoder.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.glide.cache - -import com.bumptech.glide.load.Options -import com.bumptech.glide.load.ResourceDecoder -import com.bumptech.glide.load.engine.Resource -import org.signal.apng.ApngDecoder -import org.signal.core.util.readFully -import org.signal.core.util.stream.LimitedInputStream -import org.signal.glide.apng.ApngOptions -import java.io.ByteArrayInputStream -import java.io.IOException -import java.io.InputStream - -class ApngInputStreamResourceDecoder : ResourceDecoder { - companion object { - /** Set to match [com.bumptech.glide.load.data.InputStreamRewinder]'s read limit */ - private const val READ_LIMIT: Long = 5 * 1024 * 1024 - } - - override fun handles(source: InputStream, options: Options): Boolean { - return if (options.get(ApngOptions.ANIMATE)!!) { - ApngDecoder.isApng(LimitedInputStream(source, READ_LIMIT)) - } else { - false - } - } - - @Throws(IOException::class) - override fun decode(source: InputStream, width: Int, height: Int, options: Options): Resource? { - val data: ByteArray = source.readFully() - val decoder = ApngDecoder(ByteArrayInputStream(data)) - return ApngResource(decoder, data.size) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngResource.kt b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngResource.kt index c21de6eae4..3f0174e466 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngResource.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngResource.kt @@ -8,14 +8,14 @@ package org.thoughtcrime.securesms.glide.cache import com.bumptech.glide.load.engine.Resource import org.signal.apng.ApngDecoder -class ApngResource(private val decoder: ApngDecoder, private val size: Int) : Resource { +class ApngResource(private val decoder: ApngDecoder) : Resource { override fun getResourceClass(): Class = ApngDecoder::class.java override fun get(): ApngDecoder = decoder - override fun getSize(): Int = size + override fun getSize(): Int = 0 override fun recycle() { - decoder.inputStream.close() + decoder.close() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedApngCacheDecoder.kt b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedApngCacheDecoder.kt new file mode 100644 index 0000000000..a4d9c7b5af --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedApngCacheDecoder.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.glide.cache + +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.ResourceDecoder +import com.bumptech.glide.load.engine.Resource +import org.signal.apng.ApngDecoder +import org.signal.core.util.logging.Log +import org.signal.core.util.stream.LimitedInputStream +import org.signal.glide.apng.ApngOptions +import java.io.File +import java.io.IOException + +internal class EncryptedApngCacheDecoder(private val secret: ByteArray) : EncryptedCoder(), ResourceDecoder { + + companion object { + private val TAG = Log.tag(EncryptedApngCacheDecoder::class.java) + private const val READ_LIMIT: Long = 5 * 1024 * 1024 + } + + override fun handles(source: File, options: Options): Boolean { + if (options.get(ApngOptions.ANIMATE) != true) { + return false + } + + return try { + createEncryptedInputStream(secret, source).use { inputStream -> + ApngDecoder.isApng(LimitedInputStream(inputStream, READ_LIMIT)) + } + } catch (e: IOException) { + Log.w(TAG, e) + false + } + } + + @Throws(IOException::class) + override fun decode(source: File, width: Int, height: Int, options: Options): Resource? { + val decoder = ApngDecoder.create { createEncryptedInputStream(secret, source) } + return ApngResource(decoder) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedApngResourceEncoder.kt b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedApngResourceEncoder.kt index f7d9e1fdba..d8bc8b5658 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedApngResourceEncoder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedApngResourceEncoder.kt @@ -17,11 +17,11 @@ internal class EncryptedApngResourceEncoder(private val secret: ByteArray) : Enc override fun encode(data: Resource, file: File, options: Options): Boolean { try { - val input = data.get().inputStream + val input = data.get().streamFactory() val output = createEncryptedOutputStream(secret, file) - input.reset() input.copyTo(output) + input.close() return true } catch (e: IOException) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/starred/StarredMessagesActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/starred/StarredMessagesActivity.kt index 53d4fc57f8..ff8951669e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/starred/StarredMessagesActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/starred/StarredMessagesActivity.kt @@ -88,7 +88,6 @@ import org.thoughtcrime.securesms.stickers.StickerLocator import org.thoughtcrime.securesms.util.StickyHeaderDecoration import org.thoughtcrime.securesms.util.viewModel import java.util.Locale -import org.signal.core.ui.R as CoreUiR class StarredMessagesActivity : PassphraseRequiredActivity() { diff --git a/core/util-jvm/src/main/java/org/signal/core/util/InputStreamExtensions.kt b/core/util-jvm/src/main/java/org/signal/core/util/InputStreamExtensions.kt index 9e10bb9ec9..55cdc38209 100644 --- a/core/util-jvm/src/main/java/org/signal/core/util/InputStreamExtensions.kt +++ b/core/util-jvm/src/main/java/org/signal/core/util/InputStreamExtensions.kt @@ -139,6 +139,27 @@ fun InputStream.copyTo(outputStream: OutputStream, closeInputStream: Boolean = t return StreamUtil.copy(this, outputStream, closeInputStream, closeOutputStream) } +/** + * Skips exactly [n] bytes from this stream. Unlike [InputStream.skip], this method + * guarantees all bytes are skipped by looping and falling back to [read] if needed. + * + * @throws IOException if the stream ends before [n] bytes have been skipped. + */ +@Throws(IOException::class) +fun InputStream.skipNBytesOrThrow(n: Long) { + var remaining = n + while (remaining > 0) { + val skipped = skip(remaining) + if (skipped > 0) { + remaining -= skipped + } else if (read() == -1) { + throw IOException("Stream ended before $n bytes could be skipped (${n - remaining} skipped)") + } else { + remaining-- + } + } +} + /** * Returns true if every byte in this stream matches the predicate, otherwise false. */ diff --git a/demo/apng/src/main/java/org/signal/apng/DemoActivity.kt b/demo/apng/src/main/java/org/signal/apng/DemoActivity.kt index bc6ee29500..812a850e4e 100644 --- a/demo/apng/src/main/java/org/signal/apng/DemoActivity.kt +++ b/demo/apng/src/main/java/org/signal/apng/DemoActivity.kt @@ -289,7 +289,8 @@ class DemoActivity : ComponentActivity() { description.text = testModel.description - val decoder = ApngDecoder(itemView.context.assets.open(testModel.filename)) + val context = itemView.context + val decoder = ApngDecoder.create { context.assets.open(testModel.filename) } val drawable = ApngDrawable(decoder) image.setImageDrawable(drawable) } diff --git a/demo/apng/src/main/java/org/signal/apng/PlayerActivity.kt b/demo/apng/src/main/java/org/signal/apng/PlayerActivity.kt index a4f54ee5dd..c4ad79d0dd 100644 --- a/demo/apng/src/main/java/org/signal/apng/PlayerActivity.kt +++ b/demo/apng/src/main/java/org/signal/apng/PlayerActivity.kt @@ -26,7 +26,7 @@ class PlayerActivity : ComponentActivity() { val nextButton = findViewById