From 9432a45b395bc6cebe0ab7f27c3097cafe4e4f1b Mon Sep 17 00:00:00 2001 From: alex-signal Date: Thu, 17 Oct 2019 09:26:08 -0300 Subject: [PATCH] Implement blur-hash based low resolution thumbnail previews. --- build.gradle | 2 +- res/layout/thumbnail_view.xml | 10 ++ .../securesms/attachments/Attachment.java | 12 +- .../attachments/DatabaseAttachment.java | 7 +- .../MmsNotificationAttachment.java | 2 +- .../attachments/PointerAttachment.java | 10 +- .../attachments/TombstoneAttachment.java | 2 +- .../securesms/attachments/UriAttachment.java | 10 +- .../securesms/blurhash/Base83.java | 75 +++++++++ .../securesms/blurhash/BlurHash.java | 43 +++++ .../securesms/blurhash/BlurHashDecoder.java | 113 +++++++++++++ .../securesms/blurhash/BlurHashEncoder.java | 148 ++++++++++++++++++ .../blurhash/BlurHashModelLoader.java | 74 +++++++++ .../blurhash/BlurHashResourceDecoder.java | 39 +++++ .../securesms/blurhash/BlurHashUtil.java | 62 ++++++++ .../securesms/components/ThumbnailView.java | 41 ++++- .../securesms/contactshare/Contact.java | 3 +- .../conversation/ConversationActivity.java | 3 +- .../database/AttachmentDatabase.java | 26 ++- .../securesms/database/MediaDatabase.java | 1 + .../securesms/database/MmsDatabase.java | 6 +- .../securesms/database/MmsSmsDatabase.java | 3 +- .../database/helpers/SQLCipherOpenHelper.java | 7 +- .../securesms/groups/V1GroupManager.java | 3 +- .../securesms/jobs/AttachmentDownloadJob.java | 4 +- .../securesms/jobs/AttachmentUploadJob.java | 88 +++++++++-- .../securesms/jobs/AvatarDownloadJob.java | 2 +- .../securesms/jobs/MmsDownloadJob.java | 3 +- .../securesms/jobs/PushDecryptJob.java | 4 +- .../securesms/jobs/PushSendJob.java | 8 +- .../linkpreview/LinkPreviewRepository.java | 3 + .../mediasend/MediaSendActivity.java | 3 +- .../securesms/mms/AttachmentManager.java | 30 ++-- .../securesms/mms/AudioSlide.java | 5 +- .../securesms/mms/DocumentSlide.java | 3 +- .../thoughtcrime/securesms/mms/GifSlide.java | 3 +- .../securesms/mms/ImageSlide.java | 14 +- .../securesms/mms/LocationSlide.java | 2 +- .../securesms/mms/PartAuthority.java | 1 + .../securesms/mms/SignalGlideModule.java | 6 + src/org/thoughtcrime/securesms/mms/Slide.java | 9 +- .../securesms/mms/StickerSlide.java | 3 +- .../thoughtcrime/securesms/mms/TextSlide.java | 3 +- .../securesms/mms/VideoSlide.java | 3 +- witness-verifications.gradle | 4 +- 45 files changed, 817 insertions(+), 86 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/blurhash/Base83.java create mode 100644 src/org/thoughtcrime/securesms/blurhash/BlurHash.java create mode 100644 src/org/thoughtcrime/securesms/blurhash/BlurHashDecoder.java create mode 100644 src/org/thoughtcrime/securesms/blurhash/BlurHashEncoder.java create mode 100644 src/org/thoughtcrime/securesms/blurhash/BlurHashModelLoader.java create mode 100644 src/org/thoughtcrime/securesms/blurhash/BlurHashResourceDecoder.java create mode 100644 src/org/thoughtcrime/securesms/blurhash/BlurHashUtil.java diff --git a/build.gradle b/build.gradle index ab3d03d22..4ab9d8242 100644 --- a/build.gradle +++ b/build.gradle @@ -96,7 +96,7 @@ dependencies { implementation 'org.conscrypt:conscrypt-android:2.0.0' implementation 'org.signal:aesgcmprovider:0.0.3' - implementation 'org.whispersystems:signal-service-android:2.13.8' + implementation 'org.whispersystems:signal-service-android:2.13.9' implementation 'org.signal:ringrtc-android:0.1.4' diff --git a/res/layout/thumbnail_view.xml b/res/layout/thumbnail_view.xml index 4d213257e..6b6119653 100644 --- a/res/layout/thumbnail_view.xml +++ b/res/layout/thumbnail_view.xml @@ -4,6 +4,16 @@ xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto"> + + MAX_LENGTH) return false; + + for (int i = 0; i < length; i++) { + if (indexOf(ALPHABET, value.charAt(i)) == -1) return false; + } + + return true; + } + + private Base83() { + } +} + diff --git a/src/org/thoughtcrime/securesms/blurhash/BlurHash.java b/src/org/thoughtcrime/securesms/blurhash/BlurHash.java new file mode 100644 index 000000000..fd054f030 --- /dev/null +++ b/src/org/thoughtcrime/securesms/blurhash/BlurHash.java @@ -0,0 +1,43 @@ +package org.thoughtcrime.securesms.blurhash; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Objects; + +/** + * A BlurHash is a compact string representation of a blurred image that we can use to show fast + * image previews. + */ +public class BlurHash { + + private final String hash; + + private BlurHash(@NonNull String hash) { + this.hash = hash; + } + + public static @Nullable BlurHash parseOrNull(@Nullable String hash) { + if (Base83.isValid(hash)) { + return new BlurHash(hash); + } + return null; + } + + public @NonNull String getHash() { + return hash; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BlurHash blurHash = (BlurHash) o; + return Objects.equals(hash, blurHash.hash); + } + + @Override + public int hashCode() { + return Objects.hash(hash); + } +} diff --git a/src/org/thoughtcrime/securesms/blurhash/BlurHashDecoder.java b/src/org/thoughtcrime/securesms/blurhash/BlurHashDecoder.java new file mode 100644 index 000000000..4012c3822 --- /dev/null +++ b/src/org/thoughtcrime/securesms/blurhash/BlurHashDecoder.java @@ -0,0 +1,113 @@ +/** + * Source: https://github.com/woltapp/blurhash + * + * Copyright (c) 2018 Wolt Enterprises + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package org.thoughtcrime.securesms.blurhash; + +import android.graphics.Bitmap; +import android.graphics.Color; + +import androidx.annotation.Nullable; + +import static org.thoughtcrime.securesms.blurhash.BlurHashUtil.linearTosRGB; +import static org.thoughtcrime.securesms.blurhash.BlurHashUtil.sRGBToLinear; +import static org.thoughtcrime.securesms.blurhash.BlurHashUtil.signPow; + +class BlurHashDecoder { + + static @Nullable Bitmap decode(@Nullable String blurHash, int width, int height) { + return decode(blurHash, width, height, 1f); + } + + static @Nullable Bitmap decode(@Nullable String blurHash, int width, int height, double punch) { + + if (blurHash == null || blurHash.length() < 6) { + return null; + } + + int numCompEnc = Base83.decode(blurHash, 0, 1); + int numCompX = (numCompEnc % 9) + 1; + int numCompY = (numCompEnc / 9) + 1; + + if (blurHash.length() != 4 + 2 * numCompX * numCompY) { + return null; + } + + int maxAcEnc = Base83.decode(blurHash, 1, 2); + double maxAc = (maxAcEnc + 1) / 166f; + double[][] colors = new double[numCompX * numCompY][]; + for (int i = 0; i < colors.length; i++) { + if (i == 0) { + int colorEnc = Base83.decode(blurHash, 2, 6); + colors[i] = decodeDc(colorEnc); + } else { + int from = 4 + i * 2; + int colorEnc = Base83.decode(blurHash, from, from + 2); + colors[i] = decodeAc(colorEnc, maxAc * punch); + } + } + + return composeBitmap(width, height, numCompX, numCompY, colors); + } + + private static double[] decodeDc(int colorEnc) { + int r = colorEnc >> 16; + int g = (colorEnc >> 8) & 255; + int b = colorEnc & 255; + return new double[] {sRGBToLinear(r), + sRGBToLinear(g), + sRGBToLinear(b)}; + } + + private static double[] decodeAc(int value, double maxAc) { + int r = value / (19 * 19); + int g = (value / 19) % 19; + int b = value % 19; + return new double[]{ + signPow((r - 9) / 9.0f, 2f) * maxAc, + signPow((g - 9) / 9.0f, 2f) * maxAc, + signPow((b - 9) / 9.0f, 2f) * maxAc + }; + } + + private static Bitmap composeBitmap(int width, int height, int numCompX, int numCompY, double[][] colors) { + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + + double r = 0f; + double g = 0f; + double b = 0f; + + for (int j = 0; j < numCompY; j++) { + for (int i = 0; i < numCompX; i++) { + double basis = (Math.cos(Math.PI * x * i / width) * Math.cos(Math.PI * y * j / height)); + double[] color = colors[j * numCompX + i]; + r += color[0] * basis; + g += color[1] * basis; + b += color[2] * basis; + } + } + bitmap.setPixel(x, y, Color.rgb((int) linearTosRGB(r), (int) linearTosRGB(g), (int) linearTosRGB(b))); + } + } + + return bitmap; + } +} diff --git a/src/org/thoughtcrime/securesms/blurhash/BlurHashEncoder.java b/src/org/thoughtcrime/securesms/blurhash/BlurHashEncoder.java new file mode 100644 index 000000000..603481910 --- /dev/null +++ b/src/org/thoughtcrime/securesms/blurhash/BlurHashEncoder.java @@ -0,0 +1,148 @@ +/** + * Source: https://github.com/hsch/blurhash-java + * + * Copyright (c) 2019 Hendrik Schnepel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.thoughtcrime.securesms.blurhash; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.InputStream; + +import static org.thoughtcrime.securesms.blurhash.BlurHashUtil.linearTosRGB; +import static org.thoughtcrime.securesms.blurhash.BlurHashUtil.max; +import static org.thoughtcrime.securesms.blurhash.BlurHashUtil.sRGBToLinear; +import static org.thoughtcrime.securesms.blurhash.BlurHashUtil.signPow; + +public final class BlurHashEncoder { + + private BlurHashEncoder() { + } + + public static @Nullable String encode(InputStream inputStream) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = 16; + + Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options); + if (bitmap == null) return null; + + String hash = encode(bitmap); + + bitmap.recycle(); + + return hash; + } + + public static @Nullable String encode(@NonNull Bitmap bitmap) { + return encode(bitmap, 4, 3); + } + + static String encode(Bitmap bitmap, int componentX, int componentY) { + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + int[] pixels = new int[width * height]; + bitmap.getPixels(pixels, 0, width, 0, 0, width, height); + return encode(pixels, width, height, componentX, componentY); + } + + private static String encode(int[] pixels, int width, int height, int componentX, int componentY) { + + if (componentX < 1 || componentX > 9 || componentY < 1 || componentY > 9) { + throw new IllegalArgumentException("Blur hash must have between 1 and 9 components"); + } + if (width * height != pixels.length) { + throw new IllegalArgumentException("Width and height must match the pixels array"); + } + + double[][] factors = new double[componentX * componentY][3]; + for (int j = 0; j < componentY; j++) { + for (int i = 0; i < componentX; i++) { + double normalisation = i == 0 && j == 0 ? 1 : 2; + applyBasisFunction(pixels, width, height, + normalisation, i, j, + factors, j * componentX + i); + } + } + + char[] hash = new char[1 + 1 + 4 + 2 * (factors.length - 1)]; // size flag + max AC + DC + 2 * AC components + + long sizeFlag = componentX - 1 + (componentY - 1) * 9; + Base83.encode(sizeFlag, 1, hash, 0); + + double maximumValue; + if (factors.length > 1) { + double actualMaximumValue = max(factors, 1, factors.length); + double quantisedMaximumValue = Math.floor(Math.max(0, Math.min(82, Math.floor(actualMaximumValue * 166 - 0.5)))); + maximumValue = (quantisedMaximumValue + 1) / 166; + Base83.encode(Math.round(quantisedMaximumValue), 1, hash, 1); + } else { + maximumValue = 1; + Base83.encode(0, 1, hash, 1); + } + + double[] dc = factors[0]; + Base83.encode(encodeDC(dc), 4, hash, 2); + + for (int i = 1; i < factors.length; i++) { + Base83.encode(encodeAC(factors[i], maximumValue), 2, hash, 6 + 2 * (i - 1)); + } + return new String(hash); + } + + private static void applyBasisFunction(int[] pixels, int width, int height, + double normalisation, int i, int j, + double[][] factors, int index) + { + double r = 0, g = 0, b = 0; + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + double basis = normalisation + * Math.cos((Math.PI * i * x) / width) + * Math.cos((Math.PI * j * y) / height); + int pixel = pixels[y * width + x]; + r += basis * sRGBToLinear((pixel >> 16) & 0xff); + g += basis * sRGBToLinear((pixel >> 8) & 0xff); + b += basis * sRGBToLinear( pixel & 0xff); + } + } + double scale = 1.0 / (width * height); + factors[index][0] = r * scale; + factors[index][1] = g * scale; + factors[index][2] = b * scale; + } + + private static long encodeDC(double[] value) { + long r = linearTosRGB(value[0]); + long g = linearTosRGB(value[1]); + long b = linearTosRGB(value[2]); + return (r << 16) + (g << 8) + b; + } + + private static long encodeAC(double[] value, double maximumValue) { + double quantR = Math.floor(Math.max(0, Math.min(18, Math.floor(signPow(value[0] / maximumValue, 0.5) * 9 + 9.5)))); + double quantG = Math.floor(Math.max(0, Math.min(18, Math.floor(signPow(value[1] / maximumValue, 0.5) * 9 + 9.5)))); + double quantB = Math.floor(Math.max(0, Math.min(18, Math.floor(signPow(value[2] / maximumValue, 0.5) * 9 + 9.5)))); + return Math.round(quantR * 19 * 19 + quantG * 19 + quantB); + } + +} diff --git a/src/org/thoughtcrime/securesms/blurhash/BlurHashModelLoader.java b/src/org/thoughtcrime/securesms/blurhash/BlurHashModelLoader.java new file mode 100644 index 000000000..0dfb8ed70 --- /dev/null +++ b/src/org/thoughtcrime/securesms/blurhash/BlurHashModelLoader.java @@ -0,0 +1,74 @@ +package org.thoughtcrime.securesms.blurhash; + +import androidx.annotation.NonNull; + +import com.bumptech.glide.Priority; +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.data.DataFetcher; +import com.bumptech.glide.load.model.ModelLoader; +import com.bumptech.glide.load.model.ModelLoaderFactory; +import com.bumptech.glide.load.model.MultiModelLoaderFactory; +import com.bumptech.glide.signature.ObjectKey; + +public final class BlurHashModelLoader implements ModelLoader { + + private BlurHashModelLoader() {} + + @Override + public LoadData buildLoadData(@NonNull BlurHash blurHash, + int width, + int height, + @NonNull Options options) + { + return new LoadData<>(new ObjectKey(blurHash.getHash()), new BlurDataFetcher(blurHash)); + } + + @Override + public boolean handles(@NonNull BlurHash blurHash) { + return true; + } + + private final class BlurDataFetcher implements DataFetcher { + + private final BlurHash blurHash; + + private BlurDataFetcher(@NonNull BlurHash blurHash) { + this.blurHash = blurHash; + } + + @Override + public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) { + callback.onDataReady(blurHash); + } + + @Override + public void cleanup() { + } + + @Override + public void cancel() { + } + + @Override + public @NonNull Class getDataClass() { + return BlurHash.class; + } + + @Override + public @NonNull DataSource getDataSource() { + return DataSource.LOCAL; + } + } + + public static class Factory implements ModelLoaderFactory { + @Override + public @NonNull ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { + return new BlurHashModelLoader(); + } + + @Override + public void teardown() { + } + } +} diff --git a/src/org/thoughtcrime/securesms/blurhash/BlurHashResourceDecoder.java b/src/org/thoughtcrime/securesms/blurhash/BlurHashResourceDecoder.java new file mode 100644 index 000000000..a4464b933 --- /dev/null +++ b/src/org/thoughtcrime/securesms/blurhash/BlurHashResourceDecoder.java @@ -0,0 +1,39 @@ +package org.thoughtcrime.securesms.blurhash; + +import android.graphics.Bitmap; + +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 com.bumptech.glide.load.resource.SimpleResource; + +import java.io.IOException; + +public class BlurHashResourceDecoder implements ResourceDecoder { + + private static final int MAX_DIMEN = 20; + + @Override + public boolean handles(@NonNull BlurHash source, @NonNull Options options) throws IOException { + return true; + } + + @Override + public @Nullable Resource decode(@NonNull BlurHash source, int width, int height, @NonNull Options options) throws IOException { + final int finalWidth; + final int finalHeight; + + if (width > height) { + finalWidth = Math.min(width, MAX_DIMEN); + finalHeight = (int) (finalWidth * height / (float) width); + } else { + finalHeight = Math.min(height, MAX_DIMEN); + finalWidth = (int) (finalHeight * width / (float) height); + } + + return new SimpleResource<>(BlurHashDecoder.decode(source.getHash(), finalWidth, finalHeight)); + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/blurhash/BlurHashUtil.java b/src/org/thoughtcrime/securesms/blurhash/BlurHashUtil.java new file mode 100644 index 000000000..0012c18e2 --- /dev/null +++ b/src/org/thoughtcrime/securesms/blurhash/BlurHashUtil.java @@ -0,0 +1,62 @@ +/** + * Source: https://github.com/hsch/blurhash-java + * + * Copyright (c) 2019 Hendrik Schnepel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package org.thoughtcrime.securesms.blurhash; + +final class BlurHashUtil { + + static double sRGBToLinear(long value) { + double v = value / 255.0; + if (v <= 0.04045) { + return v / 12.92; + } else { + return Math.pow((v + 0.055) / 1.055, 2.4); + } + } + + static long linearTosRGB(double value) { + double v = Math.max(0, Math.min(1, value)); + if (v <= 0.0031308) { + return (long)(v * 12.92 * 255 + 0.5); + } else { + return (long)((1.055 * Math.pow(v, 1 / 2.4) - 0.055) * 255 + 0.5); + } + } + + static double signPow(double val, double exp) { + return Math.copySign(Math.pow(Math.abs(val), exp), val); + } + + static double max(double[][] values, int from, int endExclusive) { + double result = Double.NEGATIVE_INFINITY; + for (int i = from; i < endExclusive; i++) { + for (int j = 0; j < values[i].length; j++) { + double value = values[i][j]; + if (value > result) { + result = value; + } + } + } + return result; + } + + private BlurHashUtil() { + } +} diff --git a/src/org/thoughtcrime/securesms/components/ThumbnailView.java b/src/org/thoughtcrime/securesms/components/ThumbnailView.java index d34716cb6..973a772a4 100644 --- a/src/org/thoughtcrime/securesms/components/ThumbnailView.java +++ b/src/org/thoughtcrime/securesms/components/ThumbnailView.java @@ -2,10 +2,13 @@ package org.thoughtcrime.securesms.components; import android.content.Context; import android.content.res.TypedArray; +import android.graphics.Bitmap; import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.UiThread; import android.util.AttributeSet; + +import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.logging.Log; import android.view.View; import android.view.ViewGroup; @@ -36,6 +39,7 @@ import org.whispersystems.libsignal.util.guava.Optional; import java.util.Collections; import java.util.Locale; +import java.util.Objects; import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; @@ -50,6 +54,7 @@ public class ThumbnailView extends FrameLayout { private static final int MAX_HEIGHT = 3; private ImageView image; + private ImageView blurhash; private View playOverlay; private View captionIcon; private OnClickListener parentClickListener; @@ -79,6 +84,7 @@ public class ThumbnailView extends FrameLayout { inflate(context, R.layout.thumbnail_view, this); this.image = findViewById(R.id.thumbnail_image); + this.blurhash = findViewById(R.id.thumbnail_blurhash); this.playOverlay = findViewById(R.id.play_overlay); this.captionIcon = findViewById(R.id.thumbnail_caption_icon); super.setOnClickListener(new ThumbnailClickDispatcher()); @@ -270,22 +276,37 @@ public class ThumbnailView extends FrameLayout { + ", progress " + slide.getTransferState() + ", fast preflight id: " + slide.asAttachment().getFastPreflightId()); + BlurHash previousBlurhash = this.slide != null ? this.slide.getPlaceholderBlur() : null; + this.slide = slide; this.captionIcon.setVisibility(slide.getCaption().isPresent() ? VISIBLE : GONE); dimens[WIDTH] = naturalWidth; dimens[HEIGHT] = naturalHeight; + invalidate(); - SettableFuture result = new SettableFuture<>(); + SettableFuture result = new SettableFuture<>(); + boolean resultHandled = false; + + if (slide.hasPlaceholder() && !Objects.equals(slide.getPlaceholderBlur(), previousBlurhash)) { + buildPlaceholderGlideRequest(glideRequests, slide).into(new GlideBitmapListeningTarget(blurhash, result)); + resultHandled = true; + } else if (!slide.hasPlaceholder()) { + glideRequests.clear(blurhash); + blurhash.setImageDrawable(null); + } if (slide.getThumbnailUri() != null) { buildThumbnailGlideRequest(glideRequests, slide).into(new GlideDrawableListeningTarget(image, result)); - } else if (slide.hasPlaceholder()) { - buildPlaceholderGlideRequest(glideRequests, slide).into(new GlideBitmapListeningTarget(image, result)); + resultHandled = true; } else { glideRequests.clear(image); + image.setImageDrawable(null); + } + + if (!resultHandled) { result.set(false); } @@ -308,6 +329,7 @@ public class ThumbnailView extends FrameLayout { } request.into(new GlideDrawableListeningTarget(image, future)); + blurhash.setImageDrawable(null); return future; } @@ -352,9 +374,16 @@ public class ThumbnailView extends FrameLayout { } private RequestBuilder buildPlaceholderGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) { - return applySizing(glideRequests.asBitmap() - .load(slide.getPlaceholderRes(getContext().getTheme())) - .diskCacheStrategy(DiskCacheStrategy.NONE), new FitCenter()); + GlideRequest bitmap = glideRequests.asBitmap(); + BlurHash placeholderBlur = slide.getPlaceholderBlur(); + + if (placeholderBlur != null) { + bitmap = bitmap.load(placeholderBlur); + } else { + bitmap = bitmap.load(slide.getPlaceholderRes(getContext().getTheme())); + } + + return applySizing(bitmap.diskCacheStrategy(DiskCacheStrategy.NONE), new CenterCrop()); } private GlideRequest applySizing(@NonNull GlideRequest request, @NonNull BitmapTransformation fitting) { diff --git a/src/org/thoughtcrime/securesms/contactshare/Contact.java b/src/org/thoughtcrime/securesms/contactshare/Contact.java index aad4ade1c..d33922db4 100644 --- a/src/org/thoughtcrime/securesms/contactshare/Contact.java +++ b/src/org/thoughtcrime/securesms/contactshare/Contact.java @@ -14,6 +14,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.attachments.UriAttachment; +import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.util.JsonUtils; import org.thoughtcrime.securesms.util.MediaUtil; @@ -642,7 +643,7 @@ public class Contact implements Parcelable { private static Attachment attachmentFromUri(@Nullable Uri uri) { if (uri == null) return null; - return new UriAttachment(uri, MediaUtil.IMAGE_JPEG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, false, false, null, null); + return new UriAttachment(uri, MediaUtil.IMAGE_JPEG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, false, false, null, null, null); } @Override diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 4c5dd3e23..c005d8d11 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -100,6 +100,7 @@ import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.TombstoneAttachment; import org.thoughtcrime.securesms.audio.AudioRecorder; import org.thoughtcrime.securesms.audio.AudioSlidePlayer; +import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.color.MaterialColor; import org.thoughtcrime.securesms.components.AnimatingToggle; import org.thoughtcrime.securesms.components.AttachmentTypeSelector; @@ -586,7 +587,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } else if (MediaUtil.isGif(mediaItem.getMimeType())) { slideDeck.addSlide(new GifSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull())); } else if (MediaUtil.isImageType(mediaItem.getMimeType())) { - slideDeck.addSlide(new ImageSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull())); + slideDeck.addSlide(new ImageSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull(), null)); } else { Log.w(TAG, "Asked to send an unexpected mimeType: '" + mediaItem.getMimeType() + "'. Skipping."); } diff --git a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java index b04fbe88b..4eae3ed42 100644 --- a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -42,6 +42,7 @@ import org.json.JSONException; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; +import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.crypto.AttachmentSecret; import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream; @@ -112,6 +113,7 @@ public class AttachmentDatabase extends Database { static final String HEIGHT = "height"; static final String CAPTION = "caption"; static final String DATA_HASH = "data_hash"; + static final String BLUR_HASH = "blur_hash"; public static final String DIRECTORY = "parts"; @@ -130,7 +132,7 @@ public class AttachmentDatabase extends Database { UNIQUE_ID, DIGEST, FAST_PREFLIGHT_ID, VOICE_NOTE, QUOTE, DATA_RANDOM, THUMBNAIL_RANDOM, WIDTH, HEIGHT, CAPTION, STICKER_PACK_ID, STICKER_PACK_KEY, STICKER_ID, - DATA_HASH}; + DATA_HASH, BLUR_HASH}; public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ROW_ID + " INTEGER PRIMARY KEY, " + MMS_ID + " INTEGER, " + "seq" + " INTEGER DEFAULT 0, " + @@ -145,7 +147,7 @@ public class AttachmentDatabase extends Database { QUOTE + " INTEGER DEFAULT 0, " + WIDTH + " INTEGER DEFAULT 0, " + HEIGHT + " INTEGER DEFAULT 0, " + CAPTION + " TEXT DEFAULT NULL, " + STICKER_PACK_ID + " TEXT DEFAULT NULL, " + STICKER_PACK_KEY + " DEFAULT NULL, " + STICKER_ID + " INTEGER DEFAULT -1, " + - DATA_HASH + " TEXT DEFAULT NULL);"; + DATA_HASH + " TEXT DEFAULT NULL, " + BLUR_HASH + " TEXT DEFAULT NULL);"; public static final String[] CREATE_INDEXS = { "CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");", @@ -342,6 +344,7 @@ public class AttachmentDatabase extends Database { values.put(WIDTH, 0); values.put(HEIGHT, 0); values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE); + values.put(BLUR_HASH, (String) null); database.update(TABLE_NAME, values, MMS_ID + " = ?", new String[] {mmsId + ""}); notifyAttachmentListeners(); @@ -456,6 +459,10 @@ public class AttachmentDatabase extends Database { values.put(DATA_HASH, dataInfo.hash); } + if (placeholder != null && placeholder.getBlurHash() != null) { + values.put(BLUR_HASH, placeholder.getBlurHash().getHash()); + } + values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE); values.put(CONTENT_LOCATION, (String)null); values.put(CONTENT_DISPOSITION, (String)null); @@ -474,6 +481,11 @@ public class AttachmentDatabase extends Database { thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId)); } + private static @Nullable String getBlurHashStringOrNull(@Nullable BlurHash blurHash) { + if (blurHash == null) return null; + return blurHash.getHash(); + } + public void copyAttachmentData(@NonNull AttachmentId sourceId, @NonNull AttachmentId destinationId) throws MmsException { @@ -507,7 +519,7 @@ public class AttachmentDatabase extends Database { contentValues.put(WIDTH, sourceAttachment.getWidth()); contentValues.put(HEIGHT, sourceAttachment.getHeight()); contentValues.put(CONTENT_TYPE, sourceAttachment.getContentType()); - + contentValues.put(BLUR_HASH, getBlurHashStringOrNull(sourceAttachment.getBlurHash())); database.update(TABLE_NAME, contentValues, PART_ID_WHERE, destinationId.toStrings()); } @@ -523,6 +535,7 @@ public class AttachmentDatabase extends Database { values.put(NAME, attachment.getRelay()); values.put(SIZE, attachment.getSize()); values.put(FAST_PREFLIGHT_ID, attachment.getFastPreflightId()); + values.put(BLUR_HASH, getBlurHashStringOrNull(attachment.getBlurHash())); database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings()); } @@ -862,7 +875,8 @@ public class AttachmentDatabase extends Database { ? new StickerLocator(object.getString(STICKER_PACK_ID), object.getString(STICKER_PACK_KEY), object.getInt(STICKER_ID)) - : null)); + : null, + BlurHash.parseOrNull(object.getString(BLUR_HASH)))); } } @@ -891,7 +905,8 @@ public class AttachmentDatabase extends Database { ? new StickerLocator(cursor.getString(cursor.getColumnIndexOrThrow(STICKER_PACK_ID)), cursor.getString(cursor.getColumnIndexOrThrow(STICKER_PACK_KEY)), cursor.getInt(cursor.getColumnIndexOrThrow(STICKER_ID))) - : null)); + : null, + BlurHash.parseOrNull(cursor.getString(cursor.getColumnIndex(BLUR_HASH))))); } } catch (JSONException e) { throw new AssertionError(e); @@ -930,6 +945,7 @@ public class AttachmentDatabase extends Database { contentValues.put(HEIGHT, attachment.getHeight()); contentValues.put(QUOTE, quote); contentValues.put(CAPTION, attachment.getCaption()); + contentValues.put(BLUR_HASH, getBlurHashStringOrNull(attachment.getBlurHash())); if (attachment.isSticker()) { contentValues.put(STICKER_PACK_ID, attachment.getSticker().getPackId()); diff --git a/src/org/thoughtcrime/securesms/database/MediaDatabase.java b/src/org/thoughtcrime/securesms/database/MediaDatabase.java index 7de7e5aa6..090da9329 100644 --- a/src/org/thoughtcrime/securesms/database/MediaDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MediaDatabase.java @@ -37,6 +37,7 @@ public class MediaDatabase extends Database { + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.BLUR_HASH + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CAPTION + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.MESSAGE_BOX + ", " diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java index 713655ab3..446455621 100644 --- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -174,7 +174,8 @@ public class MmsDatabase extends MessagingDatabase { "'" + AttachmentDatabase.CAPTION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CAPTION + ", " + "'" + AttachmentDatabase.STICKER_PACK_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID+ ", " + "'" + AttachmentDatabase.STICKER_PACK_KEY + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " + - "'" + AttachmentDatabase.STICKER_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + + "'" + AttachmentDatabase.STICKER_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", " + + "'" + AttachmentDatabase.BLUR_HASH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.BLUR_HASH + ")) AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS, }; @@ -795,7 +796,8 @@ public class MmsDatabase extends MessagingDatabase { databaseAttachment.getHeight(), databaseAttachment.isQuote(), databaseAttachment.getCaption(), - databaseAttachment.getSticker())); + databaseAttachment.getSticker(), + databaseAttachment.getBlurHash())); } return insertMediaMessage(request.getBody(), diff --git a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index e2f356ec6..56908887a 100644 --- a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -255,7 +255,8 @@ public class MmsSmsDatabase extends Database { "'" + AttachmentDatabase.CAPTION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CAPTION + ", " + "'" + AttachmentDatabase.STICKER_PACK_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID + ", " + "'" + AttachmentDatabase.STICKER_PACK_KEY + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " + - "'" + AttachmentDatabase.STICKER_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + + "'" + AttachmentDatabase.STICKER_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", " + + "'" + AttachmentDatabase.BLUR_HASH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.BLUR_HASH + ")) AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS, SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID, SmsDatabase.TYPE, SmsDatabase.RECIPIENT_ID, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE, diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 5f6e9144a..d83a8d850 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -84,8 +84,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int MMS_RECIPIENT_CLEANUP = 27; private static final int ATTACHMENT_HASHING = 28; private static final int NOTIFICATION_RECIPIENT_IDS = 29; + private static final int BLUR_HASH = 30; - private static final int DATABASE_VERSION = 29; + private static final int DATABASE_VERSION = 30; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -584,6 +585,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { } } + if (oldVersion < BLUR_HASH) { + db.execSQL("ALTER TABLE part ADD COLUMN blur_hash TEXT DEFAULT NULL"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/src/org/thoughtcrime/securesms/groups/V1GroupManager.java b/src/org/thoughtcrime/securesms/groups/V1GroupManager.java index b8a42643e..8462476b3 100644 --- a/src/org/thoughtcrime/securesms/groups/V1GroupManager.java +++ b/src/org/thoughtcrime/securesms/groups/V1GroupManager.java @@ -10,6 +10,7 @@ import com.google.protobuf.ByteString; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.UriAttachment; +import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; @@ -111,7 +112,7 @@ final class V1GroupManager { if (avatar != null) { Uri avatarUri = BlobProvider.getInstance().forData(avatar).createForSingleUseInMemory(); - avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null, null); + avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null, null, null); } OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, false, null, Collections.emptyList(), Collections.emptyList()); diff --git a/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java index bcf79d978..d80df1356 100644 --- a/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java @@ -8,6 +8,7 @@ import org.greenrobot.eventbus.EventBus; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; +import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; @@ -210,7 +211,8 @@ public class AttachmentDownloadJob extends BaseJob { Optional.fromNullable(attachment.getDigest()), Optional.fromNullable(attachment.getFileName()), attachment.isVoiceNote(), - Optional.absent()); + Optional.absent(), + Optional.fromNullable(attachment.getBlurHash()).transform(BlurHash::getHash)); } catch (IOException | ArithmeticException e) { Log.w(TAG, e); throw new InvalidPartException(e); diff --git a/src/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java b/src/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java index 2dd1ece46..ec806b56d 100644 --- a/src/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java @@ -1,5 +1,10 @@ package org.thoughtcrime.securesms.jobs; +import android.graphics.Bitmap; +import android.media.MediaDataSource; +import android.media.MediaMetadataRetriever; +import android.os.Build; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -9,6 +14,7 @@ import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.attachments.PointerAttachment; +import org.thoughtcrime.securesms.blurhash.BlurHashEncoder; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; @@ -20,6 +26,7 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.service.GenericForegroundService; import org.thoughtcrime.securesms.service.NotificationController; +import org.thoughtcrime.securesms.util.MediaUtil; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; @@ -116,27 +123,76 @@ public final class AttachmentUploadJob extends BaseJob { try { if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!"); InputStream is = PartAuthority.getAttachmentStream(context, attachment.getDataUri()); - return SignalServiceAttachment.newStreamBuilder() - .withStream(is) - .withContentType(attachment.getContentType()) - .withLength(attachment.getSize()) - .withFileName(attachment.getFileName()) - .withVoiceNote(attachment.isVoiceNote()) - .withWidth(attachment.getWidth()) - .withHeight(attachment.getHeight()) - .withCaption(attachment.getCaption()) - .withListener((total, progress) -> { - EventBus.getDefault().postSticky(new PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress)); - if (notification != null) { - notification.setProgress(total, progress); - } - }) - .build(); + SignalServiceAttachment.Builder builder = SignalServiceAttachment.newStreamBuilder() + .withStream(is) + .withContentType(attachment.getContentType()) + .withLength(attachment.getSize()) + .withFileName(attachment.getFileName()) + .withVoiceNote(attachment.isVoiceNote()) + .withWidth(attachment.getWidth()) + .withHeight(attachment.getHeight()) + .withCaption(attachment.getCaption()) + .withListener((total, progress) -> { + EventBus.getDefault().postSticky(new PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress)); + if (notification != null) { + notification.setProgress(total, progress); + } + }); + if (MediaUtil.isImageType(attachment.getContentType())) { + return builder.withBlurHash(getImageBlurHash(attachment)).build(); + } else if (MediaUtil.isVideoType(attachment.getContentType())) { + return builder.withBlurHash(getVideoBlurHash(attachment)).build(); + } else { + return builder.build(); + } + } catch (IOException ioe) { throw new InvalidAttachmentException(ioe); } } + private @Nullable String getImageBlurHash(@NonNull Attachment attachment) throws IOException { + if (attachment.getBlurHash() != null) return attachment.getBlurHash().getHash(); + if (attachment.getDataUri() == null) return null; + + return BlurHashEncoder.encode(PartAuthority.getAttachmentStream(context, attachment.getDataUri())); + } + + private @Nullable String getVideoBlurHash(@NonNull Attachment attachment) throws IOException { + if (attachment.getThumbnailUri() != null) { + return BlurHashEncoder.encode(PartAuthority.getAttachmentStream(context, attachment.getThumbnailUri())); + } + + if (attachment.getBlurHash() != null) return attachment.getBlurHash().getHash(); + + if (Build.VERSION.SDK_INT < 23) { + Log.w(TAG, "Video thumbnails not supported..."); + return null; + } + + try (MediaDataSource dataSource = DatabaseFactory.getAttachmentDatabase(context).mediaDataSourceFor(attachmentId)) { + if (dataSource == null) return null; + + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + retriever.setDataSource(dataSource); + + Bitmap bitmap = retriever.getFrameAtTime(1000); + + if (bitmap != null) { + Bitmap thumb = Bitmap.createScaledBitmap(bitmap, 100, 100, false); + bitmap.recycle(); + + Log.i(TAG, "Generated video thumbnail..."); + String hash = BlurHashEncoder.encode(thumb); + thumb.recycle(); + + return hash; + } else { + return null; + } + } + } + private class InvalidAttachmentException extends Exception { InvalidAttachmentException(String message) { super(message); diff --git a/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java index 447d07e3e..11a2faa44 100644 --- a/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java @@ -89,7 +89,7 @@ public class AvatarDownloadJob extends BaseJob { attachment.deleteOnExit(); SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver(); - SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(avatarId, contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, Optional.absent()); + SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(avatarId, contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, Optional.absent(), Optional.absent()); InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, MAX_AVATAR_SIZE); Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key, 0, digest), 500, 500); diff --git a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java index e4b75f2ca..5ed05c000 100644 --- a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java @@ -13,6 +13,7 @@ import com.google.android.mms.pdu_alt.RetrieveConf; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.UriAttachment; +import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.DatabaseFactory; @@ -229,7 +230,7 @@ public class MmsDownloadJob extends BaseJob { attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()), AttachmentDatabase.TRANSFER_PROGRESS_DONE, - part.getData().length, name, false, false, null, null)); + part.getData().length, name, false, false, null, null, null)); } } } diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 473fd7cec..32b5ea8f4 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.attachments.PointerAttachment; import org.thoughtcrime.securesms.attachments.TombstoneAttachment; import org.thoughtcrime.securesms.attachments.UriAttachment; +import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.contactshare.Contact; import org.thoughtcrime.securesms.contactshare.ContactModelMapper; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; @@ -1250,7 +1251,8 @@ public class PushDecryptJob extends BaseJob { false, false, null, - stickerLocator)); + stickerLocator, + null)); } else { return Optional.of(PointerAttachment.forPointer(Optional.of(sticker.get().getAttachment()), stickerLocator).get()); } diff --git a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java index fb5467a7e..d5cbf9ee8 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -11,17 +11,15 @@ import com.annimon.stream.Stream; import org.greenrobot.eventbus.EventBus; import org.signal.libsignal.metadata.certificate.InvalidCertificateException; import org.signal.libsignal.metadata.certificate.SenderCertificate; -import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.TextSecureExpiredException; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; +import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.contactshare.Contact; import org.thoughtcrime.securesms.contactshare.ContactModelMapper; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.MmsDatabase; -import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.events.PartProgressEvent; import org.thoughtcrime.securesms.jobmanager.Job; @@ -30,7 +28,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; -import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.notifications.MessageNotifier; @@ -201,7 +198,8 @@ public abstract class PushSendJob extends SendJob { Optional.fromNullable(attachment.getDigest()), Optional.fromNullable(attachment.getFileName()), attachment.isVoiceNote(), - Optional.fromNullable(attachment.getCaption())); + Optional.fromNullable(attachment.getCaption()), + Optional.fromNullable(attachment.getBlurHash()).transform(BlurHash::getHash)); } catch (IOException | ArithmeticException e) { Log.w(TAG, e); return null; diff --git a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java index dedc298b6..d0d783f73 100644 --- a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java +++ b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java @@ -12,6 +12,7 @@ import com.bumptech.glide.request.FutureTarget; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.UriAttachment; +import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl; @@ -174,6 +175,7 @@ public class LinkPreviewRepository { false, false, null, + null, null)); callback.onComplete(thumbnail); @@ -246,6 +248,7 @@ public class LinkPreviewRepository { false, false, null, + null, null)); callback.onComplete(Optional.of(new LinkPreview(packUrl, title, thumbnail))); diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java b/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java index 67b22c82a..44be0569b 100644 --- a/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java +++ b/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java @@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.TransportOption; import org.thoughtcrime.securesms.TransportOptions; +import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.components.ComposeText; import org.thoughtcrime.securesms.components.InputAwareLayout; import org.thoughtcrime.securesms.components.SendButton; @@ -966,7 +967,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple } else if (MediaUtil.isGif(mediaItem.getMimeType())) { slideDeck.addSlide(new GifSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull())); } else if (MediaUtil.isImageType(mediaItem.getMimeType())) { - slideDeck.addSlide(new ImageSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull())); + slideDeck.addSlide(new ImageSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull(), null)); } else { Log.w(TAG, "Asked to send an unexpected mimeType: '" + mediaItem.getMimeType() + "'. Skipping."); } diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java index abf1dfd5b..8f23412c3 100644 --- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.TransportOption; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.color.MaterialColor; +import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.components.AudioView; import org.thoughtcrime.securesms.components.DocumentView; import org.thoughtcrime.securesms.components.RemovableEditableMediaView; @@ -315,7 +316,7 @@ public class AttachmentManager { } Log.d(TAG, "remote slide with size " + fileSize + " took " + (System.currentTimeMillis() - start) + "ms"); - return mediaType.createSlide(context, uri, fileName, mimeType, fileSize, width, height); + return mediaType.createSlide(context, uri, fileName, mimeType, null, fileSize, width, height); } } finally { if (cursor != null) cursor.close(); @@ -325,10 +326,10 @@ public class AttachmentManager { } private @NonNull Slide getManuallyCalculatedSlideInfo(Uri uri, int width, int height) throws IOException { - long start = System.currentTimeMillis(); - Long mediaSize = null; - String fileName = null; - String mimeType = null; + long start = System.currentTimeMillis(); + Long mediaSize = null; + String fileName = null; + String mimeType = null; if (PartAuthority.isLocalUri(uri)) { mediaSize = PartAuthority.getAttachmentSize(context, uri); @@ -351,7 +352,7 @@ public class AttachmentManager { } Log.d(TAG, "local slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms"); - return mediaType.createSlide(context, uri, fileName, mimeType, mediaSize, width, height); + return mediaType.createSlide(context, uri, fileName, mimeType, null, mediaSize, width, height); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); @@ -525,20 +526,21 @@ public class AttachmentManager { public enum MediaType { IMAGE, GIF, AUDIO, VIDEO, DOCUMENT, VCARD; - public @NonNull Slide createSlide(@NonNull Context context, - @NonNull Uri uri, - @Nullable String fileName, - @Nullable String mimeType, - long dataSize, - int width, - int height) + public @NonNull Slide createSlide(@NonNull Context context, + @NonNull Uri uri, + @Nullable String fileName, + @Nullable String mimeType, + @Nullable BlurHash blurHash, + long dataSize, + int width, + int height) { if (mimeType == null) { mimeType = "application/octet-stream"; } switch (this) { - case IMAGE: return new ImageSlide(context, uri, dataSize, width, height); + case IMAGE: return new ImageSlide(context, uri, dataSize, width, height, blurHash); case GIF: return new GifSlide(context, uri, dataSize, width, height); case AUDIO: return new AudioSlide(context, uri, dataSize, false); case VIDEO: return new VideoSlide(context, uri, dataSize); diff --git a/src/org/thoughtcrime/securesms/mms/AudioSlide.java b/src/org/thoughtcrime/securesms/mms/AudioSlide.java index 7aac798b7..ba2531d6f 100644 --- a/src/org/thoughtcrime/securesms/mms/AudioSlide.java +++ b/src/org/thoughtcrime/securesms/mms/AudioSlide.java @@ -26,6 +26,7 @@ import androidx.annotation.Nullable; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.UriAttachment; +import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.ResUtil; @@ -34,11 +35,11 @@ import org.thoughtcrime.securesms.util.ResUtil; public class AudioSlide extends Slide { public AudioSlide(Context context, Uri uri, long dataSize, boolean voiceNote) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.AUDIO_UNSPECIFIED, dataSize, 0, 0, false, null, null, null, voiceNote, false)); + super(context, constructAttachmentFromUri(context, uri, MediaUtil.AUDIO_UNSPECIFIED, dataSize, 0, 0, false, null, null, null, null, voiceNote, false)); } public AudioSlide(Context context, Uri uri, long dataSize, String contentType, boolean voiceNote) { - super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, null, null)); + super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, null, null, null)); } public AudioSlide(Context context, Attachment attachment) { diff --git a/src/org/thoughtcrime/securesms/mms/DocumentSlide.java b/src/org/thoughtcrime/securesms/mms/DocumentSlide.java index 24d6ceb13..d11682bc2 100644 --- a/src/org/thoughtcrime/securesms/mms/DocumentSlide.java +++ b/src/org/thoughtcrime/securesms/mms/DocumentSlide.java @@ -7,6 +7,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.util.StorageUtil; public class DocumentSlide extends Slide { @@ -19,7 +20,7 @@ public class DocumentSlide extends Slide { @NonNull String contentType, long size, @Nullable String fileName) { - super(context, constructAttachmentFromUri(context, uri, contentType, size, 0, 0, true, StorageUtil.getCleanFileName(fileName), null, null, false, false)); + super(context, constructAttachmentFromUri(context, uri, contentType, size, 0, 0, true, StorageUtil.getCleanFileName(fileName), null, null, null, false, false)); } @Override diff --git a/src/org/thoughtcrime/securesms/mms/GifSlide.java b/src/org/thoughtcrime/securesms/mms/GifSlide.java index a0f1caaae..1a6c0bf8c 100644 --- a/src/org/thoughtcrime/securesms/mms/GifSlide.java +++ b/src/org/thoughtcrime/securesms/mms/GifSlide.java @@ -5,6 +5,7 @@ import android.net.Uri; import androidx.annotation.Nullable; import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.util.MediaUtil; public class GifSlide extends ImageSlide { @@ -19,7 +20,7 @@ public class GifSlide extends ImageSlide { } public GifSlide(Context context, Uri uri, long size, int width, int height, @Nullable String caption) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_GIF, size, width, height, true, null, caption, null, false, false)); + super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_GIF, size, width, height, true, null, caption, null, null, false, false)); } @Override diff --git a/src/org/thoughtcrime/securesms/mms/ImageSlide.java b/src/org/thoughtcrime/securesms/mms/ImageSlide.java index 9100ccb40..46d1d7e67 100644 --- a/src/org/thoughtcrime/securesms/mms/ImageSlide.java +++ b/src/org/thoughtcrime/securesms/mms/ImageSlide.java @@ -25,6 +25,7 @@ import androidx.annotation.Nullable; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.util.MediaUtil; public class ImageSlide extends Slide { @@ -36,12 +37,12 @@ public class ImageSlide extends Slide { super(context, attachment); } - public ImageSlide(Context context, Uri uri, long size, int width, int height) { - this(context, uri, size, width, height, null); + public ImageSlide(Context context, Uri uri, long size, int width, int height, @Nullable BlurHash blurHash) { + this(context, uri, size, width, height, null, blurHash); } - public ImageSlide(Context context, Uri uri, long size, int width, int height, @Nullable String caption) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_JPEG, size, width, height, true, null, caption, null, false, false)); + public ImageSlide(Context context, Uri uri, long size, int width, int height, @Nullable String caption, @Nullable BlurHash blurHash) { + super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_JPEG, size, width, height, true, null, caption, null, blurHash, false, false)); } @Override @@ -59,6 +60,11 @@ public class ImageSlide extends Slide { return true; } + @Override + public boolean hasPlaceholder() { + return getPlaceholderBlur() != null; + } + @NonNull @Override public String getContentDescription() { diff --git a/src/org/thoughtcrime/securesms/mms/LocationSlide.java b/src/org/thoughtcrime/securesms/mms/LocationSlide.java index 2c89e0ec9..f667c116c 100644 --- a/src/org/thoughtcrime/securesms/mms/LocationSlide.java +++ b/src/org/thoughtcrime/securesms/mms/LocationSlide.java @@ -14,7 +14,7 @@ public class LocationSlide extends ImageSlide { public LocationSlide(@NonNull Context context, @NonNull Uri uri, long size, @NonNull SignalPlace place) { - super(context, uri, size, 0, 0); + super(context, uri, size, 0, 0, null); this.place = place; } diff --git a/src/org/thoughtcrime/securesms/mms/PartAuthority.java b/src/org/thoughtcrime/securesms/mms/PartAuthority.java index 508ea06bd..c4aebd5eb 100644 --- a/src/org/thoughtcrime/securesms/mms/PartAuthority.java +++ b/src/org/thoughtcrime/securesms/mms/PartAuthority.java @@ -9,6 +9,7 @@ import androidx.annotation.Nullable; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.AttachmentId; +import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.providers.DeprecatedPersistentBlobProvider; diff --git a/src/org/thoughtcrime/securesms/mms/SignalGlideModule.java b/src/org/thoughtcrime/securesms/mms/SignalGlideModule.java index 374f08cb6..bcca70879 100644 --- a/src/org/thoughtcrime/securesms/mms/SignalGlideModule.java +++ b/src/org/thoughtcrime/securesms/mms/SignalGlideModule.java @@ -20,6 +20,9 @@ import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.load.resource.gif.StreamGifDecoder; import com.bumptech.glide.module.AppGlideModule; +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.crypto.AttachmentSecret; import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; @@ -64,6 +67,8 @@ public class SignalGlideModule extends AppGlideModule { 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(Bitmap.class, new EncryptedBitmapResourceEncoder(secret)); registry.prepend(GifDrawable.class, new EncryptedGifDrawableResourceEncoder(secret)); @@ -72,6 +77,7 @@ public class SignalGlideModule extends AppGlideModule { registry.append(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory()); 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.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory()); } diff --git a/src/org/thoughtcrime/securesms/mms/Slide.java b/src/org/thoughtcrime/securesms/mms/Slide.java index a7743f972..7346f670f 100644 --- a/src/org/thoughtcrime/securesms/mms/Slide.java +++ b/src/org/thoughtcrime/securesms/mms/Slide.java @@ -25,6 +25,7 @@ import androidx.annotation.Nullable; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.UriAttachment; +import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.util.MediaUtil; @@ -126,6 +127,10 @@ public abstract class Slide { throw new AssertionError("getPlaceholderRes() called for non-drawable slide"); } + public @Nullable BlurHash getPlaceholderBlur() { + return attachment.getBlurHash(); + } + public boolean hasPlaceholder() { return false; } @@ -144,6 +149,7 @@ public abstract class Slide { @Nullable String fileName, @Nullable String caption, @Nullable StickerLocator stickerLocator, + @Nullable BlurHash blurHash, boolean voiceNote, boolean quote) { @@ -161,7 +167,8 @@ public abstract class Slide { voiceNote, quote, caption, - stickerLocator); + stickerLocator, + blurHash); } @Override diff --git a/src/org/thoughtcrime/securesms/mms/StickerSlide.java b/src/org/thoughtcrime/securesms/mms/StickerSlide.java index 43f6e885a..4f2ba5eec 100644 --- a/src/org/thoughtcrime/securesms/mms/StickerSlide.java +++ b/src/org/thoughtcrime/securesms/mms/StickerSlide.java @@ -9,6 +9,7 @@ import androidx.annotation.Nullable; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.util.MediaUtil; @@ -22,7 +23,7 @@ public class StickerSlide extends Slide { } public StickerSlide(Context context, Uri uri, long size, @NonNull StickerLocator stickerLocator) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_WEBP, size, WIDTH, HEIGHT, true, null, null, stickerLocator, false, false)); + super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_WEBP, size, WIDTH, HEIGHT, true, null, null, stickerLocator, null, false, false)); } @Override diff --git a/src/org/thoughtcrime/securesms/mms/TextSlide.java b/src/org/thoughtcrime/securesms/mms/TextSlide.java index d3612143e..31ec745e2 100644 --- a/src/org/thoughtcrime/securesms/mms/TextSlide.java +++ b/src/org/thoughtcrime/securesms/mms/TextSlide.java @@ -7,6 +7,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.util.MediaUtil; public class TextSlide extends Slide { @@ -16,6 +17,6 @@ public class TextSlide extends Slide { } public TextSlide(@NonNull Context context, @NonNull Uri uri, @Nullable String filename, long size) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.LONG_TEXT, size, 0, 0, true, filename, null, null, false, false)); + super(context, constructAttachmentFromUri(context, uri, MediaUtil.LONG_TEXT, size, 0, 0, true, filename, null, null, null, false, false)); } } diff --git a/src/org/thoughtcrime/securesms/mms/VideoSlide.java b/src/org/thoughtcrime/securesms/mms/VideoSlide.java index 47bd43af3..509b381fc 100644 --- a/src/org/thoughtcrime/securesms/mms/VideoSlide.java +++ b/src/org/thoughtcrime/securesms/mms/VideoSlide.java @@ -25,6 +25,7 @@ import androidx.annotation.Nullable; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.ResUtil; @@ -35,7 +36,7 @@ public class VideoSlide extends Slide { } public VideoSlide(Context context, Uri uri, long dataSize, @Nullable String caption) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, 0, 0, MediaUtil.hasVideoThumbnail(uri), null, caption, null, false, false)); + super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, 0, 0, MediaUtil.hasVideoThumbnail(uri), null, caption, null, null, false, false)); } public VideoSlide(Context context, Attachment attachment) { diff --git a/witness-verifications.gradle b/witness-verifications.gradle index 9ade74421..0e2391659 100644 --- a/witness-verifications.gradle +++ b/witness-verifications.gradle @@ -126,8 +126,8 @@ dependencyVerification { 'org.whispersystems:curve25519-java:0aadd43cf01d11e9b58f867b3c4f25c3194e8b0623d1953d32dfbfbee009e38d', 'org.whispersystems:signal-protocol-android:c80aac5f93114da2810e2e89437831f79fcbc8bece652f64aeab313a651cba85', 'org.whispersystems:signal-protocol-java:7f6df67a963acbab7716424b01b12fa7279f18a9623a2a7c8ba7b1c285830168', - 'org.whispersystems:signal-service-android:893d42dfa6d08fc1268c4da5aee6f6f3f1bb683ad4a241ef2f3e3d4dfb114e47', - 'org.whispersystems:signal-service-java:045026003e2ddef0325fe1e930de9ce503010aec8e8a8ac6ddbdd9a79f94e878', + 'org.whispersystems:signal-service-android:e0a85fa937f7ad0a446ea65405204ab69533339a78d3aa098921cf43c7997348', + 'org.whispersystems:signal-service-java:5d833e946dbbfb7b4f5dbcf26c1585376e92645aa2958503047ee7a17357897f', 'pl.tajchert:waitingdots:2835d49e0787dbcb606c5a60021ced66578503b1e9fddcd7a5ef0cd5f095ba2c', 'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb', ]