diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java index fc997388f2..afc4d691b7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java @@ -40,6 +40,7 @@ public abstract class Attachment { private final boolean voiceNote; private final boolean borderless; + private final boolean videoGif; private final int width; private final int height; private final boolean quote; @@ -72,6 +73,7 @@ public abstract class Attachment { @Nullable String fastPreflightId, boolean voiceNote, boolean borderless, + boolean videoGif, int width, int height, boolean quote, @@ -94,6 +96,7 @@ public abstract class Attachment { this.fastPreflightId = fastPreflightId; this.voiceNote = voiceNote; this.borderless = borderless; + this.videoGif = videoGif; this.width = width; this.height = height; this.quote = quote; @@ -170,6 +173,10 @@ public abstract class Attachment { return borderless; } + public boolean isVideoGif() { + return videoGif; + } + public int getWidth() { return width; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java index 023109572c..06489236ba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java @@ -36,6 +36,7 @@ public class DatabaseAttachment extends Attachment { String fastPreflightId, boolean voiceNote, boolean borderless, + boolean videoGif, int width, int height, boolean quote, @@ -47,7 +48,7 @@ public class DatabaseAttachment extends Attachment { int displayOrder, long uploadTimestamp) { - super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties); + super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties); this.attachmentId = attachmentId; this.hasData = hasData; this.hasThumbnail = hasThumbnail; diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java index 7ec89b80d8..f88b19138d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java @@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase; public class MmsNotificationAttachment extends Attachment { public MmsNotificationAttachment(int status, long size) { - super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, false, 0, 0, false, 0, null, null, null, null, null); + super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, false, false, 0, 0, false, 0, null, null, null, null, null); } @Nullable diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java index 7912fd8e89..58e3343eff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java @@ -30,6 +30,7 @@ public class PointerAttachment extends Attachment { @Nullable String fastPreflightId, boolean voiceNote, boolean borderless, + boolean videoGif, int width, int height, long uploadTimestamp, @@ -37,7 +38,7 @@ public class PointerAttachment extends Attachment { @Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash) { - super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null); + super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, videoGif, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null); } @Nullable @@ -111,6 +112,7 @@ public class PointerAttachment extends Attachment { fastPreflightId, pointer.get().asPointer().getVoiceNote(), pointer.get().asPointer().isBorderless(), + pointer.get().asPointer().isGif(), pointer.get().asPointer().getWidth(), pointer.get().asPointer().getHeight(), pointer.get().asPointer().getUploadTimestamp(), @@ -135,6 +137,7 @@ public class PointerAttachment extends Attachment { null, false, false, + false, thumbnail != null ? thumbnail.asPointer().getWidth() : 0, thumbnail != null ? thumbnail.asPointer().getHeight() : 0, thumbnail != null ? thumbnail.asPointer().getUploadTimestamp() : 0, diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java index 0504367899..6aabd328fa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java @@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase; public class TombstoneAttachment extends Attachment { public TombstoneAttachment(@NonNull String contentType, boolean quote) { - super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, false, 0, 0, quote, 0, null, null, null, null, null); + super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, false, false, 0, 0, quote, 0, null, null, null, null, null); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java index c5fe46a936..c1317f7e6c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java @@ -21,6 +21,7 @@ public class UriAttachment extends Attachment { @Nullable String fileName, boolean voiceNote, boolean borderless, + boolean videoGif, boolean quote, @Nullable String caption, @Nullable StickerLocator stickerLocator, @@ -28,7 +29,7 @@ public class UriAttachment extends Attachment { @Nullable AudioHash audioHash, @Nullable TransformProperties transformProperties) { - this(uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, quote, caption, stickerLocator, blurHash, audioHash, transformProperties); + this(uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, videoGif, quote, caption, stickerLocator, blurHash, audioHash, transformProperties); } public UriAttachment(@NonNull Uri dataUri, @@ -41,6 +42,7 @@ public class UriAttachment extends Attachment { @Nullable String fastPreflightId, boolean voiceNote, boolean borderless, + boolean videoGif, boolean quote, @Nullable String caption, @Nullable StickerLocator stickerLocator, @@ -48,7 +50,7 @@ public class UriAttachment extends Attachment { @Nullable AudioHash audioHash, @Nullable TransformProperties transformProperties) { - super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties); + super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties); this.dataUri = dataUri; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java index 4184210419..7a4a376e12 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java @@ -12,6 +12,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.core.content.ContextCompat; +import androidx.lifecycle.Lifecycle; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.attachments.Attachment; diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java b/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java index 97efaf48cb..92a1fb5df1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java @@ -36,6 +36,8 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; import org.thoughtcrime.securesms.util.concurrent.SettableFuture; +import org.thoughtcrime.securesms.util.views.Stub; +import org.thoughtcrime.securesms.video.VideoPlayer; import org.whispersystems.libsignal.util.guava.Optional; import java.util.Collections; @@ -55,11 +57,12 @@ public class ThumbnailView extends FrameLayout { private static final int MIN_HEIGHT = 2; private static final int MAX_HEIGHT = 3; - private ImageView image; - private ImageView blurhash; - private View playOverlay; - private View captionIcon; - private OnClickListener parentClickListener; + private ImageView image; + private ImageView blurhash; + private View playOverlay; + private View captionIcon; + private Stub videoPlayer; + private OnClickListener parentClickListener; private final int[] dimens = new int[2]; private final int[] bounds = new int[4]; @@ -90,6 +93,7 @@ public class ThumbnailView extends FrameLayout { this.blurhash = findViewById(R.id.thumbnail_blurhash); this.playOverlay = findViewById(R.id.play_overlay); this.captionIcon = findViewById(R.id.thumbnail_caption_icon); + this.videoPlayer = new Stub<>(findViewById(R.id.thumbnail_player_stub)); super.setOnClickListener(new ThumbnailClickDispatcher()); if (attrs != null) { @@ -335,6 +339,7 @@ public class ThumbnailView extends FrameLayout { } buildThumbnailGlideRequest(glideRequests, slide).into(new GlideDrawableListeningTarget(image, result)); + resultHandled = true; } else { glideRequests.clear(image); @@ -442,7 +447,7 @@ public class ThumbnailView extends FrameLayout { } request = request.override(size[WIDTH], size[HEIGHT]); - + if (radius > 0) { return request.transforms(fitting, new RoundedCorners(radius)); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/Contact.java b/app/src/main/java/org/thoughtcrime/securesms/contactshare/Contact.java index 7d98289f92..3bfa7a03da 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contactshare/Contact.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contactshare/Contact.java @@ -643,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, false, null, null, null, null, null); + return new UriAttachment(uri, MediaUtil.IMAGE_JPEG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, false, false, false, false, null, null, null, null, null); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 494b5d72b8..523bbd720e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -685,10 +685,11 @@ public class ConversationActivity extends PassphraseRequiredActivity break; case PICK_GIF: setMedia(data.getData(), - SlideFactory.MediaType.GIF, + Objects.requireNonNull(MediaType.from(BlobProvider.getMimeType(data.getData()))), data.getIntExtra(GiphyActivity.EXTRA_WIDTH, 0), data.getIntExtra(GiphyActivity.EXTRA_HEIGHT, 0), - data.getBooleanExtra(GiphyActivity.EXTRA_BORDERLESS, false)); + data.getBooleanExtra(GiphyActivity.EXTRA_BORDERLESS, false), + true); break; case SMS_DEFAULT: initializeSecurity(isSecureText, isDefaultSms); @@ -718,7 +719,7 @@ public class ConversationActivity extends PassphraseRequiredActivity for (Media mediaItem : result.getNonUploadedMedia()) { if (MediaUtil.isVideoType(mediaItem.getMimeType())) { - slideDeck.addSlide(new VideoSlide(this, mediaItem.getUri(), mediaItem.getSize(), mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull(), mediaItem.getTransformProperties().orNull())); + slideDeck.addSlide(new VideoSlide(this, mediaItem.getUri(), mediaItem.getSize(), mediaItem.isVideoGif(), mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull(), mediaItem.getTransformProperties().orNull())); } else if (MediaUtil.isGif(mediaItem.getMimeType())) { slideDeck.addSlide(new GifSlide(this, mediaItem.getUri(), mediaItem.getSize(), mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.isBorderless(), mediaItem.getCaption().orNull())); } else if (MediaUtil.isImageType(mediaItem.getMimeType())) { @@ -1662,7 +1663,6 @@ public class ConversationActivity extends PassphraseRequiredActivity .observe(this, b -> updateReminders()); } - private ListenableFuture initializeDraftFromDatabase() { SettableFuture future = new SettableFuture<>(); @@ -2444,10 +2444,10 @@ public class ConversationActivity extends PassphraseRequiredActivity //////// Helper Methods private ListenableFuture setMedia(@Nullable Uri uri, @NonNull MediaType mediaType) { - return setMedia(uri, mediaType, 0, 0, false); + return setMedia(uri, mediaType, 0, 0, false, false); } - private ListenableFuture setMedia(@Nullable Uri uri, @NonNull MediaType mediaType, int width, int height, boolean borderless) { + private ListenableFuture setMedia(@Nullable Uri uri, @NonNull MediaType mediaType, int width, int height, boolean borderless, boolean videoGif) { if (uri == null) { return new SettableFuture<>(false); } @@ -2461,7 +2461,7 @@ public class ConversationActivity extends PassphraseRequiredActivity mimeType = mediaType.toFallbackMimeType(); } - Media media = new Media(uri, mimeType, 0, width, height, 0, 0, borderless, Optional.absent(), Optional.absent(), Optional.absent()); + Media media = new Media(uri, mimeType, 0, width, height, 0, 0, borderless, videoGif, Optional.absent(), Optional.absent(), Optional.absent()); startActivityForResult(MediaSendActivity.buildEditorIntent(ConversationActivity.this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER); return new SettableFuture<>(false); } else { @@ -3168,7 +3168,7 @@ public class ConversationActivity extends PassphraseRequiredActivity private void sendSticker(@NonNull StickerLocator stickerLocator, @NonNull String contentType, @NonNull Uri uri, long size, boolean clearCompose) { if (sendButton.getSelectedTransport().isSms()) { - Media media = new Media(uri, contentType, System.currentTimeMillis(), StickerSlide.WIDTH, StickerSlide.HEIGHT, size, 0, false, Optional.absent(), Optional.absent(), Optional.absent()); + Media media = new Media(uri, contentType, System.currentTimeMillis(), StickerSlide.WIDTH, StickerSlide.HEIGHT, size, 0, false, false, Optional.absent(), Optional.absent(), Optional.absent()); Intent intent = MediaSendActivity.buildEditorIntent(this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport()); startActivityForResult(intent, MEDIA_SENDER); return; diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index de3252d834..ad27056564 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -872,6 +872,7 @@ public class ConversationFragment extends LoggingFragment { attachment.getSize(), 0, attachment.isBorderless(), + attachment.isVideoGif(), Optional.absent(), Optional.fromNullable(attachment.getCaption()), Optional.absent())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java index 9b78f9b484..0ab468f6fd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -102,6 +102,7 @@ public class AttachmentDatabase extends Database { static final String DIGEST = "digest"; static final String VOICE_NOTE = "voice_note"; static final String BORDERLESS = "borderless"; + static final String VIDEO_GIF = "video_gif"; static final String QUOTE = "quote"; public static final String STICKER_PACK_ID = "sticker_pack_id"; public static final String STICKER_PACK_KEY = "sticker_pack_key"; @@ -135,7 +136,7 @@ public class AttachmentDatabase extends Database { MMS_ID, CONTENT_TYPE, NAME, CONTENT_DISPOSITION, CDN_NUMBER, CONTENT_LOCATION, DATA, TRANSFER_STATE, SIZE, FILE_NAME, UNIQUE_ID, DIGEST, - FAST_PREFLIGHT_ID, VOICE_NOTE, BORDERLESS, QUOTE, DATA_RANDOM, + FAST_PREFLIGHT_ID, VOICE_NOTE, BORDERLESS, VIDEO_GIF, QUOTE, DATA_RANDOM, WIDTH, HEIGHT, CAPTION, STICKER_PACK_ID, STICKER_PACK_KEY, STICKER_ID, STICKER_EMOJI, DATA_HASH, VISUAL_HASH, TRANSFORM_PROPERTIES, TRANSFER_FILE, DISPLAY_ORDER, @@ -163,6 +164,7 @@ public class AttachmentDatabase extends Database { FAST_PREFLIGHT_ID + " TEXT, " + VOICE_NOTE + " INTEGER DEFAULT 0, " + BORDERLESS + " INTEGER DEFAULT 0, " + + VIDEO_GIF + " INTEGER DEFAULT 0, " + DATA_RANDOM + " BLOB, " + QUOTE + " INTEGER DEFAULT 0, " + WIDTH + " INTEGER DEFAULT 0, " + @@ -1144,6 +1146,7 @@ public class AttachmentDatabase extends Database { object.getString(FAST_PREFLIGHT_ID), object.getInt(VOICE_NOTE) == 1, object.getInt(BORDERLESS) == 1, + object.getInt(VIDEO_GIF) == 1, object.getInt(WIDTH), object.getInt(HEIGHT), object.getInt(QUOTE) == 1, @@ -1182,6 +1185,7 @@ public class AttachmentDatabase extends Database { cursor.getString(cursor.getColumnIndexOrThrow(FAST_PREFLIGHT_ID)), cursor.getInt(cursor.getColumnIndexOrThrow(VOICE_NOTE)) == 1, cursor.getInt(cursor.getColumnIndexOrThrow(BORDERLESS)) == 1, + cursor.getInt(cursor.getColumnIndexOrThrow(VIDEO_GIF)) == 1, cursor.getInt(cursor.getColumnIndexOrThrow(WIDTH)), cursor.getInt(cursor.getColumnIndexOrThrow(HEIGHT)), cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE)) == 1, @@ -1250,6 +1254,7 @@ public class AttachmentDatabase extends Database { contentValues.put(FAST_PREFLIGHT_ID, attachment.getFastPreflightId()); contentValues.put(VOICE_NOTE, attachment.isVoiceNote() ? 1 : 0); contentValues.put(BORDERLESS, attachment.isBorderless() ? 1 : 0); + contentValues.put(VIDEO_GIF, attachment.isVideoGif() ? 1 : 0); contentValues.put(WIDTH, template.getWidth()); contentValues.put(HEIGHT, template.getHeight()); contentValues.put(QUOTE, quote); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java index 5baf43a61a..bf33868095 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java @@ -20,51 +20,52 @@ public class MediaDatabase extends Database { private static final String THREAD_RECIPIENT_ID = "THREAD_RECIPIENT_ID"; private static final String BASE_MEDIA_QUERY = "SELECT " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " AS " + AttachmentDatabase.ROW_ID + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DIGEST + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FAST_PREFLIGHT_ID + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VOICE_NOTE + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.BORDERLESS + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.WIDTH + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.HEIGHT + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_EMOJI + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VISUAL_HASH + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CAPTION + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UPLOAD_TIMESTAMP + ", " - + MmsDatabase.TABLE_NAME + "." + MmsDatabase.MESSAGE_BOX + ", " - + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_SENT + ", " - + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + ", " - + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_SERVER + ", " - + MmsDatabase.TABLE_NAME + "." + MmsDatabase.THREAD_ID + ", " - + MmsDatabase.TABLE_NAME + "." + MmsDatabase.RECIPIENT_ID + ", " - + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID + " as " + THREAD_RECIPIENT_ID + " " - + "FROM " + AttachmentDatabase.TABLE_NAME + " LEFT JOIN " + MmsDatabase.TABLE_NAME - + " ON " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " - + "LEFT JOIN " + ThreadDatabase.TABLE_NAME - + " ON " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.THREAD_ID + " " - + "WHERE " + AttachmentDatabase.MMS_ID + " IN (SELECT " + MmsSmsColumns.ID - + " FROM " + MmsDatabase.TABLE_NAME - + " WHERE " + MmsDatabase.THREAD_ID + " __EQUALITY__ ?) AND (%s) AND " - + MmsDatabase.VIEW_ONCE + " = 0 AND " - + AttachmentDatabase.DATA + " IS NOT NULL AND " - + "(" + AttachmentDatabase.QUOTE + " = 0 OR (" + AttachmentDatabase.QUOTE + " = 1 AND " + AttachmentDatabase.DATA_HASH + " IS NULL)) AND " - + AttachmentDatabase.STICKER_PACK_ID + " IS NULL "; + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DIGEST + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FAST_PREFLIGHT_ID + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VOICE_NOTE + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.BORDERLESS + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VIDEO_GIF + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.WIDTH + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.HEIGHT + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_EMOJI + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VISUAL_HASH + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CAPTION + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UPLOAD_TIMESTAMP + ", " + + MmsDatabase.TABLE_NAME + "." + MmsDatabase.MESSAGE_BOX + ", " + + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_SENT + ", " + + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + ", " + + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_SERVER + ", " + + MmsDatabase.TABLE_NAME + "." + MmsDatabase.THREAD_ID + ", " + + MmsDatabase.TABLE_NAME + "." + MmsDatabase.RECIPIENT_ID + ", " + + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID + " as " + THREAD_RECIPIENT_ID + " " + + "FROM " + AttachmentDatabase.TABLE_NAME + " LEFT JOIN " + MmsDatabase.TABLE_NAME + + " ON " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " + + "LEFT JOIN " + ThreadDatabase.TABLE_NAME + + " ON " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.THREAD_ID + " " + + "WHERE " + AttachmentDatabase.MMS_ID + " IN (SELECT " + MmsSmsColumns.ID + + " FROM " + MmsDatabase.TABLE_NAME + + " WHERE " + MmsDatabase.THREAD_ID + " __EQUALITY__ ?) AND (%s) AND " + + MmsDatabase.VIEW_ONCE + " = 0 AND " + + AttachmentDatabase.DATA + " IS NOT NULL AND " + + "(" + AttachmentDatabase.QUOTE + " = 0 OR (" + AttachmentDatabase.QUOTE + " = 1 AND " + AttachmentDatabase.DATA_HASH + " IS NULL)) AND " + + AttachmentDatabase.STICKER_PACK_ID + " IS NULL "; private static final String UNIQUE_MEDIA_QUERY = "SELECT " + "MAX(" + AttachmentDatabase.SIZE + ") as " + AttachmentDatabase.SIZE + ", " diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index e789d64fbf..ba76f74f83 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -212,34 +212,35 @@ public class MmsDatabase extends MessageDatabase { SHARED_CONTACTS, LINK_PREVIEWS, UNIDENTIFIED, VIEW_ONCE, REACTIONS, REACTIONS_UNREAD, REACTIONS_LAST_SEEN, REMOTE_DELETED, MENTIONS_SELF, NOTIFIED_TIMESTAMP, VIEWED_RECEIPT_COUNT, "json_group_array(json_object(" + - "'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " + - "'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " + - "'" + AttachmentDatabase.MMS_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ", " + - "'" + AttachmentDatabase.SIZE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " + - "'" + AttachmentDatabase.FILE_NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " + - "'" + AttachmentDatabase.DATA + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " + - "'" + AttachmentDatabase.CONTENT_TYPE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " + - "'" + AttachmentDatabase.CDN_NUMBER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", " + - "'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " + - "'" + AttachmentDatabase.FAST_PREFLIGHT_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FAST_PREFLIGHT_ID + "," + - "'" + AttachmentDatabase.VOICE_NOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VOICE_NOTE + "," + - "'" + AttachmentDatabase.BORDERLESS + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.BORDERLESS + "," + - "'" + AttachmentDatabase.WIDTH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.WIDTH + "," + - "'" + AttachmentDatabase.HEIGHT + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.HEIGHT + "," + - "'" + AttachmentDatabase.QUOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", " + - "'" + AttachmentDatabase.CONTENT_DISPOSITION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", " + - "'" + AttachmentDatabase.NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", " + - "'" + AttachmentDatabase.TRANSFER_STATE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + ", " + - "'" + 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_EMOJI + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_EMOJI + ", " + - "'" + AttachmentDatabase.VISUAL_HASH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VISUAL_HASH + ", " + - "'" + AttachmentDatabase.TRANSFORM_PROPERTIES + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES + ", " + - "'" + AttachmentDatabase.DISPLAY_ORDER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", " + - "'" + AttachmentDatabase.UPLOAD_TIMESTAMP + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UPLOAD_TIMESTAMP + - ")) AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS, + "'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " + + "'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " + + "'" + AttachmentDatabase.MMS_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ", " + + "'" + AttachmentDatabase.SIZE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " + + "'" + AttachmentDatabase.FILE_NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " + + "'" + AttachmentDatabase.DATA + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " + + "'" + AttachmentDatabase.CONTENT_TYPE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " + + "'" + AttachmentDatabase.CDN_NUMBER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", " + + "'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " + + "'" + AttachmentDatabase.FAST_PREFLIGHT_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FAST_PREFLIGHT_ID + "," + + "'" + AttachmentDatabase.VOICE_NOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VOICE_NOTE + "," + + "'" + AttachmentDatabase.BORDERLESS + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.BORDERLESS + "," + + "'" + AttachmentDatabase.VIDEO_GIF + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VIDEO_GIF + "," + + "'" + AttachmentDatabase.WIDTH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.WIDTH + "," + + "'" + AttachmentDatabase.HEIGHT + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.HEIGHT + "," + + "'" + AttachmentDatabase.QUOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", " + + "'" + AttachmentDatabase.CONTENT_DISPOSITION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", " + + "'" + AttachmentDatabase.NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", " + + "'" + AttachmentDatabase.TRANSFER_STATE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + ", " + + "'" + 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_EMOJI + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_EMOJI + ", " + + "'" + AttachmentDatabase.VISUAL_HASH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VISUAL_HASH + ", " + + "'" + AttachmentDatabase.TRANSFORM_PROPERTIES + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES + ", " + + "'" + AttachmentDatabase.DISPLAY_ORDER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", " + + "'" + AttachmentDatabase.UPLOAD_TIMESTAMP + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UPLOAD_TIMESTAMP + + ")) AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS, }; private static final String RAW_ID_WHERE = TABLE_NAME + "._id = ?"; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index 08b5f93924..53f2098b09 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -572,34 +572,35 @@ public class MmsSmsDatabase extends Database { + " || '::' || " + MmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.UNIQUE_ROW_ID, "json_group_array(json_object(" + - "'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " + - "'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " + - "'" + AttachmentDatabase.MMS_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + "," + - "'" + AttachmentDatabase.SIZE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " + - "'" + AttachmentDatabase.FILE_NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " + - "'" + AttachmentDatabase.DATA + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " + - "'" + AttachmentDatabase.CONTENT_TYPE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " + - "'" + AttachmentDatabase.CDN_NUMBER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", " + - "'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " + - "'" + AttachmentDatabase.FAST_PREFLIGHT_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FAST_PREFLIGHT_ID + ", " + - "'" + AttachmentDatabase.VOICE_NOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VOICE_NOTE + ", " + - "'" + AttachmentDatabase.BORDERLESS + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.BORDERLESS + ", " + - "'" + AttachmentDatabase.WIDTH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.WIDTH + ", " + - "'" + AttachmentDatabase.HEIGHT + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.HEIGHT + ", " + - "'" + AttachmentDatabase.QUOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", " + - "'" + AttachmentDatabase.CONTENT_DISPOSITION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", " + - "'" + AttachmentDatabase.NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", " + - "'" + AttachmentDatabase.TRANSFER_STATE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + ", " + - "'" + 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_EMOJI + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_EMOJI + ", " + - "'" + AttachmentDatabase.VISUAL_HASH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VISUAL_HASH + ", " + - "'" + AttachmentDatabase.TRANSFORM_PROPERTIES + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES + ", " + - "'" + AttachmentDatabase.DISPLAY_ORDER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", " + - "'" + AttachmentDatabase.UPLOAD_TIMESTAMP + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UPLOAD_TIMESTAMP + - ")) AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS, + "'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " + + "'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " + + "'" + AttachmentDatabase.MMS_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + "," + + "'" + AttachmentDatabase.SIZE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " + + "'" + AttachmentDatabase.FILE_NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " + + "'" + AttachmentDatabase.DATA + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " + + "'" + AttachmentDatabase.CONTENT_TYPE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " + + "'" + AttachmentDatabase.CDN_NUMBER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", " + + "'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " + + "'" + AttachmentDatabase.FAST_PREFLIGHT_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FAST_PREFLIGHT_ID + ", " + + "'" + AttachmentDatabase.VOICE_NOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VOICE_NOTE + ", " + + "'" + AttachmentDatabase.BORDERLESS + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.BORDERLESS + ", " + + "'" + AttachmentDatabase.VIDEO_GIF + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VIDEO_GIF + ", " + + "'" + AttachmentDatabase.WIDTH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.WIDTH + ", " + + "'" + AttachmentDatabase.HEIGHT + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.HEIGHT + ", " + + "'" + AttachmentDatabase.QUOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", " + + "'" + AttachmentDatabase.CONTENT_DISPOSITION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", " + + "'" + AttachmentDatabase.NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", " + + "'" + AttachmentDatabase.TRANSFER_STATE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + ", " + + "'" + 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_EMOJI + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_EMOJI + ", " + + "'" + AttachmentDatabase.VISUAL_HASH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VISUAL_HASH + ", " + + "'" + AttachmentDatabase.TRANSFORM_PROPERTIES + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES + ", " + + "'" + AttachmentDatabase.DISPLAY_ORDER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", " + + "'" + AttachmentDatabase.UPLOAD_TIMESTAMP + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UPLOAD_TIMESTAMP + + ")) 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, MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, MmsDatabase.PART_COUNT, diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 4d6cbb39bf..d557a199d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -171,8 +171,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab private static final int SPLIT_SYSTEM_NAMES = 90; private static final int PAYMENTS = 91; private static final int CLEAN_STORAGE_IDS = 92; + private static final int MP4_GIF_SUPPORT = 93; - private static final int DATABASE_VERSION = 92; + private static final int DATABASE_VERSION = 93; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -1299,6 +1300,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab Log.i(TAG, "There were " + count + " bad rows that had their storageID removed."); } + if (oldVersion < MP4_GIF_SUPPORT) { + db.execSQL("ALTER TABLE part ADD COLUMN video_gif INTEGER DEFAULT 0"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java index fb0d5d4f00..4ad3934e66 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java @@ -18,7 +18,9 @@ import org.thoughtcrime.securesms.megaphone.MegaphoneRepository; import org.thoughtcrime.securesms.messages.BackgroundMessageRetriever; import org.thoughtcrime.securesms.messages.IncomingMessageObserver; import org.thoughtcrime.securesms.messages.IncomingMessageProcessor; +import org.thoughtcrime.securesms.net.ContentProxySelector; import org.thoughtcrime.securesms.net.PipeConnectivityListener; +import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.payments.Payments; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; @@ -40,6 +42,8 @@ import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; +import okhttp3.OkHttpClient; + /** * Location for storing and retrieving application-scoped singletons. Users must call * {@link #init(Application, Provider)} before using any of the methods, preferably early on in @@ -82,6 +86,7 @@ public class ApplicationDependencies { private static volatile Payments payments; private static volatile ShakeToReport shakeToReport; private static volatile SignalCallManager signalCallManager; + private static volatile OkHttpClient okHttpClient; @MainThread public static void init(@NonNull Application application, @NonNull Provider provider) { @@ -441,6 +446,22 @@ public class ApplicationDependencies { return signalCallManager; } + public static @NonNull OkHttpClient getOkHttpClient() { + if (okHttpClient == null) { + synchronized (LOCK) { + if (okHttpClient == null) { + okHttpClient = new OkHttpClient.Builder() + .proxySelector(new ContentProxySelector()) + .addInterceptor(new StandardUserAgentInterceptor()) + .dns(SignalServiceNetworkAccess.DNS) + .build(); + } + } + } + + return okHttpClient; + } + public static @NonNull AppForegroundObserver getAppForegroundObserver() { return appForegroundObserver; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/model/GiphyImage.java b/app/src/main/java/org/thoughtcrime/securesms/giph/model/GiphyImage.java index 2b0ebbabe1..e8f64b21cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/model/GiphyImage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/model/GiphyImage.java @@ -3,12 +3,15 @@ package org.thoughtcrime.securesms.giph.model; import android.text.TextUtils; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.fasterxml.jackson.annotation.JsonProperty; public class GiphyImage { + private static final int MAX_SIZE = 1024 * 1024; // 1MB + @JsonProperty private ImageTypes images; @@ -24,6 +27,16 @@ public class GiphyImage { return data != null ? data.url : null; } + public String getMp4Url() { + ImageData data = getMp4Data(); + return data != null ? data.mp4 : null; + } + + public String getMp4PreviewUrl() { + ImageData data = getMp4PreviewData(); + return data != null ? data.mp4 : null; + } + public long getGifSize() { ImageData data = getGifData(); return data != null ? data.size : 0; @@ -40,7 +53,7 @@ public class GiphyImage { } public float getGifAspectRatio() { - return (float)images.downsized.width / (float)images.downsized.height; + return (float)images.downsized_small.width / (float)images.downsized_small.height; } public int getGifWidth() { @@ -63,16 +76,24 @@ public class GiphyImage { return data != null ? data.size : 0; } + private @Nullable ImageData getMp4Data() { + return getLargestMp4WithinSizeConstraint(images.fixed_width, images.fixed_height, images.fixed_width_small, images.fixed_height_small, images.downsized_small); + } + + private @Nullable ImageData getMp4PreviewData() { + return images.preview; + } + private @Nullable ImageData getGifData() { - return getFirstNonEmpty(images.downsized, images.downsized_medium, images.fixed_height, images.fixed_width); + return getLargestGifWithinSizeConstraint(images.fixed_width, images.fixed_height, images.fixed_width_small, images.fixed_height_small); } private @Nullable ImageData getGifMmsData() { - return getFirstNonEmpty(images.fixed_height_downsampled, images.fixed_width_downsampled); + return getLargestGifWithinSizeConstraint(images.fixed_width_small, images.fixed_height_small); } private @Nullable ImageData getStillData() { - return getFirstNonEmpty(images.downsized_still, images.fixed_height_still, images.fixed_width_still); + return getFirstNonEmpty(images.fixed_width_small_still, images.fixed_height_small_still); } private static @Nullable ImageData getFirstNonEmpty(ImageData... data) { @@ -85,27 +106,52 @@ public class GiphyImage { return null; } + private @Nullable ImageData getLargestGifWithinSizeConstraint(ImageData ... buckets) { + return getLargestWithinSizeConstraint(imageData -> imageData.size, buckets); + } + + private @Nullable ImageData getLargestMp4WithinSizeConstraint(ImageData ... buckets) { + return getLargestWithinSizeConstraint(imageData -> imageData.mp4_size, buckets); + } + + private @Nullable ImageData getLargestWithinSizeConstraint(@NonNull SizeFunction sizeFunction, ImageData ... buckets) { + ImageData data = null; + int size = 0; + + for (final ImageData bucket : buckets) { + if (bucket == null) continue; + + int bucketSize = sizeFunction.getSize(bucket); + if (bucketSize <= MAX_SIZE && bucketSize > size) { + data = bucket; + size = bucketSize; + } + } + + return data; + } + + private interface SizeFunction { + int getSize(@NonNull ImageData imageData); + } + public static class ImageTypes { @JsonProperty private ImageData fixed_height; @JsonProperty - private ImageData fixed_height_still; + private ImageData fixed_height_small; @JsonProperty - private ImageData fixed_height_downsampled; + private ImageData fixed_height_small_still; @JsonProperty private ImageData fixed_width; @JsonProperty - private ImageData fixed_width_still; - @JsonProperty - private ImageData fixed_width_downsampled; - @JsonProperty private ImageData fixed_width_small; @JsonProperty - private ImageData downsized_medium; + private ImageData fixed_width_small_still; @JsonProperty - private ImageData downsized; + private ImageData downsized_small; @JsonProperty - private ImageData downsized_still; + private ImageData preview; } public static class ImageData { @@ -126,6 +172,9 @@ public class GiphyImage { @JsonProperty private String webp; + + @JsonProperty + private int mp4_size; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/model/GiphyPagination.java b/app/src/main/java/org/thoughtcrime/securesms/giph/model/GiphyPagination.java new file mode 100644 index 0000000000..667caea03a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/model/GiphyPagination.java @@ -0,0 +1,18 @@ +package org.thoughtcrime.securesms.giph.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class GiphyPagination { + @JsonProperty + private int total_count; + + @JsonProperty + private int count; + + @JsonProperty + private int offset; + + public int getTotalCount() { + return total_count; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/model/GiphyResponse.java b/app/src/main/java/org/thoughtcrime/securesms/giph/model/GiphyResponse.java index 4ab61b5715..438fa4a94b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/model/GiphyResponse.java +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/model/GiphyResponse.java @@ -10,8 +10,14 @@ public class GiphyResponse { @JsonProperty private List data; + @JsonProperty + private GiphyPagination pagination; + public List getData() { return data; } + public GiphyPagination getPagination() { + return pagination; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4Adapter.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4Adapter.java new file mode 100644 index 0000000000..2c52380006 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4Adapter.java @@ -0,0 +1,79 @@ +package org.thoughtcrime.securesms.giph.mp4; + + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListAdapter; + +import org.signal.paging.PagingController; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.giph.model.GiphyImage; + +import java.util.Objects; + +/** + * Maintains and displays a list of GiphyImage objects. This Adapter always displays gifs + * as MP4 videos. + */ +final class GiphyMp4Adapter extends ListAdapter { + + private final Callback listener; + private final GiphyMp4MediaSourceFactory mediaSourceFactory; + + private PagingController pagingController; + + public GiphyMp4Adapter(@NonNull GiphyMp4MediaSourceFactory mediaSourceFactory, @Nullable Callback listener) { + super(new GiphyImageDiffUtilCallback()); + + this.listener = listener; + this.mediaSourceFactory = mediaSourceFactory; + } + + @Override + public @NonNull GiphyMp4ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View itemView = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.giphy_mp4, parent, false); + + return new GiphyMp4ViewHolder(itemView, listener, mediaSourceFactory); + } + + @Override + public void onBindViewHolder(@NonNull GiphyMp4ViewHolder holder, int position) { + holder.onBind(getItem(position)); + } + + @Override + protected GiphyImage getItem(int position) { + if (pagingController != null) { + pagingController.onDataNeededAroundIndex(position); + } + + return super.getItem(position); + } + + void setPagingController(@Nullable PagingController pagingController) { + this.pagingController = pagingController; + } + + interface Callback { + void onClick(@NonNull GiphyImage giphyImage); + } + + private static final class GiphyImageDiffUtilCallback extends DiffUtil.ItemCallback { + + @Override + public boolean areItemsTheSame(@NonNull GiphyImage oldItem, @NonNull GiphyImage newItem) { + return Objects.equals(oldItem.getMp4Url(), newItem.getMp4Url()); + } + + @Override + public boolean areContentsTheSame(@NonNull GiphyImage oldItem, @NonNull GiphyImage newItem) { + return areItemsTheSame(oldItem, newItem); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4AdapterPlaybackController.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4AdapterPlaybackController.java new file mode 100644 index 0000000000..764691deeb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4AdapterPlaybackController.java @@ -0,0 +1,133 @@ +package org.thoughtcrime.securesms.giph.mp4; + +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.StaggeredGridLayoutManager; + +import java.util.LinkedList; +import java.util.List; + +/** + * Controls playback of gifs in a {@link GiphyMp4Adapter}. The maximum number of gifs that will play back at any one + * time is determined by the passed parameter, and the exact gifs that play back is algorithmically determined, starting + * with the center-most gifs. + *

+ * This algorithm is devised to play back only those gifs which the user is most likely looking at. + */ +final class GiphyMp4AdapterPlaybackController extends RecyclerView.OnScrollListener implements View.OnLayoutChangeListener { + + private final int maxSimultaneousPlayback; + private final Callback callback; + + private GiphyMp4AdapterPlaybackController(@NonNull Callback callback, int maxSimultaneousPlayback) { + this.maxSimultaneousPlayback = maxSimultaneousPlayback; + this.callback = callback; + } + + public static void attach(@NonNull RecyclerView recyclerView, @NonNull Callback callback, int maxSimultaneousPlayback) { + GiphyMp4AdapterPlaybackController controller = new GiphyMp4AdapterPlaybackController(callback, maxSimultaneousPlayback); + + recyclerView.addOnScrollListener(controller); + recyclerView.addOnLayoutChangeListener(controller); + } + + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + enqueuePlaybackUpdate(recyclerView); + } + + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { + RecyclerView recyclerView = (RecyclerView) v; + enqueuePlaybackUpdate(recyclerView); + } + + private void enqueuePlaybackUpdate(@NonNull RecyclerView recyclerView) { + performPlaybackUpdate(recyclerView); + } + + private void performPlaybackUpdate(@NonNull RecyclerView recyclerView) { + RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + + if (layoutManager == null) { + return; + } + + int[] firstVisiblePositions = findFirstVisibleItemPositions(layoutManager); + int[] lastVisiblePositions = findLastVisibleItemPositions(layoutManager); + + GiphyMp4PlaybackRange playbackRange = getPlaybackRangeForMaximumDistance(firstVisiblePositions, lastVisiblePositions); + + if (playbackRange != null) { + List holders = new LinkedList<>(); + + for (int i = 0; i < recyclerView.getChildCount(); i++) { + GiphyMp4ViewHolder viewHolder = (GiphyMp4ViewHolder) recyclerView.getChildViewHolder(recyclerView.getChildAt(i)); + holders.add(viewHolder); + } + + callback.update(holders, playbackRange); + } + } + + private @Nullable GiphyMp4PlaybackRange getPlaybackRangeForMaximumDistance(int[] firstVisiblePositions, int[] lastVisiblePositions) { + int firstVisiblePosition = Integer.MAX_VALUE; + int lastVisiblePosition = Integer.MIN_VALUE; + + for (int i = 0; i < firstVisiblePositions.length; i++) { + firstVisiblePosition = Math.min(firstVisiblePosition, firstVisiblePositions[i]); + lastVisiblePosition = Math.max(lastVisiblePosition, lastVisiblePositions[i]); + } + + return getPlaybackRange(firstVisiblePosition, lastVisiblePosition); + } + + private @Nullable GiphyMp4PlaybackRange getPlaybackRange(int firstVisiblePosition, int lastVisiblePosition) { + int distance = lastVisiblePosition - firstVisiblePosition; + + if (maxSimultaneousPlayback == 0) { + return null; + } + + if (distance <= maxSimultaneousPlayback) { + return new GiphyMp4PlaybackRange(firstVisiblePosition, lastVisiblePosition); + } else { + int center = (distance / 2) + firstVisiblePosition; + if (maxSimultaneousPlayback == 1) { + return new GiphyMp4PlaybackRange(center, center); + } else { + int first = Math.max(center - maxSimultaneousPlayback / 2, firstVisiblePosition); + int last = Math.min(first + maxSimultaneousPlayback, lastVisiblePosition); + return new GiphyMp4PlaybackRange(first, last); + } + } + } + + private static int[] findFirstVisibleItemPositions(@NonNull RecyclerView.LayoutManager layoutManager) { + if (layoutManager instanceof LinearLayoutManager) { + return new int[]{((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition()}; + } else if (layoutManager instanceof StaggeredGridLayoutManager) { + return ((StaggeredGridLayoutManager) layoutManager).findFirstVisibleItemPositions(null); + } else { + throw new IllegalStateException("Unsupported type: " + layoutManager.getClass().getName()); + } + } + + private static int[] findLastVisibleItemPositions(@NonNull RecyclerView.LayoutManager layoutManager) { + if (layoutManager instanceof LinearLayoutManager) { + return new int[]{((LinearLayoutManager) layoutManager).findLastVisibleItemPosition()}; + } else if (layoutManager instanceof StaggeredGridLayoutManager) { + return ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(null); + } else { + throw new IllegalStateException("Unsupported type: " + layoutManager.getClass().getName()); + } + } + + interface Callback { + void update(@NonNull List holders, @NonNull GiphyMp4PlaybackRange range); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4AdapterPlaybackControllerCallback.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4AdapterPlaybackControllerCallback.java new file mode 100644 index 0000000000..83cb13cb17 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4AdapterPlaybackControllerCallback.java @@ -0,0 +1,116 @@ +package org.thoughtcrime.securesms.giph.mp4; + +import android.util.SparseArray; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Logic for updating content and positioning of videos as the user scrolls the list of gifs. + */ +final class GiphyMp4AdapterPlaybackControllerCallback implements GiphyMp4AdapterPlaybackController.Callback { + + private final List holders; + private final SparseArray playing; + private final SparseArray notPlaying; + + GiphyMp4AdapterPlaybackControllerCallback(@NonNull List holders) { + this.holders = holders; + this.playing = new SparseArray<>(holders.size()); + this.notPlaying = new SparseArray<>(holders.size()); + } + + @Override public void update(@NonNull List holders, + @NonNull GiphyMp4PlaybackRange range) + { + stopAndReleaseAssignedVideos(range); + + for (final GiphyMp4ViewHolder holder : holders) { + if (range.shouldPlayVideo(holder.getAdapterPosition())) { + startPlayback(acquireHolderForPosition(holder.getAdapterPosition()), holder); + } else { + holder.show(); + } + } + + for (final GiphyMp4ViewHolder holder : holders) { + GiphyMp4PlayerHolder playerHolder = getCurrentHolder(holder.getAdapterPosition()); + if (playerHolder != null) { + updateDisplay(playerHolder, holder); + } + } + } + + private void stopAndReleaseAssignedVideos(@NonNull GiphyMp4PlaybackRange playbackRange) { + List markedForDeletion = new ArrayList<>(playing.size()); + for (int i = 0; i < playing.size(); i++) { + if (!playbackRange.shouldPlayVideo(playing.keyAt(i))) { + notPlaying.put(playing.keyAt(i), playing.valueAt(i)); + playing.valueAt(i).setMediaSource(null); + playing.valueAt(i).setOnPlaybackReady(null); + markedForDeletion.add(playing.keyAt(i)); + } + } + + for (final Integer key : markedForDeletion) { + playing.remove(key); + } + } + + private void updateDisplay(@NonNull GiphyMp4PlayerHolder holder, @NonNull GiphyMp4ViewHolder giphyMp4ViewHolder) { + holder.getContainer().setX(giphyMp4ViewHolder.itemView.getX()); + holder.getContainer().setY(giphyMp4ViewHolder.itemView.getY()); + + ViewGroup.LayoutParams params = holder.getContainer().getLayoutParams(); + if (params.width != giphyMp4ViewHolder.itemView.getWidth() || params.height != giphyMp4ViewHolder.itemView.getHeight()) { + params.width = giphyMp4ViewHolder.itemView.getWidth(); + params.height = giphyMp4ViewHolder.itemView.getHeight(); + holder.getContainer().setLayoutParams(params); + } + } + + private void startPlayback(@NonNull GiphyMp4PlayerHolder holder, @NonNull GiphyMp4ViewHolder giphyMp4ViewHolder) { + if (!Objects.equals(holder.getMediaSource(), giphyMp4ViewHolder.getMediaSource())) { + holder.setOnPlaybackReady(null); + giphyMp4ViewHolder.show(); + + holder.setOnPlaybackReady(giphyMp4ViewHolder::hide); + holder.setMediaSource(giphyMp4ViewHolder.getMediaSource()); + } + } + + private @Nullable GiphyMp4PlayerHolder getCurrentHolder(int adapterPosition) { + if (playing.get(adapterPosition) != null) { + return playing.get(adapterPosition); + } else if (notPlaying.get(adapterPosition) != null) { + return notPlaying.get(adapterPosition); + } else { + return null; + } + } + + private @NonNull GiphyMp4PlayerHolder acquireHolderForPosition(int adapterPosition) { + GiphyMp4PlayerHolder holder = playing.get(adapterPosition); + if (holder == null) { + if (notPlaying.size() != 0) { + holder = notPlaying.get(adapterPosition); + if (holder == null) { + int key = notPlaying.keyAt(0); + holder = Objects.requireNonNull(notPlaying.get(key)); + notPlaying.remove(key); + } else { + notPlaying.remove(adapterPosition); + } + } else { + holder = holders.remove(0); + } + playing.put(adapterPosition, holder); + } + return holder; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ExoPlayerProvider.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ExoPlayerProvider.java new file mode 100644 index 0000000000..727ac51e72 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ExoPlayerProvider.java @@ -0,0 +1,46 @@ +package org.thoughtcrime.securesms.giph.mp4; + +import android.content.Context; + +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.lifecycle.DefaultLifecycleObserver; + +import com.google.android.exoplayer2.DefaultLoadControl; +import com.google.android.exoplayer2.DefaultRenderersFactory; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.LoadControl; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelector; + +/** + * Provider which creates ExoPlayer instances for displaying Giphy content. + */ +final class GiphyMp4ExoPlayerProvider implements DefaultLifecycleObserver { + + private final Context context; + private final TrackSelection.Factory videoTrackSelectionFactory; + private final DefaultRenderersFactory renderersFactory; + private final TrackSelector trackSelector; + private final LoadControl loadControl; + + GiphyMp4ExoPlayerProvider(@NonNull Context context) { + this.context = context; + this.videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(); + this.renderersFactory = new DefaultRenderersFactory(context); + this.trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); + this.loadControl = new DefaultLoadControl(); + } + + @MainThread final @NonNull ExoPlayer create() { + ExoPlayer exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector, loadControl); + + exoPlayer.setRepeatMode(Player.REPEAT_MODE_ALL); + + return exoPlayer; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4Fragment.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4Fragment.java new file mode 100644 index 0000000000..b4d2673577 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4Fragment.java @@ -0,0 +1,110 @@ +package org.thoughtcrime.securesms.giph.mp4; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProviders; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.StaggeredGridLayoutManager; + +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.util.TextSecurePreferences; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Fragment which displays GyphyImages. + */ +public class GiphyMp4Fragment extends Fragment { + + private static final String IS_FOR_MMS = "is_for_mms"; + + public GiphyMp4Fragment() { + super(R.layout.giphy_mp4_fragment); + } + + public static Fragment create(boolean isForMms) { + Fragment fragment = new GiphyMp4Fragment(); + Bundle bundle = new Bundle(); + + bundle.putBoolean(IS_FOR_MMS, isForMms); + fragment.setArguments(bundle); + + return fragment; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + boolean isForMms = requireArguments().getBoolean(IS_FOR_MMS, false); + FrameLayout frameLayout = view.findViewById(R.id.giphy_parent); + RecyclerView recycler = view.findViewById(R.id.giphy_recycler); + GiphyMp4ViewModel viewModel = ViewModelProviders.of(requireActivity(), new GiphyMp4ViewModel.Factory(isForMms)).get(GiphyMp4ViewModel.class); + GiphyMp4MediaSourceFactory mediaSourceFactory = new GiphyMp4MediaSourceFactory(ApplicationDependencies.getOkHttpClient()); + GiphyMp4Adapter adapter = new GiphyMp4Adapter(mediaSourceFactory, viewModel::saveToBlob); + List holders = injectVideoViews(frameLayout); + GiphyMp4AdapterPlaybackControllerCallback callback = new GiphyMp4AdapterPlaybackControllerCallback(holders); + + recycler.setLayoutManager(getLayoutManager(TextSecurePreferences.isGifSearchInGridLayout(getContext()))); + recycler.setAdapter(adapter); + recycler.setItemAnimator(null); + + GiphyMp4AdapterPlaybackController.attach(recycler, callback, GiphyMp4PlaybackPolicy.maxSimultaneousPlaybackInSearchResults()); + + viewModel.getImages().observe(getViewLifecycleOwner(), adapter::submitList); + + viewModel.getPagingController().observe(getViewLifecycleOwner(), adapter::setPagingController); + viewModel.isGridMode().observe(getViewLifecycleOwner(), isGridLayout -> updateGridLayout(recycler, isGridLayout)); + } + + private void updateGridLayout(@NonNull RecyclerView recyclerView, boolean isGridLayout) { + RecyclerView.LayoutManager oldLayoutManager = recyclerView.getLayoutManager(); + RecyclerView.LayoutManager newLayoutManager = getLayoutManager(isGridLayout); + + if (oldLayoutManager == null || !Objects.equals(oldLayoutManager.getClass(), newLayoutManager.getClass())) { + recyclerView.setLayoutManager(newLayoutManager); + } + } + + private RecyclerView.LayoutManager getLayoutManager(boolean gridLayout) { + return gridLayout ? new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL) + : new LinearLayoutManager(requireContext()); + } + + private List injectVideoViews(@NonNull ViewGroup viewGroup) { + int nPlayers = GiphyMp4PlaybackPolicy.maxSimultaneousPlaybackInSearchResults(); + List holders = new ArrayList<>(nPlayers); + GiphyMp4ExoPlayerProvider playerProvider = new GiphyMp4ExoPlayerProvider(requireContext()); + + for (int i = 0; i < nPlayers; i++) { + FrameLayout container = (FrameLayout) LayoutInflater.from(requireContext()) + .inflate(R.layout.giphy_mp4_player, viewGroup, false); + GiphyMp4VideoPlayer player = container.findViewById(R.id.video_player); + ExoPlayer exoPlayer = playerProvider.create(); + GiphyMp4PlayerHolder holder = new GiphyMp4PlayerHolder(container, player); + + getViewLifecycleOwner().getLifecycle().addObserver(player); + player.setExoPlayer(exoPlayer); + player.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FILL); + exoPlayer.addListener(holder); + + holders.add(holder); + viewGroup.addView(container); + } + + return holders; + } + +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4MediaKeyboardProvider.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4MediaKeyboardProvider.java new file mode 100644 index 0000000000..8eba32737c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4MediaKeyboardProvider.java @@ -0,0 +1,76 @@ +package org.thoughtcrime.securesms.giph.mp4; + +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.emoji.MediaKeyboardProvider; +import org.thoughtcrime.securesms.mms.GlideRequests; + +/** + * MediaKeyboardProvider for MP4 Gifs + */ +@SuppressWarnings("unused") +public final class GiphyMp4MediaKeyboardProvider implements MediaKeyboardProvider { + + private final GiphyMp4MediaKeyboardPagerAdapter pagerAdapter; + + private Controller controller; + + public GiphyMp4MediaKeyboardProvider(@NonNull FragmentActivity fragmentActivity, boolean isForMms) { + pagerAdapter = new GiphyMp4MediaKeyboardPagerAdapter(fragmentActivity.getSupportFragmentManager(), isForMms); + } + + @Override + public int getProviderIconView(boolean selected) { + if (selected) { + return R.layout.giphy_mp4_keyboard_icon_selected; + } else { + return R.layout.giphy_mp4_keyboard_icon; + } + } + + @Override + public void requestPresentation(@NonNull Presenter presenter, boolean isSoloProvider) { + presenter.present(this, pagerAdapter, new GiphyMp4MediaKeyboardTabIconProvider(), null, null, null, 0); + } + + @Override + public void setController(@Nullable Controller controller) { + this.controller = controller; + } + + @Override + public void setCurrentPosition(int currentPosition) { + // ignored. + } + + private static final class GiphyMp4MediaKeyboardPagerAdapter extends FragmentStatePagerAdapter { + + private final boolean isForMms; + + public GiphyMp4MediaKeyboardPagerAdapter(@NonNull FragmentManager fm, boolean isForMms) { + super(fm, FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + this.isForMms = isForMms; + } + + @Override + public @NonNull Fragment getItem(int position) { + return GiphyMp4Fragment.create(isForMms); + } + + @Override public int getCount() { + return 1; + } + } + + private static final class GiphyMp4MediaKeyboardTabIconProvider implements TabIconProvider { + @Override public void loadCategoryTabIcon(@NonNull GlideRequests glideRequests, @NonNull ImageView imageView, int index) { } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4MediaSourceFactory.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4MediaSourceFactory.java new file mode 100644 index 0000000000..89bc7f952e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4MediaSourceFactory.java @@ -0,0 +1,35 @@ +package org.thoughtcrime.securesms.giph.mp4; + +import android.net.Uri; + +import androidx.annotation.NonNull; + +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.extractor.ExtractorsFactory; +import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.upstream.DataSource; + +import org.thoughtcrime.securesms.video.exo.ChunkedDataSourceFactory; + +import okhttp3.OkHttpClient; + +/** + * Factory which creates MediaSource objects for given Giphy URIs + */ +final class GiphyMp4MediaSourceFactory { + + private final DataSource.Factory dataSourceFactory; + private final ExtractorsFactory extractorsFactory; + private final ExtractorMediaSource.Factory extractorMediaSourceFactory; + + GiphyMp4MediaSourceFactory(@NonNull OkHttpClient okHttpClient) { + dataSourceFactory = new ChunkedDataSourceFactory(okHttpClient, null); + extractorsFactory = new DefaultExtractorsFactory(); + extractorMediaSourceFactory = new ExtractorMediaSource.Factory(dataSourceFactory).setExtractorsFactory(extractorsFactory); + } + + @NonNull MediaSource create(@NonNull Uri uri) { + return extractorMediaSourceFactory.createMediaSource(uri); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4PagedDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4PagedDataSource.java new file mode 100644 index 0000000000..7cf0c44246 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4PagedDataSource.java @@ -0,0 +1,86 @@ +package org.thoughtcrime.securesms.giph.mp4; + +import android.net.Uri; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.signal.core.util.logging.Log; +import org.signal.paging.PagedDataSource; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.giph.model.GiphyImage; +import org.thoughtcrime.securesms.giph.model.GiphyResponse; +import org.thoughtcrime.securesms.util.JsonUtils; + +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +/** + * Data source for GiphyImages. + */ +final class GiphyMp4PagedDataSource implements PagedDataSource { + + private static final String TAG = Log.tag(GiphyMp4PagedDataSource.class); + + private final String searchString; + private final OkHttpClient client; + + GiphyMp4PagedDataSource(@Nullable String searchQuery) { + this.searchString = searchQuery; + this.client = ApplicationDependencies.getOkHttpClient(); + } + + @Override + public int size() { + try { + GiphyResponse response = performFetch(0, 1); + + return response.getPagination().getTotalCount(); + } catch (IOException e) { + return 0; + } + } + + @Override + public @NonNull List load(int start, int length, @NonNull CancellationSignal cancellationSignal) { + try { + Log.d(TAG, "Loading from " + start + " to " + (start + length)); + return new LinkedList<>(performFetch(start, length).getData()); + } catch (IOException e) { + Log.w(TAG, e); + return new LinkedList<>(); + } + } + + private @NonNull GiphyResponse performFetch(int start, int length) throws IOException { + String url; + + if (TextUtils.isEmpty(searchString)) url = getTrendingUrl(start, length); + else url = getSearchUrl(start, length, Uri.encode(searchString)); + + Request request = new Request.Builder().url(url).build(); + + try (Response response = client.newCall(request).execute()) { + + if (!response.isSuccessful()) { + throw new IOException("Unexpected code " + response); + } + + return JsonUtils.fromJson(response.body().byteStream(), GiphyResponse.class); + } + } + + private String getTrendingUrl(int start, int length) { + return "https://api.giphy.com/v1/gifs/trending?api_key=3o6ZsYH6U6Eri53TXy&offset=" + start + "&limit=" + length; + } + + private String getSearchUrl(int start, int length, @NonNull String query) { + return "https://api.giphy.com/v1/gifs/search?api_key=3o6ZsYH6U6Eri53TXy&offset=" + start + "&limit=" + length + "&q=" + Uri.encode(query); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4PlaybackPolicy.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4PlaybackPolicy.java new file mode 100644 index 0000000000..f84143e537 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4PlaybackPolicy.java @@ -0,0 +1,57 @@ +package org.thoughtcrime.securesms.giph.mp4; + +import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; +import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; +import com.google.android.exoplayer2.util.MimeTypes; + +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.util.DeviceProperties; +import org.thoughtcrime.securesms.util.FeatureFlags; +import org.thoughtcrime.securesms.util.MediaUtil; + +import java.util.concurrent.TimeUnit; + +/** + * Central policy object for determining what kind of gifs to display, routing, etc. + */ +public final class GiphyMp4PlaybackPolicy { + + private GiphyMp4PlaybackPolicy() { } + + public static boolean sendAsMp4() { + return FeatureFlags.mp4GifSendSupport(); + } + + public static int maxRepeatsOfSinglePlayback() { + return 3; + } + + public static long maxDurationOfSinglePlayback() { + return TimeUnit.SECONDS.toMillis(6); + } + + public static int maxSimultaneousPlaybackInSearchResults() { + int maxInstances = 0; + + try { + MediaCodecInfo info = MediaCodecUtil.getDecoderInfo(MimeTypes.VIDEO_H264, false); + + if (info != null) { + maxInstances = (int) (info.getMaxSupportedInstances() * 0.75f); + } + + } catch (MediaCodecUtil.DecoderQueryException ignored) { + } + + if (maxInstances > 0) { + return maxInstances; + } + + if (DeviceProperties.isLowMemoryDevice(ApplicationDependencies.getApplication())) { + return 2; + } else { + return 6; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4PlaybackRange.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4PlaybackRange.java new file mode 100644 index 0000000000..5dd812bbbc --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4PlaybackRange.java @@ -0,0 +1,46 @@ +package org.thoughtcrime.securesms.giph.mp4; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.Objects; + +/** + * Object describing the range of adapter positions for which playback should begin. + */ +final class GiphyMp4PlaybackRange { + private final int startPosition; + private final int endPosition; + + GiphyMp4PlaybackRange(int startPosition, int endPosition) { + this.startPosition = startPosition; + this.endPosition = endPosition; + } + + boolean shouldPlayVideo(int adapterPosition) { + if (adapterPosition == RecyclerView.NO_POSITION) return false; + + return this.startPosition <= adapterPosition && this.endPosition > adapterPosition; + } + + @Override + public @NonNull String toString() { + return "PlaybackRange{" + + "startPosition=" + startPosition + + ", endPosition=" + endPosition + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final GiphyMp4PlaybackRange that = (GiphyMp4PlaybackRange) o; + return startPosition == that.startPosition && + endPosition == that.endPosition; + } + + @Override public int hashCode() { + return Objects.hash(startPosition, endPosition); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4PlayerHolder.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4PlayerHolder.java new file mode 100644 index 0000000000..de3c827f95 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4PlayerHolder.java @@ -0,0 +1,57 @@ +package org.thoughtcrime.securesms.giph.mp4; + +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.source.MediaSource; + +/** + * Object which holds on to an injected video player. + */ +final class GiphyMp4PlayerHolder implements Player.EventListener { + private final FrameLayout container; + private final GiphyMp4VideoPlayer player; + + private Runnable onPlaybackReady; + private MediaSource mediaSource; + + GiphyMp4PlayerHolder(@NonNull FrameLayout container, @NonNull GiphyMp4VideoPlayer player) { + this.container = container; + this.player = player; + } + + @NonNull FrameLayout getContainer() { + return container; + } + + public void setMediaSource(@Nullable MediaSource mediaSource) { + this.mediaSource = mediaSource; + + if (mediaSource != null) { + player.setVideoSource(mediaSource); + player.play(); + } else { + player.stop(); + } + } + + public @Nullable MediaSource getMediaSource() { + return mediaSource; + } + + void setOnPlaybackReady(@Nullable Runnable onPlaybackReady) { + this.onPlaybackReady = onPlaybackReady; + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (playbackState == Player.STATE_READY) { + if (onPlaybackReady != null) { + onPlaybackReady.run(); + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4Repository.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4Repository.java new file mode 100644 index 0000000000..f842b0610f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4Repository.java @@ -0,0 +1,82 @@ +package org.thoughtcrime.securesms.giph.mp4; + +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; +import androidx.core.util.Consumer; + +import org.signal.core.util.concurrent.SignalExecutors; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.giph.model.GiphyImage; +import org.thoughtcrime.securesms.net.ContentProxySelector; +import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor; +import org.thoughtcrime.securesms.providers.BlobProvider; +import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; +import org.thoughtcrime.securesms.util.MediaUtil; + +import java.io.IOException; +import java.util.concurrent.Executor; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +/** + * Repository responsible for downloading gifs selected by the user in the appropriate format. + */ +final class GiphyMp4Repository { + + private static final Executor EXECUTOR = SignalExecutors.BOUNDED; + + private final OkHttpClient client; + + GiphyMp4Repository() { + this.client = new OkHttpClient.Builder().proxySelector(new ContentProxySelector()) + .addInterceptor(new StandardUserAgentInterceptor()) + .dns(SignalServiceNetworkAccess.DNS) + .build(); + } + + void saveToBlob(@NonNull GiphyImage giphyImage, boolean isForMms, @NonNull Consumer resultConsumer) { + EXECUTOR.execute(() -> { + try { + Uri blob = saveToBlobInternal(giphyImage, isForMms); + resultConsumer.accept(new GiphyMp4SaveResult.Success(blob, giphyImage)); + } catch (IOException e) { + resultConsumer.accept(new GiphyMp4SaveResult.Error(e)); + } + }); + } + + @WorkerThread + private @NonNull Uri saveToBlobInternal(@NonNull GiphyImage giphyImage, boolean isForMms) throws IOException { + boolean sendAsMp4 = GiphyMp4PlaybackPolicy.sendAsMp4(); + String url; + String mime; + + if (sendAsMp4) { + url = giphyImage.getMp4Url(); + mime = MediaUtil.VIDEO_MP4; + } else if (isForMms) { + url = giphyImage.getGifMmsUrl(); + mime = MediaUtil.IMAGE_GIF; + } else { + url = giphyImage.getGifUrl(); + mime = MediaUtil.IMAGE_GIF; + } + + Request request = new Request.Builder().url(url).build(); + + try (Response response = client.newCall(request).execute()) { + if (response.code() >= 200 && response.code() < 300) { + return BlobProvider.getInstance() + .forData(response.body().byteStream(), response.body().contentLength()) + .withMimeType(mime) + .createForSingleSessionOnDisk(ApplicationDependencies.getApplication()); + } else { + throw new IOException("Unexpected response code: " + response.code()); + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4SaveResult.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4SaveResult.java new file mode 100644 index 0000000000..467f9d1168 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4SaveResult.java @@ -0,0 +1,56 @@ +package org.thoughtcrime.securesms.giph.mp4; + +import android.net.Uri; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.giph.model.GiphyImage; + +/** + * Encapsulates the result of downloading a Giphy MP4 or GIF for + * sending to a user. + */ +public abstract class GiphyMp4SaveResult { + private GiphyMp4SaveResult() {} + + public final static class Success extends GiphyMp4SaveResult { + private final Uri blobUri; + private final int width; + private final int height; + private final boolean isBorderless; + + Success(@NonNull Uri blobUri, @NonNull GiphyImage giphyImage) { + this.blobUri = blobUri; + this.width = giphyImage.getGifWidth(); + this.height = giphyImage.getGifHeight(); + this.isBorderless = giphyImage.isSticker(); + } + + public int getHeight() { + return height; + } + + public int getWidth() { + return width; + } + + public @NonNull Uri getBlobUri() { + return blobUri; + } + + public boolean isBorderless() { + return isBorderless; + } + } + + public final static class InProgress extends GiphyMp4SaveResult { + } + + public final static class Error extends GiphyMp4SaveResult { + private final Exception exception; + + Error(@NonNull Exception exception) { + this.exception = exception; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4VideoPlayer.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4VideoPlayer.java new file mode 100644 index 0000000000..c1e4bbdd90 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4VideoPlayer.java @@ -0,0 +1,83 @@ +package org.thoughtcrime.securesms.giph.mp4; + +import android.content.Context; +import android.graphics.Color; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleOwner; + +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.ui.PlayerView; + +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.R; + +/** + * Video Player class specifically created for the GiphyMp4Fragment. + */ +public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLifecycleObserver { + + @SuppressWarnings("unused") + private static final String TAG = Log.tag(GiphyMp4VideoPlayer.class); + + private final PlayerView exoView; + private ExoPlayer exoPlayer; + + public GiphyMp4VideoPlayer(Context context) { + this(context, null); + } + + public GiphyMp4VideoPlayer(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public GiphyMp4VideoPlayer(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + inflate(context, R.layout.gif_player, this); + + this.exoView = findViewById(R.id.video_view); + } + + @Override + protected void onDetachedFromWindow() { + Log.d(TAG, "onDetachedFromWindow"); + super.onDetachedFromWindow(); + } + + void setExoPlayer(@NonNull ExoPlayer exoPlayer) { + exoView.setPlayer(exoPlayer); + this.exoPlayer = exoPlayer; + } + + void setVideoSource(@NonNull MediaSource mediaSource) { + exoPlayer.prepare(mediaSource); + } + + void play() { + if (exoPlayer != null) { + exoPlayer.setPlayWhenReady(true); + } + } + + void stop() { + if (exoPlayer != null) { + exoPlayer.stop(true); + } + } + + void setResizeMode(@AspectRatioFrameLayout.ResizeMode int resizeMode) { + exoView.setResizeMode(resizeMode); + } + + @Override public void onDestroy(@NonNull LifecycleOwner owner) { + if (exoPlayer != null) { + exoPlayer.release(); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewHolder.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewHolder.java new file mode 100644 index 0000000000..c6c42f0723 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewHolder.java @@ -0,0 +1,85 @@ +package org.thoughtcrime.securesms.giph.mp4; + +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.view.View; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.color.MaterialColor; +import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl; +import org.thoughtcrime.securesms.giph.model.GiphyImage; +import org.thoughtcrime.securesms.mms.GlideApp; +import org.thoughtcrime.securesms.util.Util; + +/** + * Holds a view which will either play back an MP4 gif or show its still. + */ +final class GiphyMp4ViewHolder extends RecyclerView.ViewHolder { + + private final AspectRatioFrameLayout container; + private final ImageView stillImage; + private final GiphyMp4Adapter.Callback listener; + private final Drawable placeholder; + private final GiphyMp4MediaSourceFactory mediaSourceFactory; + + private float aspectRatio; + private MediaSource mediaSource; + + GiphyMp4ViewHolder(@NonNull View itemView, + @Nullable GiphyMp4Adapter.Callback listener, + @NonNull GiphyMp4MediaSourceFactory mediaSourceFactory) + { + super(itemView); + this.container = (AspectRatioFrameLayout) itemView; + this.listener = listener; + this.stillImage = itemView.findViewById(R.id.still_image); + this.placeholder = new ColorDrawable(Util.getRandomElement(MaterialColor.values()).toConversationColor(itemView.getContext())); + this.mediaSourceFactory = mediaSourceFactory; + + container.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH); + } + + void onBind(@NonNull GiphyImage giphyImage) { + aspectRatio = giphyImage.getGifAspectRatio(); + mediaSource = mediaSourceFactory.create(Uri.parse(giphyImage.getMp4PreviewUrl())); + + container.setAspectRatio(aspectRatio); + container.setBackground(placeholder); + + loadPlaceholderImage(giphyImage); + + itemView.setOnClickListener(v -> listener.onClick(giphyImage)); + } + + void show() { + container.setAlpha(1f); + } + + void hide() { + container.setAlpha(0f); + } + + private void loadPlaceholderImage(@NonNull GiphyImage giphyImage) { + GlideApp.with(itemView) + .load(new ChunkedImageUrl(giphyImage.getStillUrl())) + .placeholder(placeholder) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .transition(DrawableTransitionOptions.withCrossFade()) + .into(stillImage); + } + + @NonNull MediaSource getMediaSource() { + return mediaSource; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewModel.java new file mode 100644 index 0000000000..63e6cf7ada --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewModel.java @@ -0,0 +1,109 @@ +package org.thoughtcrime.securesms.giph.mp4; + +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Transformations; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; + +import com.annimon.stream.Stream; + +import org.signal.paging.PagedData; +import org.signal.paging.PagingConfig; +import org.signal.paging.PagingController; +import org.thoughtcrime.securesms.giph.model.GiphyImage; +import org.thoughtcrime.securesms.util.DefaultValueLiveData; +import org.thoughtcrime.securesms.util.SingleLiveEvent; + +import java.util.List; +import java.util.Objects; + +/** + * ViewModel which drives GiphyMp4Fragment. This is to be bound to the activity, + * and used as both a data provider and controller. + */ +public final class GiphyMp4ViewModel extends ViewModel { + + private final GiphyMp4Repository repository; + private final MutableLiveData> pagedData; + private final LiveData> images; + private final LiveData pagingController; + private final SingleLiveEvent saveResultEvents; + private final MutableLiveData isGridMode; + private final boolean isForMms; + + private String query; + + private GiphyMp4ViewModel(boolean isForMms) { + this.isForMms = isForMms; + this.repository = new GiphyMp4Repository(); + this.pagedData = new DefaultValueLiveData<>(getGiphyImagePagedData(null)); + this.saveResultEvents = new SingleLiveEvent<>(); + this.isGridMode = new MutableLiveData<>(); + this.pagingController = Transformations.map(pagedData, PagedData::getController); + this.images = Transformations.switchMap(pagedData, pagedData -> Transformations.map(pagedData.getData(), + data -> Stream.of(data) + .filter(g -> g != null) + .filterNot(g -> TextUtils.isEmpty(isForMms ? g.getGifMmsUrl() : g.getGifUrl())) + .filterNot(g -> TextUtils.isEmpty(g.getMp4PreviewUrl())) + .filterNot(g -> TextUtils.isEmpty(g.getStillUrl())) + .toList())); + } + + public void updateSearchQuery(@Nullable String query) { + if (!Objects.equals(query, this.query)) { + this.query = query; + + pagedData.setValue(getGiphyImagePagedData(query)); + } + } + + public void updateLayout(boolean isGridMode) { + this.isGridMode.setValue(isGridMode); + } + + public void saveToBlob(@NonNull GiphyImage giphyImage) { + saveResultEvents.postValue(new GiphyMp4SaveResult.InProgress()); + repository.saveToBlob(giphyImage, isForMms, saveResultEvents::postValue); + } + + public @NonNull LiveData getSaveResultEvents() { + return saveResultEvents; + } + + public @NonNull LiveData> getImages() { + return images; + } + + public @NonNull LiveData getPagingController() { + return pagingController; + } + + public @NonNull LiveData isGridMode() { + return isGridMode; + } + + private PagedData getGiphyImagePagedData(@Nullable String query) { + return PagedData.create(new GiphyMp4PagedDataSource(query), + new PagingConfig.Builder().setPageSize(20) + .setBufferPages(1) + .build()); + } + + public static class Factory implements ViewModelProvider.Factory { + private final boolean isForMms; + + public Factory(boolean isForMms) { + this.isForMms = isForMms; + } + + @Override + public @NonNull T create(@NonNull Class modelClass) { + return Objects.requireNonNull(modelClass.cast(new GiphyMp4ViewModel(isForMms))); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/net/GiphyGifLoader.java b/app/src/main/java/org/thoughtcrime/securesms/giph/net/GiphyGifLoader.java deleted file mode 100644 index b88ea20087..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/net/GiphyGifLoader.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.thoughtcrime.securesms.giph.net; - - -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -public class GiphyGifLoader extends GiphyLoader { - - public GiphyGifLoader(@NonNull Context context, @Nullable String searchString) { - super(context, searchString); - } - - @Override - protected String getTrendingUrl() { - return "https://api.giphy.com/v1/gifs/trending?api_key=3o6ZsYH6U6Eri53TXy&offset=%d&limit=" + PAGE_SIZE; - } - - @Override - protected String getSearchUrl() { - return "https://api.giphy.com/v1/gifs/search?api_key=3o6ZsYH6U6Eri53TXy&offset=%d&limit=" + PAGE_SIZE + "&q=%s"; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java index eadf0fb0f3..3196691c67 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java @@ -13,10 +13,12 @@ import android.widget.Toast; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.core.app.ActivityCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; +import androidx.lifecycle.ViewModelProviders; import androidx.viewpager.widget.ViewPager; import com.google.android.material.tabs.TabLayout; @@ -24,12 +26,16 @@ import com.google.android.material.tabs.TabLayout; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.PassphraseRequiredActivity; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Fragment; +import org.thoughtcrime.securesms.giph.mp4.GiphyMp4SaveResult; +import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ViewModel; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.WindowUtil; +import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; import java.io.IOException; import java.util.concurrent.ExecutionException; @@ -51,12 +57,15 @@ public class GiphyActivity extends PassphraseRequiredActivity private final DynamicTheme dynamicTheme = new DynamicDarkToolbarTheme(); private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); - private GiphyGifFragment gifFragment; + private Fragment gifFragment; private GiphyStickerFragment stickerFragment; private boolean forMms; private GiphyAdapter.GiphyViewHolder finishingImage; + private GiphyMp4ViewModel giphyMp4ViewModel; + private AlertDialog progressDialog; + @Override public void onPreCreate() { dynamicTheme.onCreate(this); @@ -67,6 +76,11 @@ public class GiphyActivity extends PassphraseRequiredActivity public void onCreate(Bundle bundle, boolean ready) { setContentView(R.layout.giphy_activity); + forMms = getIntent().getBooleanExtra(EXTRA_IS_MMS, false); + giphyMp4ViewModel = ViewModelProviders.of(this, new GiphyMp4ViewModel.Factory(forMms)).get(GiphyMp4ViewModel.class); + + giphyMp4ViewModel.getSaveResultEvents().observe(this, this::handleGiphyMp4SaveResult); + initializeToolbar(); initializeResources(); } @@ -92,11 +106,9 @@ public class GiphyActivity extends PassphraseRequiredActivity ViewPager viewPager = findViewById(R.id.giphy_pager); TabLayout tabLayout = findViewById(R.id.tab_layout); - this.gifFragment = new GiphyGifFragment(); + this.gifFragment = GiphyMp4Fragment.create(forMms); this.stickerFragment = new GiphyStickerFragment(); - this.forMms = getIntent().getBooleanExtra(EXTRA_IS_MMS, false); - gifFragment.setClickListener(this); stickerFragment.setClickListener(this); viewPager.setAdapter(new GiphyFragmentPagerAdapter(this, getSupportFragmentManager(), @@ -105,19 +117,52 @@ public class GiphyActivity extends PassphraseRequiredActivity tabLayout.setBackgroundColor(getConversationColor()); } + private void handleGiphyMp4SaveResult(@NonNull GiphyMp4SaveResult result) { + if (result instanceof GiphyMp4SaveResult.Success) { + hideProgressDialog(); + handleGiphyMp4SuccessfulResult((GiphyMp4SaveResult.Success) result); + } else if (result instanceof GiphyMp4SaveResult.Error) { + hideProgressDialog(); + handleGiphyMp4ErrorResult((GiphyMp4SaveResult.Error) result); + } else { + progressDialog = SimpleProgressDialog.show(this); + } + } + + private void hideProgressDialog() { + if (progressDialog != null) { + progressDialog.dismiss(); + } + } + + private void handleGiphyMp4SuccessfulResult(@NonNull GiphyMp4SaveResult.Success success) { + Intent intent = new Intent(); + intent.setData(success.getBlobUri()); + intent.putExtra(EXTRA_WIDTH, success.getWidth()); + intent.putExtra(EXTRA_HEIGHT, success.getHeight()); + intent.putExtra(EXTRA_BORDERLESS, success.getBlobUri()); + + setResult(RESULT_OK, intent); + finish(); + } + + private void handleGiphyMp4ErrorResult(@NonNull GiphyMp4SaveResult.Error error) { + Toast.makeText(this, R.string.GiphyActivity_error_while_retrieving_full_resolution_gif, Toast.LENGTH_LONG).show(); + } + private @ColorInt int getConversationColor() { return getIntent().getIntExtra(EXTRA_COLOR, ActivityCompat.getColor(this, R.color.core_ultramarine)); } @Override public void onFilterChanged(String filter) { - this.gifFragment.setSearchString(filter); + giphyMp4ViewModel.updateSearchQuery(filter); this.stickerFragment.setSearchString(filter); } @Override public void onLayoutChanged(boolean gridLayout) { - gifFragment.setLayoutManager(gridLayout); + giphyMp4ViewModel.updateLayout(gridLayout); stickerFragment.setLayoutManager(gridLayout); } @@ -164,14 +209,14 @@ public class GiphyActivity extends PassphraseRequiredActivity private static class GiphyFragmentPagerAdapter extends FragmentPagerAdapter { - private final Context context; - private final GiphyGifFragment gifFragment; - private final GiphyStickerFragment stickerFragment; + private final Context context; + private final Fragment gifFragment; + private final Fragment stickerFragment; private GiphyFragmentPagerAdapter(@NonNull Context context, @NonNull FragmentManager fragmentManager, - @NonNull GiphyGifFragment gifFragment, - @NonNull GiphyStickerFragment stickerFragment) + @NonNull Fragment gifFragment, + @NonNull Fragment stickerFragment) { super(fragmentManager); this.context = context.getApplicationContext(); @@ -182,7 +227,7 @@ public class GiphyActivity extends PassphraseRequiredActivity @Override public Fragment getItem(int position) { if (position == 0) return gifFragment; - else return stickerFragment; + else return stickerFragment; } @Override @@ -193,7 +238,7 @@ public class GiphyActivity extends PassphraseRequiredActivity @Override public CharSequence getPageTitle(int position) { if (position == 0) return context.getString(R.string.GiphyFragmentPagerAdapter_gifs); - else return context.getString(R.string.GiphyFragmentPagerAdapter_stickers); + else return context.getString(R.string.GiphyFragmentPagerAdapter_stickers); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyGifFragment.java b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyGifFragment.java deleted file mode 100644 index 6f4edeeebe..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyGifFragment.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.thoughtcrime.securesms.giph.ui; - - -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.loader.content.Loader; - -import org.thoughtcrime.securesms.giph.model.GiphyImage; -import org.thoughtcrime.securesms.giph.net.GiphyGifLoader; - -import java.util.List; - -public class GiphyGifFragment extends GiphyFragment { - - @Override - public @NonNull Loader> onCreateLoader(int id, Bundle args) { - return new GiphyGifLoader(getActivity(), searchString); - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java index 58b23e1f9c..fd7cd95feb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java @@ -176,7 +176,7 @@ final class GroupManagerV1 { 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, false, null, null, null, null, null); + avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, false, false, null, null, null, null, null); } OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, false, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java index 191b53e1af..8fb9b42df7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java @@ -19,7 +19,6 @@ import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobLogger; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.thoughtcrime.securesms.jobmanager.impl.NotInCallConstraint; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.util.AttachmentUtil; @@ -213,6 +212,7 @@ public final class AttachmentDownloadJob extends BaseJob { Optional.fromNullable(attachment.getFileName()), attachment.isVoiceNote(), attachment.isBorderless(), + attachment.isVideoGif(), Optional.absent(), Optional.fromNullable(attachment.getBlurHash()).transform(BlurHash::getHash), attachment.getUploadTimestamp()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java index 00b2d842d9..d013665a17 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java @@ -167,6 +167,7 @@ public final class AttachmentUploadJob extends BaseJob { .withFileName(attachment.getFileName()) .withVoiceNote(attachment.isVoiceNote()) .withBorderless(attachment.isBorderless()) + .withGif(attachment.isVideoGif()) .withWidth(attachment.getWidth()) .withHeight(attachment.getHeight()) .withUploadTimestamp(System.currentTimeMillis()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV1DownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV1DownloadJob.java index b517f0349f..557b0095ce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV1DownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV1DownloadJob.java @@ -85,7 +85,7 @@ public final class AvatarGroupsV1DownloadJob extends BaseJob { attachment.deleteOnExit(); SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver(); - SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(0, new SignalServiceAttachmentRemoteId(avatarId), contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, false, Optional.absent(), Optional.absent(), System.currentTimeMillis()); + SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(0, new SignalServiceAttachmentRemoteId(avatarId), contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, false, false, Optional.absent(), Optional.absent(), System.currentTimeMillis()); InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, AvatarHelper.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE); AvatarHelper.setAvatar(context, record.get().getRecipientId(), inputStream); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java index 3a9fb03d77..a61b402883 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java @@ -237,7 +237,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, false, null, null, null, null, null)); + part.getData().length, name, false, false, false, false, null, null, null, null, null)); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java index 32248bccbb..1bc64bb1de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -158,6 +158,7 @@ public abstract class PushSendJob extends SendJob { .withFileName(attachment.getFileName()) .withVoiceNote(attachment.isVoiceNote()) .withBorderless(attachment.isBorderless()) + .withGif(attachment.isVideoGif()) .withWidth(attachment.getWidth()) .withHeight(attachment.getHeight()) .withCaption(attachment.getCaption()) @@ -247,6 +248,7 @@ public abstract class PushSendJob extends SendJob { Optional.fromNullable(attachment.getFileName()), attachment.isVoiceNote(), attachment.isBorderless(), + attachment.isVideoGif(), Optional.fromNullable(attachment.getCaption()), Optional.fromNullable(attachment.getBlurHash()).transform(BlurHash::getHash), attachment.getUploadTimestamp()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java index 0f29f4b9f2..0a048b41ce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java @@ -345,6 +345,7 @@ public class LinkPreviewRepository { false, false, false, + false, null, null, null, diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java index 322542eb6b..d83ad069c8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java @@ -112,6 +112,7 @@ public class MediaPreviewViewModel extends ViewModel { mediaRecord.getAttachment().getSize(), 0, mediaRecord.getAttachment().isBorderless(), + mediaRecord.getAttachment().isVideoGif(), Optional.absent(), Optional.fromNullable(mediaRecord.getAttachment().getCaption()), Optional.absent()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/VideoMediaPreviewFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/VideoMediaPreviewFragment.java index 80e82da878..ff4764db72 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/VideoMediaPreviewFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/VideoMediaPreviewFragment.java @@ -42,7 +42,7 @@ public final class VideoMediaPreviewFragment extends MediaPreviewFragment { videoView = itemView.findViewById(R.id.video_player); videoView.setWindow(requireActivity().getWindow()); - videoView.setVideoSource(new VideoSlide(getContext(), uri, size), autoPlay); + videoView.setVideoSource(new VideoSlide(getContext(), uri, size, false), autoPlay); videoView.setOnClickListener(v -> events.singleTapOnMedia()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java index 267da517ec..e8b892afe1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java @@ -93,6 +93,7 @@ public class AvatarSelectionActivity extends AppCompatActivity implements Camera data.length, 0, false, + false, Optional.of(Media.ALL_MEDIA_BUCKET_ID), Optional.absent(), Optional.absent())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/ImageEditorModelRenderMediaTransform.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/ImageEditorModelRenderMediaTransform.java index 9f1b296474..9434884b79 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/ImageEditorModelRenderMediaTransform.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/ImageEditorModelRenderMediaTransform.java @@ -49,7 +49,7 @@ public final class ImageEditorModelRenderMediaTransform implements MediaTransfor .withMimeType(MediaUtil.IMAGE_JPEG) .createForSingleSessionOnDisk(context); - return new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), 0, false, media.getBucketId(), media.getCaption(), Optional.absent()); + return new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), 0, false, false, media.getBucketId(), media.getCaption(), Optional.absent()); } catch (IOException e) { Log.w(TAG, "Failed to render image. Using base image."); return media; diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/Media.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/Media.java index fbe2525460..804e808b40 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/Media.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/Media.java @@ -27,6 +27,7 @@ public class Media implements Parcelable { private final long size; private final long duration; private final boolean borderless; + private final boolean videoGif; private Optional bucketId; private Optional caption; @@ -40,6 +41,7 @@ public class Media implements Parcelable { long size, long duration, boolean borderless, + boolean videoGif, Optional bucketId, Optional caption, Optional transformProperties) @@ -52,6 +54,7 @@ public class Media implements Parcelable { this.size = size; this.duration = duration; this.borderless = borderless; + this.videoGif = videoGif; this.bucketId = bucketId; this.caption = caption; this.transformProperties = transformProperties; @@ -66,6 +69,7 @@ public class Media implements Parcelable { size = in.readLong(); duration = in.readLong(); borderless = in.readInt() == 1; + videoGif = in.readInt() == 1; bucketId = Optional.fromNullable(in.readString()); caption = Optional.fromNullable(in.readString()); try { @@ -108,6 +112,10 @@ public class Media implements Parcelable { return borderless; } + public boolean isVideoGif() { + return videoGif; + } + public Optional getBucketId() { return bucketId; } @@ -139,6 +147,7 @@ public class Media implements Parcelable { dest.writeLong(size); dest.writeLong(duration); dest.writeInt(borderless ? 1 : 0); + dest.writeInt(videoGif ? 1 : 0); dest.writeString(bucketId.orNull()); dest.writeString(caption.orNull()); dest.writeString(transformProperties.transform(JsonUtil::toJson).orNull()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java index 226978e85c..feccd3b481 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java @@ -242,7 +242,7 @@ public class MediaRepository { long size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE)); long duration = !isImage ? cursor.getInt(cursor.getColumnIndexOrThrow(Video.Media.DURATION)) : 0; - media.add(new Media(uri, mimetype, date, width, height, size, duration, false, Optional.of(bucketId), Optional.absent(), Optional.absent())); + media.add(new Media(uri, mimetype, date, width, height, size, duration, false, false, Optional.of(bucketId), Optional.absent(), Optional.absent())); } } @@ -332,7 +332,7 @@ public class MediaRepository { height = dimens.second; } - return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.isBorderless(), media.getBucketId(), media.getCaption(), Optional.absent()); + return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.isBorderless(), media.isVideoGif(), media.getBucketId(), media.getCaption(), Optional.absent()); } private Media getContentResolverPopulatedMedia(@NonNull Context context, @NonNull Media media) throws IOException { @@ -358,7 +358,7 @@ public class MediaRepository { height = dimens.second; } - return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.isBorderless(), media.getBucketId(), media.getCaption(), Optional.absent()); + return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.isBorderless(), media.isVideoGif(), media.getBucketId(), media.getCaption(), Optional.absent()); } private static class FolderResult { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java index fa59fe2601..0fcd422d8f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java @@ -475,6 +475,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med length, 0, false, + false, Optional.of(Media.ALL_MEDIA_BUCKET_ID), Optional.absent(), Optional.absent()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendVideoFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendVideoFragment.java index 3b2a8683d0..94bd25486c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendVideoFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendVideoFragment.java @@ -79,7 +79,7 @@ public class MediaSendVideoFragment extends Fragment implements VideoEditorHud.E uri = requireArguments().getParcelable(KEY_URI); long maxOutput = requireArguments().getLong(KEY_MAX_OUTPUT); long maxSend = requireArguments().getLong(KEY_MAX_SEND); - VideoSlide slide = new VideoSlide(requireContext(), uri, 0); + VideoSlide slide = new VideoSlide(requireContext(), uri, 0, false); player.setWindow(requireActivity().getWindow()); player.setVideoSource(slide, true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java index 3f235d9998..96065b30c4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java @@ -305,7 +305,7 @@ class MediaSendViewModel extends ViewModel { captionVisible = false; List uncaptioned = Stream.of(getSelectedMediaOrDefault()) - .map(m -> new Media(m.getUri(), m.getMimeType(), m.getDate(), m.getWidth(), m.getHeight(), m.getSize(), m.getDuration(), m.isBorderless(), m.getBucketId(), Optional.absent(), Optional.absent())) + .map(m -> new Media(m.getUri(), m.getMimeType(), m.getDate(), m.getWidth(), m.getHeight(), m.getSize(), m.getDuration(), m.isBorderless(), m.isVideoGif(), m.getBucketId(), Optional.absent(), Optional.absent())) .toList(); selectedMedia.setValue(uncaptioned); @@ -408,7 +408,7 @@ class MediaSendViewModel extends ViewModel { } void onVideoBeginEdit(@NonNull Uri uri) { - cancelUpload(new Media(uri, "", 0, 0, 0, 0, 0, false, Optional.absent(), Optional.absent(), Optional.absent())); + cancelUpload(new Media(uri, "", 0, 0, 0, 0, 0, false, false, Optional.absent(), Optional.absent(), Optional.absent())); } void onMediaCaptured(@NonNull Media media) { @@ -485,7 +485,7 @@ class MediaSendViewModel extends ViewModel { if (splitMessage.getTextSlide().isPresent()) { Slide slide = splitMessage.getTextSlide().get(); - uploadRepository.startUpload(new Media(Objects.requireNonNull(slide.getUri()), slide.getContentType(), System.currentTimeMillis(), 0, 0, slide.getFileSize(), 0, slide.isBorderless(), Optional.absent(), Optional.absent(), Optional.absent()), recipient); + uploadRepository.startUpload(new Media(Objects.requireNonNull(slide.getUri()), slide.getContentType(), System.currentTimeMillis(), 0, 0, slide.getFileSize(), 0, slide.isBorderless(), slide.isVideoGif(), Optional.absent(), Optional.absent(), Optional.absent()), recipient); } uploadRepository.applyMediaUpdates(oldToNew, recipient); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaUploadRepository.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaUploadRepository.java index 0f003d1924..808d581e03 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaUploadRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaUploadRepository.java @@ -191,7 +191,7 @@ class MediaUploadRepository { public static @NonNull Attachment asAttachment(@NonNull Context context, @NonNull Media media) { if (MediaUtil.isVideoType(media.getMimeType())) { - return new VideoSlide(context, media.getUri(), media.getSize(), media.getWidth(), media.getHeight(), media.getCaption().orNull(), media.getTransformProperties().orNull()).asAttachment(); + return new VideoSlide(context, media.getUri(), media.getSize(), media.isVideoGif(), media.getWidth(), media.getHeight(), media.getCaption().orNull(), media.getTransformProperties().orNull()).asAttachment(); } else if (MediaUtil.isGif(media.getMimeType())) { return new GifSlide(context, media.getUri(), media.getSize(), media.getWidth(), media.getHeight(), media.isBorderless(), media.getCaption().orNull()).asAttachment(); } else if (MediaUtil.isImageType(media.getMimeType())) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/VideoTrimTransform.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/VideoTrimTransform.java index 58a5ed7ca8..f2711f588d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/VideoTrimTransform.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/VideoTrimTransform.java @@ -27,6 +27,7 @@ public final class VideoTrimTransform implements MediaTransform { media.getSize(), media.getDuration(), media.isBorderless(), + media.isVideoGif(), media.getBucketId(), media.getCaption(), Optional.of(new AttachmentDatabase.TransformProperties(false, data.durationEdited, data.startTimeUs, data.endTimeUs))); diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java index e071832ad5..1928f08e91 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java @@ -1679,6 +1679,7 @@ public final class MessageContentProcessor { false, false, false, + false, null, stickerLocator, null, diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java index bd0b3c3e52..dc0c5bc524 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -314,7 +314,7 @@ public class AttachmentManager { } Log.d(TAG, "remote slide with size " + fileSize + " took " + (System.currentTimeMillis() - start) + "ms"); - return mediaType.createSlide(context, uri, fileName, mimeType, null, fileSize, width, height); + return mediaType.createSlide(context, uri, fileName, mimeType, null, fileSize, width, height, false); } } finally { if (cursor != null) cursor.close(); @@ -328,11 +328,13 @@ public class AttachmentManager { Long mediaSize = null; String fileName = null; String mimeType = null; + boolean gif = false; if (PartAuthority.isLocalUri(uri)) { mediaSize = PartAuthority.getAttachmentSize(context, uri); fileName = PartAuthority.getAttachmentFileName(context, uri); mimeType = PartAuthority.getAttachmentContentType(context, uri); + gif = PartAuthority.getAttachmentIsVideoGif(context, uri); } if (mediaSize == null) { @@ -350,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, null, mediaSize, width, height); + return mediaType.createSlide(context, uri, fileName, mimeType, null, mediaSize, width, height, gif); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java index cfaa6dd01a..3411cef5ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java @@ -33,11 +33,11 @@ import org.thoughtcrime.securesms.util.MediaUtil; 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, null, null, voiceNote, false, false)); + super(context, constructAttachmentFromUri(context, uri, MediaUtil.AUDIO_UNSPECIFIED, dataSize, 0, 0, false, null, null, null, null, null, voiceNote, false, false, false)); } public AudioSlide(Context context, Uri uri, long dataSize, String contentType, boolean voiceNote) { - super(context, new UriAttachment(uri, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, false, null, null, null, null, null)); + super(context, new UriAttachment(uri, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, false, false, null, null, null, null, null)); } public AudioSlide(Context context, Attachment attachment) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/DocumentSlide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/DocumentSlide.java index 43873718ba..424451d9e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/DocumentSlide.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/DocumentSlide.java @@ -20,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, null, null, false, false, false)); + super(context, constructAttachmentFromUri(context, uri, contentType, size, 0, 0, true, StorageUtil.getCleanFileName(fileName), null, null, null, null, false, false, false, false)); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/GifSlide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/GifSlide.java index 3e303c6281..ec840e605b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/GifSlide.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/GifSlide.java @@ -22,7 +22,7 @@ public class GifSlide extends ImageSlide { } public GifSlide(Context context, Uri uri, long size, int width, int height, boolean borderless, @Nullable String caption) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_GIF, size, width, height, true, null, caption, null, null, null, false, borderless, false)); + super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_GIF, size, width, height, true, null, caption, null, null, null, false, borderless, false, false)); this.borderless = borderless; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/ImageSlide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/ImageSlide.java index ebec7d3f9d..c3f88b3f92 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/ImageSlide.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/ImageSlide.java @@ -47,7 +47,7 @@ public class ImageSlide extends Slide { } public ImageSlide(Context context, Uri uri, String contentType, long size, int width, int height, boolean borderless, @Nullable String caption, @Nullable BlurHash blurHash) { - super(context, constructAttachmentFromUri(context, uri, contentType, size, width, height, true, null, caption, null, blurHash, null, false, borderless, false)); + super(context, constructAttachmentFromUri(context, uri, contentType, size, width, height, true, null, caption, null, blurHash, null, false, borderless, false, false)); this.borderless = borderless; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java b/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java index 57f3056804..cfa1be3406 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java @@ -126,6 +126,20 @@ public class PartAuthority { } } + public static boolean getAttachmentIsVideoGif(@NonNull Context context, @NonNull Uri uri) { + int match = uriMatcher.match(uri); + + switch (match) { + case PART_ROW: + Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(new PartUriParser(uri).getPartId()); + + if (attachment != null) return attachment.isVideoGif(); + else return false; + default: + return false; + } + } + public static Uri getAttachmentPublicUri(Uri uri) { PartUriParser partUri = new PartUriParser(uri); return PartProvider.getContentUri(partUri.getPartId()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.java index 7cdc34cd3d..1a79cb87b5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.java @@ -118,6 +118,10 @@ public abstract class Slide { return false; } + public boolean isVideoGif() { + return hasVideo() && attachment.isVideoGif(); + } + public @NonNull String getContentDescription() { return ""; } public @NonNull Attachment asAttachment() { @@ -167,9 +171,10 @@ public abstract class Slide { @Nullable AudioHash audioHash, boolean voiceNote, boolean borderless, + boolean gif, boolean quote) { - return constructAttachmentFromUri(context, uri, defaultMime, size, width, height, hasThumbnail, fileName, caption, stickerLocator, blurHash, audioHash, voiceNote, borderless, quote, null); + return constructAttachmentFromUri(context, uri, defaultMime, size, width, height, hasThumbnail, fileName, caption, stickerLocator, blurHash, audioHash, voiceNote, borderless, gif, quote, null); } protected static Attachment constructAttachmentFromUri(@NonNull Context context, @@ -186,6 +191,7 @@ public abstract class Slide { @Nullable AudioHash audioHash, boolean voiceNote, boolean borderless, + boolean gif, boolean quote, @Nullable AttachmentDatabase.TransformProperties transformProperties) { @@ -201,6 +207,7 @@ public abstract class Slide { fastPreflightId, voiceNote, borderless, + gif, quote, caption, stickerLocator, diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/SlideFactory.java b/app/src/main/java/org/thoughtcrime/securesms/mms/SlideFactory.java index 423e6fb22a..5d4822bf2a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/SlideFactory.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/SlideFactory.java @@ -77,7 +77,7 @@ public final class SlideFactory { } Log.d(TAG, "remote slide with size " + fileSize + " took " + (System.currentTimeMillis() - start) + "ms"); - return mediaType.createSlide(context, uri, fileName, mimeType, null, fileSize, width, height); + return mediaType.createSlide(context, uri, fileName, mimeType, null, fileSize, width, height, false); } } finally { if (cursor != null) cursor.close(); @@ -91,11 +91,13 @@ public final class SlideFactory { Long mediaSize = null; String fileName = null; String mimeType = null; + boolean gif = false; if (PartAuthority.isLocalUri(uri)) { mediaSize = PartAuthority.getAttachmentSize(context, uri); fileName = PartAuthority.getAttachmentFileName(context, uri); mimeType = PartAuthority.getAttachmentContentType(context, uri); + gif = PartAuthority.getAttachmentIsVideoGif(context, uri); } if (mediaSize == null) { @@ -113,7 +115,7 @@ public final class SlideFactory { } Log.d(TAG, "local slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms"); - return mediaType.createSlide(context, uri, fileName, mimeType, null, mediaSize, width, height); + return mediaType.createSlide(context, uri, fileName, mimeType, null, mediaSize, width, height, gif); } public enum MediaType { @@ -139,7 +141,8 @@ public final class SlideFactory { @Nullable BlurHash blurHash, long dataSize, int width, - int height) + int height, + boolean gif) { if (mimeType == null) { mimeType = "application/octet-stream"; @@ -149,7 +152,7 @@ public final class SlideFactory { 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); + case VIDEO: return new VideoSlide(context, uri, dataSize, gif); case VCARD: case DOCUMENT: return new DocumentSlide(context, uri, mimeType, dataSize, fileName); default: throw new AssertionError("unrecognized enum"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/StickerSlide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/StickerSlide.java index ded0f36a13..0c93aeaf8e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/StickerSlide.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/StickerSlide.java @@ -27,7 +27,7 @@ public class StickerSlide extends Slide { } public StickerSlide(Context context, Uri uri, long size, @NonNull StickerLocator stickerLocator, @NonNull String contentType) { - super(context, constructAttachmentFromUri(context, uri, contentType, size, WIDTH, HEIGHT, true, null, null, stickerLocator, null, null, false, false, false)); + super(context, constructAttachmentFromUri(context, uri, contentType, size, WIDTH, HEIGHT, true, null, null, stickerLocator, null, null, false, false, false, false)); this.stickerLocator = Objects.requireNonNull(attachment.getSticker()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/TextSlide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/TextSlide.java index 01626ad530..75ea2cb099 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/TextSlide.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/TextSlide.java @@ -17,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, null, null, false, false, false)); + super(context, constructAttachmentFromUri(context, uri, MediaUtil.LONG_TEXT, size, 0, 0, true, filename, null, null, null, null, false, false, false, false)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/VideoSlide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/VideoSlide.java index 9a83923767..ff92d00cc7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/VideoSlide.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/VideoSlide.java @@ -31,16 +31,16 @@ import org.thoughtcrime.securesms.util.MediaUtil; public class VideoSlide extends Slide { - public VideoSlide(Context context, Uri uri, long dataSize) { - this(context, uri, dataSize, null, null); + public VideoSlide(Context context, Uri uri, long dataSize, boolean gif) { + this(context, uri, dataSize, gif, null, null); } - public VideoSlide(Context context, Uri uri, long dataSize, @Nullable String caption, @Nullable AttachmentDatabase.TransformProperties transformProperties) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, 0, 0, MediaUtil.hasVideoThumbnail(context, uri), null, caption, null, null, null, false, false, false, transformProperties)); + public VideoSlide(Context context, Uri uri, long dataSize, boolean gif, @Nullable String caption, @Nullable AttachmentDatabase.TransformProperties transformProperties) { + super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, 0, 0, MediaUtil.hasVideoThumbnail(context, uri), null, caption, null, null, null, false, false, gif, false, transformProperties)); } - public VideoSlide(Context context, Uri uri, long dataSize, int width, int height, @Nullable String caption, @Nullable AttachmentDatabase.TransformProperties transformProperties) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, width, height, MediaUtil.hasVideoThumbnail(context, uri), null, caption, null, null, null, false, false, false, transformProperties)); + public VideoSlide(Context context, Uri uri, long dataSize, boolean gif, int width, int height, @Nullable String caption, @Nullable AttachmentDatabase.TransformProperties transformProperties) { + super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, width, height, MediaUtil.hasVideoThumbnail(context, uri), null, caption, null, null, null, false, false, gif, false, transformProperties)); } public VideoSlide(Context context, Attachment attachment) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageActivity.java b/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageActivity.java index fbf4518e99..d89c700177 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageActivity.java @@ -127,7 +127,7 @@ public class ViewOnceMessageActivity extends PassphraseRequiredActivity implemen image.setVisibility(View.GONE); duration.setVisibility(View.VISIBLE); - VideoSlide videoSlide = new VideoSlide(this, uri, 0); + VideoSlide videoSlide = new VideoSlide(this, uri, 0, false); video.setWindow(getWindow()); video.setPlayerStateCallbacks(this); diff --git a/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareActivity.java b/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareActivity.java index 24912b94b3..58cd78a32c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareActivity.java @@ -549,6 +549,7 @@ public class ShareActivity extends PassphraseRequiredActivity 0, 0, false, + false, Optional.absent(), Optional.absent(), Optional.absent())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareRepository.java b/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareRepository.java index ae3e2c7a33..9222f772c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareRepository.java @@ -189,6 +189,7 @@ class ShareRepository { size, duration, false, + false, Optional.of(Media.ALL_MEDIA_BUCKET_ID), Optional.absent(), Optional.absent())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index deba0cb835..ab79e74f87 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -77,6 +77,7 @@ public final class FeatureFlags { private static final String MESSAGE_PROCESSOR_DELAY = "android.messageProcessor.foregroundDelayMs"; private static final String STORAGE_SYNC_V2 = "android.storageSyncV2.3"; private static final String NOTIFICATION_REWRITE = "android.notificationRewrite"; + private static final String MP4_GIF_SEND_SUPPORT = "android.mp4GifSendSupport"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable @@ -109,7 +110,8 @@ public final class FeatureFlags { MESSAGE_PROCESSOR_ALARM_INTERVAL, MESSAGE_PROCESSOR_DELAY, STORAGE_SYNC_V2, - NOTIFICATION_REWRITE + NOTIFICATION_REWRITE, + MP4_GIF_SEND_SUPPORT ); @VisibleForTesting @@ -154,7 +156,8 @@ public final class FeatureFlags { MESSAGE_PROCESSOR_DELAY, GV1_FORCED_MIGRATE, STORAGE_SYNC_V2, - NOTIFICATION_REWRITE + NOTIFICATION_REWRITE, + MP4_GIF_SEND_SUPPORT ); /** @@ -351,6 +354,10 @@ public final class FeatureFlags { return getBoolean(NOTIFICATION_REWRITE, false) && Build.VERSION.SDK_INT >= 26; } + public static boolean mp4GifSendSupport() { + return getBoolean(MP4_GIF_SEND_SUPPORT, false); + } + /** Only for rendering debug info. */ public static synchronized @NonNull Map getMemoryValues() { return new TreeMap<>(REMOTE_VALUES); diff --git a/app/src/main/java/org/thoughtcrime/securesms/video/VideoPlayer.java b/app/src/main/java/org/thoughtcrime/securesms/video/VideoPlayer.java index 54bd96ca1e..65a1441796 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/video/VideoPlayer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/video/VideoPlayer.java @@ -41,6 +41,7 @@ import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -60,12 +61,13 @@ public class VideoPlayer extends FrameLayout { private final PlayerView exoView; private final PlayerControlView exoControls; - private SimpleExoPlayer exoPlayer; - private Window window; - private PlayerStateCallback playerStateCallback; - private PlayerCallback playerCallback; - private boolean clipped; - private long clippedStartUs; + private SimpleExoPlayer exoPlayer; + private Window window; + private PlayerStateCallback playerStateCallback; + private PlayerPositionDiscontinuityCallback playerPositionDiscontinuityCallback; + private PlayerCallback playerCallback; + private boolean clipped; + private long clippedStartUs; public VideoPlayer(Context context) { this(context, null); @@ -94,25 +96,27 @@ public class VideoPlayer extends FrameLayout { TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); LoadControl loadControl = new DefaultLoadControl(); - exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector, loadControl); - exoPlayer.addListener(new ExoPlayerListener(window, playerStateCallback)); - exoPlayer.addListener(new Player.DefaultEventListener() { - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playerCallback != null) { - switch (playbackState) { - case Player.STATE_READY: - if (playWhenReady) playerCallback.onPlaying(); - break; - case Player.STATE_ENDED: - playerCallback.onStopped(); - break; + if (exoPlayer == null) { + exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector, loadControl); + exoPlayer.addListener(new ExoPlayerListener(this, window, playerStateCallback, playerPositionDiscontinuityCallback)); + exoPlayer.addListener(new Player.EventListener() { + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (playerCallback != null) { + switch (playbackState) { + case Player.STATE_READY: + if (playWhenReady) playerCallback.onPlaying(); + break; + case Player.STATE_ENDED: + playerCallback.onStopped(); + break; + } } } - } - }); - exoView.setPlayer(exoPlayer); - exoControls.setPlayer(exoPlayer); + }); + exoView.setPlayer(exoPlayer); + exoControls.setPlayer(exoPlayer); + } DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(context, "GenericUserAgent", null); AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(context, defaultDataSourceFactory, null); @@ -126,8 +130,18 @@ public class VideoPlayer extends FrameLayout { exoPlayer.setPlayWhenReady(autoplay); } + public boolean isInitialized() { + return exoPlayer != null; + } + + public void setResizeMode(@AspectRatioFrameLayout.ResizeMode int resizeMode) { + exoView.setResizeMode(resizeMode); + } + public void pause() { - this.exoPlayer.setPlayWhenReady(false); + if (this.exoPlayer != null) { + this.exoPlayer.setPlayWhenReady(false); + } } public void hideControls() { @@ -146,6 +160,7 @@ public class VideoPlayer extends FrameLayout { public void cleanup() { if (this.exoPlayer != null) { this.exoPlayer.release(); + this.exoPlayer = null; } } @@ -196,7 +211,7 @@ public class VideoPlayer extends FrameLayout { if (exoPlayer != null && createMediaSource != null) { if (clipped) { exoPlayer.prepare(createMediaSource.create()); - clipped = false; + clipped = false; clippedStartUs = 0; } exoPlayer.setPlayWhenReady(playWhenReady); @@ -215,6 +230,10 @@ public class VideoPlayer extends FrameLayout { this.playerCallback = playerCallback; } + public void setPlayerPositionDiscontinuityCallback(@NonNull PlayerPositionDiscontinuityCallback playerPositionDiscontinuityCallback) { + this.playerPositionDiscontinuityCallback = playerPositionDiscontinuityCallback; + } + /** * Resumes a paused video, or restarts if at end of video. */ @@ -227,28 +246,40 @@ public class VideoPlayer extends FrameLayout { } } - private static class ExoPlayerListener extends Player.DefaultEventListener { - private final Window window; - private final PlayerStateCallback playerStateCallback; + private static class ExoPlayerListener implements Player.EventListener { + private final VideoPlayer videoPlayer; + private final Window window; + private final PlayerStateCallback playerStateCallback; + private final PlayerPositionDiscontinuityCallback playerPositionDiscontinuityCallback; - ExoPlayerListener(Window window, PlayerStateCallback playerStateCallback) { - this.window = window; - this.playerStateCallback = playerStateCallback; + ExoPlayerListener(@NonNull VideoPlayer videoPlayer, + @Nullable Window window, + @Nullable PlayerStateCallback playerStateCallback, + @Nullable PlayerPositionDiscontinuityCallback playerPositionDiscontinuityCallback) + { + this.videoPlayer = videoPlayer; + this.window = window; + this.playerStateCallback = playerStateCallback; + this.playerPositionDiscontinuityCallback = playerPositionDiscontinuityCallback; } @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - switch(playbackState) { + switch (playbackState) { case Player.STATE_IDLE: case Player.STATE_BUFFERING: case Player.STATE_ENDED: - window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + if (window != null) { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } break; case Player.STATE_READY: - if (playWhenReady) { - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } else { - window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + if (window != null) { + if (playWhenReady) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } } notifyPlayerReady(); break; @@ -257,6 +288,13 @@ public class VideoPlayer extends FrameLayout { } } + @Override + public void onPositionDiscontinuity(int reason) { + if (playerPositionDiscontinuityCallback != null) { + playerPositionDiscontinuityCallback.onPositionDiscontinuity(videoPlayer, reason); + } + } + private void notifyPlayerReady() { if (playerStateCallback != null) playerStateCallback.onPlayerReady(); } @@ -266,6 +304,10 @@ public class VideoPlayer extends FrameLayout { void onPlayerReady(); } + public interface PlayerPositionDiscontinuityCallback { + void onPositionDiscontinuity(@NonNull VideoPlayer player, int reason); + } + public interface PlayerCallback { void onPlaying(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/video/exo/ChunkedDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/video/exo/ChunkedDataSource.java new file mode 100644 index 0000000000..78621f87e4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/video/exo/ChunkedDataSource.java @@ -0,0 +1,120 @@ +package org.thoughtcrime.securesms.video.exo; + +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.TransferListener; + +import org.thoughtcrime.securesms.net.ChunkedDataFetcher; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import okhttp3.OkHttpClient; + +/** + * DataSource which utilizes ChunkedDataFetcher to download video content via Signal content proxy. + */ +public class ChunkedDataSource implements DataSource { + + private final OkHttpClient okHttpClient; + private final TransferListener transferListener; + + private Uri uri; + private volatile InputStream inputStream; + private volatile Exception exception; + + ChunkedDataSource(@NonNull OkHttpClient okHttpClient, @Nullable TransferListener listener) { + this.okHttpClient = okHttpClient; + this.transferListener = listener; + } + + @Override + public void addTransferListener(TransferListener transferListener) { + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + this.uri = dataSpec.uri; + this.exception = null; + + if (inputStream != null) { + inputStream.close(); + } + + this.inputStream = null; + + CountDownLatch countDownLatch = new CountDownLatch(1); + ChunkedDataFetcher fetcher = new ChunkedDataFetcher(okHttpClient); + + fetcher.fetch(this.uri.toString(), dataSpec.length, new ChunkedDataFetcher.Callback() { + @Override + public void onSuccess(InputStream stream) { + inputStream = stream; + countDownLatch.countDown(); + } + + @Override + public void onFailure(Exception e) { + exception = e; + countDownLatch.countDown(); + } + }); + + try { + countDownLatch.await(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new IOException(e); + } + + if (exception != null) { + throw new IOException(exception); + } + + if (inputStream == null) { + throw new IOException("Timed out waiting for input stream"); + } + + if (transferListener != null) { + transferListener.onTransferStart(this, dataSpec, false); + } + + if ( dataSpec.length != C.LENGTH_UNSET && dataSpec.length - dataSpec.position <= 0) { + throw new EOFException("No more data"); + } + + return dataSpec.length; + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws IOException { + int read = inputStream.read(buffer, offset, readLength); + + if (read > 0 && transferListener != null) { + transferListener.onBytesTransferred(this, null, false, read); + } + + return read; + } + + @Override + public @Nullable Uri getUri() { + return uri; + } + + @Override + public void close() throws IOException { + if (inputStream != null) { + inputStream.close(); + } + } + +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/video/exo/ChunkedDataSourceFactory.java b/app/src/main/java/org/thoughtcrime/securesms/video/exo/ChunkedDataSourceFactory.java new file mode 100644 index 0000000000..603e533c6a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/video/exo/ChunkedDataSourceFactory.java @@ -0,0 +1,26 @@ +package org.thoughtcrime.securesms.video.exo; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.TransferListener; + +import okhttp3.OkHttpClient; + +public class ChunkedDataSourceFactory implements DataSource.Factory { + + private final OkHttpClient okHttpClient; + private final TransferListener listener; + + public ChunkedDataSourceFactory(@NonNull OkHttpClient okHttpClient, @Nullable TransferListener listener) { + this.okHttpClient = okHttpClient; + this.listener = listener; + } + + + @Override + public DataSource createDataSource() { + return new ChunkedDataSource(okHttpClient, listener); + } +} diff --git a/app/src/main/res/drawable/ic_gif_outline_24.xml b/app/src/main/res/drawable/ic_gif_outline_24.xml new file mode 100644 index 0000000000..2846fdf431 --- /dev/null +++ b/app/src/main/res/drawable/ic_gif_outline_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_gif_solid_24.xml b/app/src/main/res/drawable/ic_gif_solid_24.xml new file mode 100644 index 0000000000..4f7fd0d40b --- /dev/null +++ b/app/src/main/res/drawable/ic_gif_solid_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/gif_player.xml b/app/src/main/res/layout/gif_player.xml new file mode 100644 index 0000000000..a345f9e51d --- /dev/null +++ b/app/src/main/res/layout/gif_player.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/giphy_mp4.xml b/app/src/main/res/layout/giphy_mp4.xml new file mode 100644 index 0000000000..dd985c0f65 --- /dev/null +++ b/app/src/main/res/layout/giphy_mp4.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/layout/giphy_mp4_fragment.xml b/app/src/main/res/layout/giphy_mp4_fragment.xml new file mode 100644 index 0000000000..01c2817e62 --- /dev/null +++ b/app/src/main/res/layout/giphy_mp4_fragment.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/giphy_mp4_keyboard_icon.xml b/app/src/main/res/layout/giphy_mp4_keyboard_icon.xml new file mode 100644 index 0000000000..69b45d0738 --- /dev/null +++ b/app/src/main/res/layout/giphy_mp4_keyboard_icon.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/giphy_mp4_keyboard_icon_selected.xml b/app/src/main/res/layout/giphy_mp4_keyboard_icon_selected.xml new file mode 100644 index 0000000000..e90e9ac75c --- /dev/null +++ b/app/src/main/res/layout/giphy_mp4_keyboard_icon_selected.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/giphy_mp4_player.xml b/app/src/main/res/layout/giphy_mp4_player.xml new file mode 100644 index 0000000000..9cba24e111 --- /dev/null +++ b/app/src/main/res/layout/giphy_mp4_player.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/app/src/main/res/layout/thumbnail_player_stub.xml b/app/src/main/res/layout/thumbnail_player_stub.xml new file mode 100644 index 0000000000..549be2c0b2 --- /dev/null +++ b/app/src/main/res/layout/thumbnail_player_stub.xml @@ -0,0 +1,7 @@ + + diff --git a/app/src/main/res/layout/thumbnail_view.xml b/app/src/main/res/layout/thumbnail_view.xml index 9d3be4d75c..83f7facea4 100644 --- a/app/src/main/res/layout/thumbnail_view.xml +++ b/app/src/main/res/layout/thumbnail_view.xml @@ -24,6 +24,15 @@ android:scaleType="fitCenter" android:contentDescription="@string/conversation_item__mms_image_description" /> + + absent(), width, height, diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentPointer.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentPointer.java index fa97261c81..4cd57af1db 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentPointer.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentPointer.java @@ -27,6 +27,7 @@ public class SignalServiceAttachmentPointer extends SignalServiceAttachment { private final Optional fileName; private final boolean voiceNote; private final boolean borderless; + private final boolean gif; private final int width; private final int height; private final Optional caption; @@ -45,6 +46,7 @@ public class SignalServiceAttachmentPointer extends SignalServiceAttachment { Optional fileName, boolean voiceNote, boolean borderless, + boolean gif, Optional caption, Optional blurHash, long uploadTimestamp) @@ -64,6 +66,7 @@ public class SignalServiceAttachmentPointer extends SignalServiceAttachment { this.caption = caption; this.blurHash = blurHash; this.uploadTimestamp = uploadTimestamp; + this.gif = gif; } public int getCdnNumber() { @@ -112,6 +115,10 @@ public class SignalServiceAttachmentPointer extends SignalServiceAttachment { return borderless; } + public boolean isGif() { + return gif; + } + public int getWidth() { return width; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentStream.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentStream.java index 3c4fd0f737..afea6e9351 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentStream.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentStream.java @@ -25,6 +25,7 @@ public class SignalServiceAttachmentStream extends SignalServiceAttachment { private final Optional preview; private final boolean voiceNote; private final boolean borderless; + private final boolean gif; private final int width; private final int height; private final long uploadTimestamp; @@ -38,10 +39,11 @@ public class SignalServiceAttachmentStream extends SignalServiceAttachment { Optional fileName, boolean voiceNote, boolean borderless, + boolean gif, ProgressListener listener, CancelationSignal cancelationSignal) { - this(inputStream, contentType, length, fileName, voiceNote, borderless, Optional.absent(), 0, 0, System.currentTimeMillis(), Optional.absent(), Optional.absent(), listener, cancelationSignal, Optional.absent()); + this(inputStream, contentType, length, fileName, voiceNote, borderless, gif, Optional.absent(), 0, 0, System.currentTimeMillis(), Optional.absent(), Optional.absent(), listener, cancelationSignal, Optional.absent()); } public SignalServiceAttachmentStream(InputStream inputStream, @@ -50,6 +52,7 @@ public class SignalServiceAttachmentStream extends SignalServiceAttachment { Optional fileName, boolean voiceNote, boolean borderless, + boolean gif, Optional preview, int width, int height, @@ -67,6 +70,7 @@ public class SignalServiceAttachmentStream extends SignalServiceAttachment { this.listener = listener; this.voiceNote = voiceNote; this.borderless = borderless; + this.gif = gif; this.preview = preview; this.width = width; this.height = height; @@ -119,6 +123,10 @@ public class SignalServiceAttachmentStream extends SignalServiceAttachment { return borderless; } + public boolean isGif() { + return gif; + } + public int getWidth() { return width; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java index 782cd566c9..40307544ad 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java @@ -978,6 +978,7 @@ public final class SignalServiceContent { pointer.hasFileName() ? Optional.of(pointer.getFileName()) : Optional.absent(), (pointer.getFlags() & SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE) != 0, (pointer.getFlags() & SignalServiceProtos.AttachmentPointer.Flags.BORDERLESS_VALUE) != 0, + (pointer.getFlags() & SignalServiceProtos.AttachmentPointer.Flags.GIF_VALUE) != 0, pointer.hasCaption() ? Optional.of(pointer.getCaption()) : Optional.absent(), pointer.hasBlurHash() ? Optional.of(pointer.getBlurHash()) : Optional.absent(), pointer.hasUploadTimestamp() ? pointer.getUploadTimestamp() : 0); @@ -1037,6 +1038,7 @@ public final class SignalServiceContent { Optional.absent(), false, false, + false, Optional.absent(), Optional.absent(), pointer.hasUploadTimestamp() ? pointer.getUploadTimestamp() : 0); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceContactsInputStream.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceContactsInputStream.java index 80007765b9..57cf637576 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceContactsInputStream.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceContactsInputStream.java @@ -57,7 +57,7 @@ public class DeviceContactsInputStream extends ChunkedInputStream { InputStream avatarStream = new LimitedInputStream(in, avatarLength); String avatarContentType = details.getAvatar().getContentType(); - avatar = Optional.of(new SignalServiceAttachmentStream(avatarStream, avatarContentType, avatarLength, Optional.absent(), false, false, null, null)); + avatar = Optional.of(new SignalServiceAttachmentStream(avatarStream, avatarContentType, avatarLength, Optional.absent(), false, false, false, null, null)); } if (details.hasVerified()) { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceGroupsInputStream.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceGroupsInputStream.java index 29a758d3db..de228f2d18 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceGroupsInputStream.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceGroupsInputStream.java @@ -52,7 +52,7 @@ public class DeviceGroupsInputStream extends ChunkedInputStream{ InputStream avatarStream = new ChunkedInputStream.LimitedInputStream(in, avatarLength); String avatarContentType = details.getAvatar().getContentType(); - avatar = Optional.of(new SignalServiceAttachmentStream(avatarStream, avatarContentType, avatarLength, Optional.absent(), false, false, null, null)); + avatar = Optional.of(new SignalServiceAttachmentStream(avatarStream, avatarContentType, avatarLength, Optional.absent(), false, false, false, null, null)); } if (details.hasExpireTimer() && details.getExpireTimer() > 0) { diff --git a/libsignal/service/src/main/proto/SignalService.proto b/libsignal/service/src/main/proto/SignalService.proto index ef461cf3bb..b4903be734 100644 --- a/libsignal/service/src/main/proto/SignalService.proto +++ b/libsignal/service/src/main/proto/SignalService.proto @@ -501,6 +501,7 @@ message AttachmentPointer { enum Flags { VOICE_MESSAGE = 1; BORDERLESS = 2; + GIF = 3; } oneof attachment_identifier {