Render gifs in gif search as MP4s.
This commit is contained in:
parent
fcc5db2fe6
commit
c31146e902
94 changed files with 2062 additions and 273 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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> 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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<Boolean> initializeDraftFromDatabase() {
|
||||
SettableFuture<Boolean> future = new SettableFuture<>();
|
||||
|
||||
|
@ -2444,10 +2444,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
//////// Helper Methods
|
||||
|
||||
private ListenableFuture<Boolean> setMedia(@Nullable Uri uri, @NonNull MediaType mediaType) {
|
||||
return setMedia(uri, mediaType, 0, 0, false);
|
||||
return setMedia(uri, mediaType, 0, 0, false, false);
|
||||
}
|
||||
|
||||
private ListenableFuture<Boolean> setMedia(@Nullable Uri uri, @NonNull MediaType mediaType, int width, int height, boolean borderless) {
|
||||
private ListenableFuture<Boolean> 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;
|
||||
|
|
|
@ -872,6 +872,7 @@ public class ConversationFragment extends LoggingFragment {
|
|||
attachment.getSize(),
|
||||
0,
|
||||
attachment.isBorderless(),
|
||||
attachment.isVideoGif(),
|
||||
Optional.absent(),
|
||||
Optional.fromNullable(attachment.getCaption()),
|
||||
Optional.absent()));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 + ", "
|
||||
|
|
|
@ -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 = ?";
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -10,8 +10,14 @@ public class GiphyResponse {
|
|||
@JsonProperty
|
||||
private List<GiphyImage> data;
|
||||
|
||||
@JsonProperty
|
||||
private GiphyPagination pagination;
|
||||
|
||||
public List<GiphyImage> getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public GiphyPagination getPagination() {
|
||||
return pagination;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<GiphyImage, GiphyMp4ViewHolder> {
|
||||
|
||||
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<GiphyImage> {
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
* <p>
|
||||
* 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<GiphyMp4ViewHolder> 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<GiphyMp4ViewHolder> holders, @NonNull GiphyMp4PlaybackRange range);
|
||||
}
|
||||
}
|
|
@ -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<GiphyMp4PlayerHolder> holders;
|
||||
private final SparseArray<GiphyMp4PlayerHolder> playing;
|
||||
private final SparseArray<GiphyMp4PlayerHolder> notPlaying;
|
||||
|
||||
GiphyMp4AdapterPlaybackControllerCallback(@NonNull List<GiphyMp4PlayerHolder> holders) {
|
||||
this.holders = holders;
|
||||
this.playing = new SparseArray<>(holders.size());
|
||||
this.notPlaying = new SparseArray<>(holders.size());
|
||||
}
|
||||
|
||||
@Override public void update(@NonNull List<GiphyMp4ViewHolder> 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<Integer> 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<GiphyMp4PlayerHolder> 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<GiphyMp4PlayerHolder> injectVideoViews(@NonNull ViewGroup viewGroup) {
|
||||
int nPlayers = GiphyMp4PlaybackPolicy.maxSimultaneousPlaybackInSearchResults();
|
||||
List<GiphyMp4PlayerHolder> 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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) { }
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<GiphyImage> {
|
||||
|
||||
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<GiphyImage> 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<GiphyMp4SaveResult> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<GiphyImage>> pagedData;
|
||||
private final LiveData<List<GiphyImage>> images;
|
||||
private final LiveData<PagingController> pagingController;
|
||||
private final SingleLiveEvent<GiphyMp4SaveResult> saveResultEvents;
|
||||
private final MutableLiveData<Boolean> 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<GiphyMp4SaveResult> getSaveResultEvents() {
|
||||
return saveResultEvents;
|
||||
}
|
||||
|
||||
public @NonNull LiveData<List<GiphyImage>> getImages() {
|
||||
return images;
|
||||
}
|
||||
|
||||
public @NonNull LiveData<PagingController> getPagingController() {
|
||||
return pagingController;
|
||||
}
|
||||
|
||||
public @NonNull LiveData<Boolean> isGridMode() {
|
||||
return isGridMode;
|
||||
}
|
||||
|
||||
private PagedData<GiphyImage> 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 extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
return Objects.requireNonNull(modelClass.cast(new GiphyMp4ViewModel(isForMms)));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<List<GiphyImage>> onCreateLoader(int id, Bundle args) {
|
||||
return new GiphyGifLoader(getActivity(), searchString);
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -345,6 +345,7 @@ public class LinkPreviewRepository {
|
|||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<String> bucketId;
|
||||
private Optional<String> caption;
|
||||
|
@ -40,6 +41,7 @@ public class Media implements Parcelable {
|
|||
long size,
|
||||
long duration,
|
||||
boolean borderless,
|
||||
boolean videoGif,
|
||||
Optional<String> bucketId,
|
||||
Optional<String> caption,
|
||||
Optional<AttachmentDatabase.TransformProperties> 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<String> 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());
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -305,7 +305,7 @@ class MediaSendViewModel extends ViewModel {
|
|||
captionVisible = false;
|
||||
|
||||
List<Media> 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);
|
||||
|
|
|
@ -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())) {
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -1679,6 +1679,7 @@ public final class MessageContentProcessor {
|
|||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
stickerLocator,
|
||||
null,
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -549,6 +549,7 @@ public class ShareActivity extends PassphraseRequiredActivity
|
|||
0,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent()));
|
||||
|
|
|
@ -189,6 +189,7 @@ class ShareRepository {
|
|||
size,
|
||||
duration,
|
||||
false,
|
||||
false,
|
||||
Optional.of(Media.ALL_MEDIA_BUCKET_ID),
|
||||
Optional.absent(),
|
||||
Optional.absent()));
|
||||
|
|
|
@ -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<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
9
app/src/main/res/drawable/ic_gif_outline_24.xml
Normal file
9
app/src/main/res/drawable/ic_gif_outline_24.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/signal_icon_tint_primary"
|
||||
android:pathData="M14.31,3.5c2.55,0 3.23,0.26 3.91,0.62a4,4 0,0 1,1.66 1.66c0.36,0.68 0.62,1.36 0.62,3.91v4.62c0,2.55 -0.26,3.23 -0.62,3.91a4,4 0,0 1,-1.66 1.66c-0.68,0.36 -1.36,0.62 -3.91,0.62L9.69,20.5c-2.55,0 -3.23,-0.26 -3.91,-0.62a4,4 0,0 1,-1.66 -1.66c-0.36,-0.68 -0.62,-1.36 -0.62,-3.91L3.5,9.69c0,-2.55 0.26,-3.23 0.62,-3.91A4,4 0,0 1,5.78 4.12c0.68,-0.36 1.36,-0.62 3.91,-0.62h4.62m0,-1.5L9.69,2C7,2 6.05,2.28 5.07,2.8A5.52,5.52 0,0 0,2.8 5.07C2.28,6.05 2,7 2,9.69v4.62c0,2.67 0.28,3.64 0.8,4.62A5.52,5.52 0,0 0,5.07 21.2c1,0.52 1.95,0.8 4.62,0.8h4.62c2.67,0 3.64,-0.28 4.62,-0.8a5.52,5.52 0,0 0,2.27 -2.27c0.52,-1 0.8,-1.95 0.8,-4.62L22,9.69c0,-2.67 -0.28,-3.64 -0.8,-4.62A5.52,5.52 0,0 0,18.93 2.8C18,2.28 17,2 14.31,2ZM7.12,12h3.11v3.67L9,15.67v-0.81L8.8,14.86a1.3,1.3 0,0 1,-0.48 0.68,1.66 1.66,0 0,1 -1.06,0.28 2.24,2.24 0,0 1,-0.93 -0.19A2.06,2.06 0,0 1,5.57 15a2.9,2.9 0,0 1,-0.51 -0.92,4 4,0 0,1 -0.19,-1.27v-1.7A3.45,3.45 0,0 1,5.08 9.9,2.5 2.5,0 0,1 5.64,9a2.36,2.36 0,0 1,0.85 -0.57,3 3,0 0,1 1.1,-0.19 3,3 0,0 1,1.12 0.2A2.7,2.7 0,0 1,9.54 9a2.35,2.35 0,0 1,0.51 0.85,2.9 2.9,0 0,1 0.18,1L10.23,11L8.86,11v-0.12a1.31,1.31 0,0 0,-0.34 -1,1.26 1.26,0 0,0 -0.95,-0.34 1.27,1.27 0,0 0,-1 0.39,1.89 1.89,0 0,0 -0.37,1.33v1.54c0,1.12 0.45,1.68 1.37,1.68a1.22,1.22 0,0 0,0.92 -0.33,1.15 1.15,0 0,0 0.32,-0.87L7.12,13.28ZM13.12,8.41v7.26L11.7,15.67L11.7,8.37ZM14.53,8.41h4.65v1.3L15.85,9.71v1.67h3.22v1.3L15.85,12.68v3L14.48,15.68Z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_gif_solid_24.xml
Normal file
9
app/src/main/res/drawable/ic_gif_solid_24.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/signal_icon_tint_primary"
|
||||
android:pathData="M21.2,5.07A5.52,5.52 0,0 0,18.93 2.8C18,2.28 17,2 14.31,2L9.69,2C7,2 6.05,2.28 5.07,2.8A5.52,5.52 0,0 0,2.8 5.07C2.28,6.05 2,7 2,9.69v4.62c0,2.67 0.28,3.64 0.8,4.62A5.52,5.52 0,0 0,5.07 21.2c1,0.52 1.95,0.8 4.62,0.8h4.62c2.67,0 3.64,-0.28 4.62,-0.8a5.52,5.52 0,0 0,2.27 -2.27c0.52,-1 0.8,-1.95 0.8,-4.62L22,9.69C22,7 21.72,6.05 21.2,5.07ZM10.23,11L8.86,11v-0.12a1.31,1.31 0,0 0,-0.34 -1,1.26 1.26,0 0,0 -0.95,-0.34 1.27,1.27 0,0 0,-1 0.39,1.89 1.89,0 0,0 -0.37,1.33v1.54c0,1.12 0.45,1.68 1.37,1.68a1.22,1.22 0,0 0,0.92 -0.33,1.15 1.15,0 0,0 0.32,-0.87L7.12,13.28L7.12,12h3.11v3.67L9,15.67v-0.81L8.8,14.86a1.3,1.3 0,0 1,-0.48 0.68,1.66 1.66,0 0,1 -1.06,0.28 2.24,2.24 0,0 1,-0.93 -0.19A2.06,2.06 0,0 1,5.57 15a2.9,2.9 0,0 1,-0.51 -0.92,4 4,0 0,1 -0.19,-1.27v-1.7A3.45,3.45 0,0 1,5.08 9.9,2.5 2.5,0 0,1 5.64,9a2.36,2.36 0,0 1,0.85 -0.57,3 3,0 0,1 1.1,-0.19 3,3 0,0 1,1.12 0.2A2.7,2.7 0,0 1,9.54 9a2.35,2.35 0,0 1,0.51 0.85,2.9 2.9,0 0,1 0.18,1ZM13.07,15.65L11.7,15.65L11.7,8.37h1.37ZM19.13,9.65L15.85,9.65v1.67h3.22v1.3L15.85,12.62v3L14.48,15.62L14.48,8.37h4.65Z"/>
|
||||
</vector>
|
19
app/src/main/res/layout/gif_player.xml
Normal file
19
app/src/main/res/layout/gif_player.xml
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.exoplayer2.ui.PlayerView
|
||||
android:id="@+id/video_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
app:use_controller="false"
|
||||
app:keep_content_on_player_reset="false"
|
||||
app:player_layout_id="@layout/media_preview_exoplayer_layout"
|
||||
app:surface_type="texture_view" />
|
||||
|
||||
</FrameLayout>
|
13
app/src/main/res/layout/giphy_mp4.xml
Normal file
13
app/src/main/res/layout/giphy_mp4.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.exoplayer2.ui.AspectRatioFrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/still_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="fitXY" />
|
||||
|
||||
</com.google.android.exoplayer2.ui.AspectRatioFrameLayout>
|
22
app/src/main/res/layout/giphy_mp4_fragment.xml
Normal file
22
app/src/main/res/layout/giphy_mp4_fragment.xml
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/giphy_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/giphy_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/giphy_mp4" />
|
||||
|
||||
<!-- Will Inject N VideoViews @ runtime -->
|
||||
</FrameLayout>
|
15
app/src/main/res/layout/giphy_mp4_keyboard_icon.xml
Normal file
15
app/src/main/res/layout/giphy_mp4_keyboard_icon.xml
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/media_keyboard_provider_icon_margin"
|
||||
android:padding="@dimen/media_keyboard_provider_icon_padding">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/ic_gif_outline_24" />
|
||||
|
||||
</FrameLayout>
|
16
app/src/main/res/layout/giphy_mp4_keyboard_icon_selected.xml
Normal file
16
app/src/main/res/layout/giphy_mp4_keyboard_icon_selected.xml
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/media_keyboard_provider_icon_margin"
|
||||
android:padding="@dimen/media_keyboard_provider_icon_padding"
|
||||
android:background="@drawable/media_keyboard_selected_background">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/ic_gif_solid_24" />
|
||||
|
||||
</FrameLayout>
|
15
app/src/main/res/layout/giphy_mp4_player.xml
Normal file
15
app/src/main/res/layout/giphy_mp4_player.xml
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp">
|
||||
|
||||
<org.thoughtcrime.securesms.giph.mp4.GiphyMp4VideoPlayer
|
||||
android:id="@+id/video_player"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clickable="false"
|
||||
android:contentDescription="@string/conversation_item__mms_image_description"
|
||||
android:longClickable="false" />
|
||||
|
||||
</FrameLayout>
|
7
app/src/main/res/layout/thumbnail_player_stub.xml
Normal file
7
app/src/main/res/layout/thumbnail_player_stub.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.video.VideoPlayer xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clickable="false"
|
||||
android:contentDescription="@string/conversation_item__mms_image_description"
|
||||
android:longClickable="false" />
|
|
@ -24,6 +24,15 @@
|
|||
android:scaleType="fitCenter"
|
||||
android:contentDescription="@string/conversation_item__mms_image_description" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/thumbnail_player_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clickable="false"
|
||||
android:inflatedId="@id/thumbnail_player_stub"
|
||||
android:layout="@layout/thumbnail_player_stub"
|
||||
android:longClickable="false" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumbnail_caption_icon"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
@ -452,6 +452,7 @@ public class SignalServiceMessageSender {
|
|||
attachment.getFileName(),
|
||||
attachment.getVoiceNote(),
|
||||
attachment.isBorderless(),
|
||||
attachment.isGif(),
|
||||
attachment.getCaption(),
|
||||
attachment.getBlurHash(),
|
||||
attachment.getUploadTimestamp());
|
||||
|
@ -492,6 +493,7 @@ public class SignalServiceMessageSender {
|
|||
attachment.getFileName(),
|
||||
attachment.getVoiceNote(),
|
||||
attachment.isBorderless(),
|
||||
attachment.isGif(),
|
||||
attachment.getCaption(),
|
||||
attachment.getBlurHash(),
|
||||
attachment.getUploadTimestamp());
|
||||
|
@ -1562,6 +1564,10 @@ public class SignalServiceMessageSender {
|
|||
builder.setFlags(AttachmentPointer.Flags.BORDERLESS_VALUE);
|
||||
}
|
||||
|
||||
if (attachment.isGif()) {
|
||||
builder.setFlags(AttachmentPointer.Flags.GIF_VALUE);
|
||||
}
|
||||
|
||||
if (attachment.getCaption().isPresent()) {
|
||||
builder.setCaption(attachment.getCaption().get());
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ public abstract class SignalServiceAttachment {
|
|||
}
|
||||
|
||||
public static SignalServiceAttachmentStream emptyStream(String contentType) {
|
||||
return new SignalServiceAttachmentStream(new ByteArrayInputStream(new byte[0]), contentType, 0, Optional.absent(), false, false, null, null);
|
||||
return new SignalServiceAttachmentStream(new ByteArrayInputStream(new byte[0]), contentType, 0, Optional.absent(), false, false, false, null, null);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
@ -54,6 +54,7 @@ public abstract class SignalServiceAttachment {
|
|||
private CancelationSignal cancelationSignal;
|
||||
private boolean voiceNote;
|
||||
private boolean borderless;
|
||||
private boolean gif;
|
||||
private int width;
|
||||
private int height;
|
||||
private String caption;
|
||||
|
@ -103,6 +104,11 @@ public abstract class SignalServiceAttachment {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder withGif(boolean gif) {
|
||||
this.gif = gif;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withWidth(int width) {
|
||||
this.width = width;
|
||||
return this;
|
||||
|
@ -144,6 +150,7 @@ public abstract class SignalServiceAttachment {
|
|||
Optional.fromNullable(fileName),
|
||||
voiceNote,
|
||||
borderless,
|
||||
gif,
|
||||
Optional.<byte[]>absent(),
|
||||
width,
|
||||
height,
|
||||
|
|
|
@ -27,6 +27,7 @@ public class SignalServiceAttachmentPointer extends SignalServiceAttachment {
|
|||
private final Optional<String> fileName;
|
||||
private final boolean voiceNote;
|
||||
private final boolean borderless;
|
||||
private final boolean gif;
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final Optional<String> caption;
|
||||
|
@ -45,6 +46,7 @@ public class SignalServiceAttachmentPointer extends SignalServiceAttachment {
|
|||
Optional<String> fileName,
|
||||
boolean voiceNote,
|
||||
boolean borderless,
|
||||
boolean gif,
|
||||
Optional<String> caption,
|
||||
Optional<String> 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;
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ public class SignalServiceAttachmentStream extends SignalServiceAttachment {
|
|||
private final Optional<byte[]> 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<String> fileName,
|
||||
boolean voiceNote,
|
||||
boolean borderless,
|
||||
boolean gif,
|
||||
ProgressListener listener,
|
||||
CancelationSignal cancelationSignal)
|
||||
{
|
||||
this(inputStream, contentType, length, fileName, voiceNote, borderless, Optional.<byte[]>absent(), 0, 0, System.currentTimeMillis(), Optional.<String>absent(), Optional.<String>absent(), listener, cancelationSignal, Optional.absent());
|
||||
this(inputStream, contentType, length, fileName, voiceNote, borderless, gif, Optional.<byte[]>absent(), 0, 0, System.currentTimeMillis(), Optional.<String>absent(), Optional.<String>absent(), listener, cancelationSignal, Optional.absent());
|
||||
}
|
||||
|
||||
public SignalServiceAttachmentStream(InputStream inputStream,
|
||||
|
@ -50,6 +52,7 @@ public class SignalServiceAttachmentStream extends SignalServiceAttachment {
|
|||
Optional<String> fileName,
|
||||
boolean voiceNote,
|
||||
boolean borderless,
|
||||
boolean gif,
|
||||
Optional<byte[]> 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;
|
||||
}
|
||||
|
|
|
@ -978,6 +978,7 @@ public final class SignalServiceContent {
|
|||
pointer.hasFileName() ? Optional.of(pointer.getFileName()) : Optional.<String>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.<String>absent(),
|
||||
pointer.hasBlurHash() ? Optional.of(pointer.getBlurHash()) : Optional.<String>absent(),
|
||||
pointer.hasUploadTimestamp() ? pointer.getUploadTimestamp() : 0);
|
||||
|
@ -1037,6 +1038,7 @@ public final class SignalServiceContent {
|
|||
Optional.<String>absent(),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Optional.<String>absent(),
|
||||
Optional.<String>absent(),
|
||||
pointer.hasUploadTimestamp() ? pointer.getUploadTimestamp() : 0);
|
||||
|
|
|
@ -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.<String>absent(), false, false, null, null));
|
||||
avatar = Optional.of(new SignalServiceAttachmentStream(avatarStream, avatarContentType, avatarLength, Optional.<String>absent(), false, false, false, null, null));
|
||||
}
|
||||
|
||||
if (details.hasVerified()) {
|
||||
|
|
|
@ -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.<String>absent(), false, false, null, null));
|
||||
avatar = Optional.of(new SignalServiceAttachmentStream(avatarStream, avatarContentType, avatarLength, Optional.<String>absent(), false, false, false, null, null));
|
||||
}
|
||||
|
||||
if (details.hasExpireTimer() && details.getExpireTimer() > 0) {
|
||||
|
|
|
@ -501,6 +501,7 @@ message AttachmentPointer {
|
|||
enum Flags {
|
||||
VOICE_MESSAGE = 1;
|
||||
BORDERLESS = 2;
|
||||
GIF = 3;
|
||||
}
|
||||
|
||||
oneof attachment_identifier {
|
||||
|
|
Loading…
Add table
Reference in a new issue