Add support for rendering APNGs.

This commit is contained in:
Greyson Parrelli
2020-09-01 19:13:35 -04:00
committed by Cody Henthorne
parent 1d2ffe56fb
commit 250402e9b9
44 changed files with 2822 additions and 108 deletions

View File

@@ -22,6 +22,7 @@ import android.os.AsyncTask;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
@@ -32,6 +33,7 @@ import com.google.android.gms.security.ProviderInstaller;
import org.conscrypt.Conscrypt;
import org.signal.aesgcmprovider.AesGcmProvider;
import org.signal.glide.SignalGlideCodecs;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.components.TypingStatusRepository;
import org.thoughtcrime.securesms.components.TypingStatusSender;
@@ -127,6 +129,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
initializePendingMessages();
initializeBlobProvider();
initializeCleanup();
initializeGlideCodecs();
FeatureFlags.init();
NotificationChannels.create(this);
@@ -378,6 +381,35 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
});
}
private void initializeGlideCodecs() {
SignalGlideCodecs.setLogProvider(new org.signal.glide.Log.Provider() {
@Override
public void v(@NonNull String tag, @NonNull String message) {
Log.v(tag, message);
}
@Override
public void d(@NonNull String tag, @NonNull String message) {
Log.d(tag, message);
}
@Override
public void i(@NonNull String tag, @NonNull String message) {
Log.i(tag, message);
}
@Override
public void w(@NonNull String tag, @NonNull String message) {
Log.w(tag, message);
}
@Override
public void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) {
Log.e(tag, message, throwable);
}
});
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base)));

View File

@@ -0,0 +1,73 @@
package org.thoughtcrime.securesms.glide.cache;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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 java.io.IOException;
import java.nio.ByteBuffer;
public class ApngBufferCacheDecoder implements ResourceDecoder<ByteBuffer, APNGDecoder> {
@Override
public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) {
return APNGParser.isAPNG(new ByteBufferReader(source));
}
@Override
public @Nullable Resource<APNGDecoder> decode(@NonNull final ByteBuffer source, int width, int height, @NonNull Options options) throws IOException {
if (!APNGParser.isAPNG(new ByteBufferReader(source))) {
return null;
}
Loader loader = new ByteBufferLoader() {
@Override
public ByteBuffer getByteBuffer() {
source.position(0);
return source;
}
};
return new FrameSeqDecoderResource(new APNGDecoder(loader, null), source.limit());
}
private static class FrameSeqDecoderResource implements Resource<APNGDecoder> {
private final APNGDecoder decoder;
private final int size;
FrameSeqDecoderResource(@NonNull APNGDecoder decoder, int size) {
this.decoder = decoder;
this.size = size;
}
@Override
public @NonNull Class<APNGDecoder> getResourceClass() {
return APNGDecoder.class;
}
@Override
public @NonNull APNGDecoder get() {
return this.decoder;
}
@Override
public int getSize() {
return this.size;
}
@Override
public void recycle() {
this.decoder.stop();
}
}
}

View File

@@ -0,0 +1,48 @@
package org.thoughtcrime.securesms.glide.cache;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.load.Options;
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;
public class ApngFrameDrawableTranscoder implements ResourceTranscoder<APNGDecoder, Drawable> {
@Override
public @Nullable Resource<Drawable> transcode(@NonNull Resource<APNGDecoder> toTranscode, @NonNull Options options) {
APNGDecoder decoder = toTranscode.get();
APNGDrawable drawable = new APNGDrawable(decoder);
drawable.setAutoPlay(false);
drawable.setLoopLimit(0);
return new DrawableResource<Drawable>(drawable) {
@Override
public @NonNull Class<Drawable> getResourceClass() {
return Drawable.class;
}
@Override
public int getSize() {
return 0;
}
@Override
public void recycle() {
}
@Override
public void initialize() {
super.initialize();
}
};
}
}

View File

@@ -0,0 +1,44 @@
package org.thoughtcrime.securesms.glide.cache;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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.StreamReader;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
public class ApngStreamCacheDecoder implements ResourceDecoder<InputStream, APNGDecoder> {
private final ResourceDecoder<ByteBuffer, APNGDecoder> byteBufferDecoder;
public ApngStreamCacheDecoder(ResourceDecoder<ByteBuffer, APNGDecoder> byteBufferDecoder) {
this.byteBufferDecoder = byteBufferDecoder;
}
@Override
public boolean handles(@NonNull InputStream source, @NonNull Options options) {
return APNGParser.isAPNG(new StreamReader(source));
}
@Override
public @Nullable Resource<APNGDecoder> decode(@NonNull final InputStream source, int width, int height, @NonNull Options options) throws IOException {
byte[] data = Util.readFully(source);
if (data == null) {
return null;
}
ByteBuffer byteBuffer = ByteBuffer.wrap(data);
return byteBufferDecoder.decode(byteBuffer, width, height, options);
}
}

View File

@@ -0,0 +1,51 @@
package org.thoughtcrime.securesms.glide.cache;
import androidx.annotation.NonNull;
import com.bumptech.glide.load.EncodeStrategy;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceEncoder;
import com.bumptech.glide.load.engine.Resource;
import org.signal.glide.apng.decode.APNGDecoder;
import org.signal.glide.common.loader.Loader;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.Util;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class EncryptedApngCacheEncoder extends EncryptedCoder implements ResourceEncoder<APNGDecoder> {
private static final String TAG = Log.tag(EncryptedApngCacheEncoder.class);
private final byte[] secret;
public EncryptedApngCacheEncoder(@NonNull byte[] secret) {
this.secret = secret;
}
@Override
public @NonNull EncodeStrategy getEncodeStrategy(@NonNull Options options) {
return EncodeStrategy.SOURCE;
}
@Override
public boolean encode(@NonNull Resource<APNGDecoder> data, @NonNull File file, @NonNull Options options) {
try {
Loader loader = data.get().getLoader();
InputStream input = loader.obtain().toInputStream();
OutputStream output = createEncryptedOutputStream(secret, file);
Util.copy(input, output);
return true;
} catch (IOException e) {
Log.w(TAG, e);
}
return false;
}
}

View File

@@ -1,50 +0,0 @@
package org.thoughtcrime.securesms.glide.cache;
import android.graphics.Bitmap;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.logging.Log;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceDecoder;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
public class EncryptedBitmapCacheDecoder extends EncryptedCoder implements ResourceDecoder<File, Bitmap> {
private static final String TAG = EncryptedBitmapCacheDecoder.class.getSimpleName();
private final StreamBitmapDecoder streamBitmapDecoder;
private final byte[] secret;
public EncryptedBitmapCacheDecoder(@NonNull byte[] secret, @NonNull StreamBitmapDecoder streamBitmapDecoder) {
this.secret = secret;
this.streamBitmapDecoder = streamBitmapDecoder;
}
@Override
public boolean handles(@NonNull File source, @NonNull Options options)
throws IOException
{
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
return streamBitmapDecoder.handles(inputStream, options);
} catch (IOException e) {
Log.w(TAG, e);
return false;
}
}
@Override
public @Nullable Resource<Bitmap> decode(@NonNull File source, int width, int height, @NonNull Options options)
throws IOException
{
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
return streamBitmapDecoder.decode(inputStream, width, height, options);
}
}
}

View File

@@ -33,8 +33,6 @@ public class EncryptedBitmapResourceEncoder extends EncryptedCoder implements Re
@SuppressWarnings("EmptyCatchBlock")
@Override
public boolean encode(@NonNull Resource<Bitmap> data, @NonNull File file, @NonNull Options options) {
Log.i(TAG, "Encrypted resource encoder running: " + file.toString());
Bitmap bitmap = data.get();
Bitmap.CompressFormat format = getFormat(bitmap, options);
int quality = options.get(BitmapEncoder.COMPRESSION_QUALITY);

View File

@@ -0,0 +1,44 @@
package org.thoughtcrime.securesms.glide.cache;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceDecoder;
import com.bumptech.glide.load.engine.Resource;
import org.thoughtcrime.securesms.logging.Log;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
public class EncryptedCacheDecoder<DecodeType> extends EncryptedCoder implements ResourceDecoder<File, DecodeType> {
private static final String TAG = Log.tag(EncryptedCacheDecoder.class);
private final byte[] secret;
private final ResourceDecoder<InputStream, DecodeType> decoder;
public EncryptedCacheDecoder(byte[] secret, ResourceDecoder<InputStream, DecodeType> decoder) {
this.secret = secret;
this.decoder = decoder;
}
@Override
public boolean handles(@NonNull File source, @NonNull Options options) throws IOException {
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
return decoder.handles(inputStream, options);
} catch (IOException e) {
Log.w(TAG, e);
return false;
}
}
@Override
public @Nullable Resource<DecodeType> decode(@NonNull File source, int width, int height, @NonNull Options options) throws IOException {
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
return decoder.decode(inputStream, width, height, options);
}
}
}

View File

@@ -30,8 +30,6 @@ public class EncryptedCacheEncoder extends EncryptedCoder implements Encoder<Inp
@SuppressWarnings("EmptyCatchBlock")
@Override
public boolean encode(@NonNull InputStream data, @NonNull File file, @NonNull Options options) {
Log.i(TAG, "Encrypted cache encoder running: " + file.toString());
byte[] buffer = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class);
try (OutputStream outputStream = createEncryptedOutputStream(secret, file)) {

View File

@@ -1,48 +0,0 @@
package org.thoughtcrime.securesms.glide.cache;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.logging.Log;
import com.bumptech.glide.load.Options;
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 java.io.File;
import java.io.IOException;
import java.io.InputStream;
public class EncryptedGifCacheDecoder extends EncryptedCoder implements ResourceDecoder<File, GifDrawable> {
private static final String TAG = EncryptedGifCacheDecoder.class.getSimpleName();
private final byte[] secret;
private final StreamGifDecoder gifDecoder;
public EncryptedGifCacheDecoder(@NonNull byte[] secret, @NonNull StreamGifDecoder gifDecoder) {
this.secret = secret;
this.gifDecoder = gifDecoder;
}
@Override
public boolean handles(@NonNull File source, @NonNull Options options) {
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
return gifDecoder.handles(inputStream, options);
} catch (IOException e) {
Log.w(TAG, e);
return false;
}
}
@Override
public @Nullable Resource<GifDrawable> decode(@NonNull File source, int width, int height, @NonNull Options options) throws IOException {
Log.i(TAG, "Encrypted GIF cache decoder running...");
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
return gifDecoder.decode(inputStream, width, height, options);
}
}
}

View File

@@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.graphics.Bitmap;
import androidx.annotation.NonNull;
import android.graphics.drawable.Drawable;
import android.util.Log;
import com.bumptech.glide.Glide;
@@ -27,14 +29,18 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
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.ApngBufferCacheDecoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedApngCacheEncoder;
import org.thoughtcrime.securesms.glide.ChunkedImageUrlLoader;
import org.thoughtcrime.securesms.glide.ContactPhotoLoader;
import org.thoughtcrime.securesms.glide.cache.ApngFrameDrawableTranscoder;
import org.thoughtcrime.securesms.glide.OkHttpUrlLoader;
import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapCacheDecoder;
import org.thoughtcrime.securesms.glide.cache.ApngStreamCacheDecoder;
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.EncryptedGifCacheDecoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedGifDrawableResourceEncoder;
import org.signal.glide.apng.decode.APNGDecoder;
import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.stickers.StickerRemoteUri;
@@ -42,6 +48,7 @@ import org.thoughtcrime.securesms.stickers.StickerRemoteUriLoader;
import java.io.File;
import java.io.InputStream;
import java.nio.ByteBuffer;
@GlideModule
public class SignalGlideModule extends AppGlideModule {
@@ -63,14 +70,25 @@ public class SignalGlideModule extends AppGlideModule {
byte[] secret = attachmentSecret.getModernKey();
registry.prepend(File.class, File.class, UnitModelLoader.Factory.getInstance());
registry.prepend(InputStream.class, new EncryptedCacheEncoder(secret, glide.getArrayPool()));
registry.prepend(File.class, Bitmap.class, new EncryptedBitmapCacheDecoder(secret, new StreamBitmapDecoder(new Downsampler(registry.getImageHeaderParsers(), context.getResources().getDisplayMetrics(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool())));
registry.prepend(File.class, GifDrawable.class, new EncryptedGifCacheDecoder(secret, new StreamGifDecoder(registry.getImageHeaderParsers(), new ByteBufferGifDecoder(context, registry.getImageHeaderParsers(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool())));
registry.prepend(BlurHash.class, Bitmap.class, new BlurHashResourceDecoder());
registry.prepend(InputStream.class, new EncryptedCacheEncoder(secret, glide.getArrayPool()));
registry.prepend(Bitmap.class, new EncryptedBitmapResourceEncoder(secret));
registry.prepend(File.class, Bitmap.class, new EncryptedCacheDecoder<>(secret, new StreamBitmapDecoder(new Downsampler(registry.getImageHeaderParsers(), context.getResources().getDisplayMetrics(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool())));
registry.prepend(GifDrawable.class, new EncryptedGifDrawableResourceEncoder(secret));
registry.prepend(File.class, GifDrawable.class, new EncryptedCacheDecoder<>(secret, new StreamGifDecoder(registry.getImageHeaderParsers(), new ByteBufferGifDecoder(context, registry.getImageHeaderParsers(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool())));
ApngBufferCacheDecoder apngBufferCacheDecoder = new ApngBufferCacheDecoder();
ApngStreamCacheDecoder apngStreamCacheDecoder = new ApngStreamCacheDecoder(apngBufferCacheDecoder);
registry.prepend(InputStream.class, APNGDecoder.class, apngStreamCacheDecoder);
registry.prepend(ByteBuffer.class, APNGDecoder.class, apngBufferCacheDecoder);
registry.prepend(APNGDecoder.class, new EncryptedApngCacheEncoder(secret));
registry.prepend(File.class, APNGDecoder.class, new EncryptedCacheDecoder<>(secret, apngStreamCacheDecoder));
registry.register(APNGDecoder.class, Drawable.class, new ApngFrameDrawableTranscoder());
registry.prepend(BlurHash.class, Bitmap.class, new BlurHashResourceDecoder());
registry.append(ContactPhoto.class, InputStream.class, new ContactPhotoLoader.Factory(context));
registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context));