Add support for inline video playback of gifs in Conversation.
This commit is contained in:
parent
32d79ead15
commit
281630e751
45 changed files with 1364 additions and 408 deletions
|
@ -8,11 +8,14 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.Observer;
|
import androidx.lifecycle.Observer;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.components.MaskView;
|
||||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||||
|
import org.thoughtcrime.securesms.conversation.ConversationItem;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable;
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
|
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||||
|
@ -20,13 +23,14 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||||
|
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public interface BindableConversationItem extends Unbindable {
|
public interface BindableConversationItem extends Unbindable, GiphyMp4Playable {
|
||||||
void bind(@NonNull LifecycleOwner lifecycleOwner,
|
void bind(@NonNull LifecycleOwner lifecycleOwner,
|
||||||
@NonNull ConversationMessage messageRecord,
|
@NonNull ConversationMessage messageRecord,
|
||||||
@NonNull Optional<MessageRecord> previousMessageRecord,
|
@NonNull Optional<MessageRecord> previousMessageRecord,
|
||||||
|
@ -38,7 +42,9 @@ public interface BindableConversationItem extends Unbindable {
|
||||||
@Nullable String searchQuery,
|
@Nullable String searchQuery,
|
||||||
boolean pulseMention,
|
boolean pulseMention,
|
||||||
boolean hasWallpaper,
|
boolean hasWallpaper,
|
||||||
boolean isMessageRequestAccepted);
|
boolean isMessageRequestAccepted,
|
||||||
|
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
|
||||||
|
boolean canPlayInline);
|
||||||
|
|
||||||
ConversationMessage getConversationMessage();
|
ConversationMessage getConversationMessage();
|
||||||
|
|
||||||
|
@ -68,6 +74,7 @@ public interface BindableConversationItem extends Unbindable {
|
||||||
void onJoinGroupCallClicked();
|
void onJoinGroupCallClicked();
|
||||||
void onInviteFriendsToGroupClicked(@NonNull GroupId.V2 groupId);
|
void onInviteFriendsToGroupClicked(@NonNull GroupId.V2 groupId);
|
||||||
void onEnableCallNotificationsClicked();
|
void onEnableCallNotificationsClicked();
|
||||||
|
void onPlayInlineContent(ConversationMessage conversationMessage);
|
||||||
|
|
||||||
/** @return true if handled, false if you want to let the normal url handling continue */
|
/** @return true if handled, false if you want to let the normal url handling continue */
|
||||||
boolean onUrlClicked(@NonNull String url);
|
boolean onUrlClicked(@NonNull String url);
|
||||||
|
|
|
@ -12,7 +12,6 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.lifecycle.Lifecycle;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||||
|
@ -20,6 +19,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||||
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -33,6 +33,8 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||||
private Outliner outliner;
|
private Outliner outliner;
|
||||||
private Outliner pulseOutliner;
|
private Outliner pulseOutliner;
|
||||||
private boolean borderless;
|
private boolean borderless;
|
||||||
|
private int[] normalBounds;
|
||||||
|
private int[] gifBounds;
|
||||||
|
|
||||||
public ConversationItemThumbnail(Context context) {
|
public ConversationItemThumbnail(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
|
@ -61,14 +63,28 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||||
|
|
||||||
outliner.setColor(ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_20));
|
outliner.setColor(ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_20));
|
||||||
|
|
||||||
|
int gifWidth = ViewUtil.dpToPx(260);
|
||||||
if (attrs != null) {
|
if (attrs != null) {
|
||||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemThumbnail, 0, 0);
|
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemThumbnail, 0, 0);
|
||||||
thumbnail.setBounds(typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minWidth, 0),
|
normalBounds = new int[]{
|
||||||
|
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minWidth, 0),
|
||||||
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxWidth, 0),
|
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxWidth, 0),
|
||||||
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minHeight, 0),
|
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minHeight, 0),
|
||||||
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxHeight, 0));
|
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxHeight, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
gifWidth = typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_gifWidth, gifWidth);
|
||||||
typedArray.recycle();
|
typedArray.recycle();
|
||||||
|
} else {
|
||||||
|
normalBounds = new int[]{0, 0, 0, 0};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gifBounds = new int[]{
|
||||||
|
gifWidth,
|
||||||
|
gifWidth,
|
||||||
|
1,
|
||||||
|
Integer.MAX_VALUE
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("SuspiciousNameCombination")
|
@SuppressWarnings("SuspiciousNameCombination")
|
||||||
|
@ -89,6 +105,18 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void hideThumbnailView() {
|
||||||
|
thumbnail.setAlpha(0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showThumbnailView() {
|
||||||
|
thumbnail.setAlpha(1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable CornerMask getCornerMask() {
|
||||||
|
return cornerMask;
|
||||||
|
}
|
||||||
|
|
||||||
public void setPulseOutliner(@NonNull Outliner outliner) {
|
public void setPulseOutliner(@NonNull Outliner outliner) {
|
||||||
this.pulseOutliner = outliner;
|
this.pulseOutliner = outliner;
|
||||||
}
|
}
|
||||||
|
@ -138,6 +166,13 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||||
boolean showControls, boolean isPreview)
|
boolean showControls, boolean isPreview)
|
||||||
{
|
{
|
||||||
if (slides.size() == 1) {
|
if (slides.size() == 1) {
|
||||||
|
Slide slide = slides.get(0);
|
||||||
|
if (slide.isVideoGif()) {
|
||||||
|
setThumbnailBounds(gifBounds);
|
||||||
|
} else {
|
||||||
|
setThumbnailBounds(normalBounds);
|
||||||
|
}
|
||||||
|
|
||||||
thumbnail.setVisibility(VISIBLE);
|
thumbnail.setVisibility(VISIBLE);
|
||||||
album.setVisibility(GONE);
|
album.setVisibility(GONE);
|
||||||
|
|
||||||
|
@ -168,4 +203,8 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||||
thumbnail.setDownloadClickListener(listener);
|
thumbnail.setDownloadClickListener(listener);
|
||||||
album.setDownloadClickListener(listener);
|
album.setDownloadClickListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setThumbnailBounds(@NonNull int[] bounds) {
|
||||||
|
thumbnail.setBounds(bounds[0], bounds[1], bounds[2], bounds[3]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,11 @@ import android.graphics.Path;
|
||||||
import android.graphics.PorterDuff;
|
import android.graphics.PorterDuff;
|
||||||
import android.graphics.PorterDuffXfermode;
|
import android.graphics.PorterDuffXfermode;
|
||||||
import android.graphics.RectF;
|
import android.graphics.RectF;
|
||||||
|
import android.graphics.drawable.shapes.RoundRectShape;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
public class CornerMask {
|
public class CornerMask {
|
||||||
|
|
||||||
|
@ -20,19 +22,24 @@ public class CornerMask {
|
||||||
private final RectF bounds = new RectF();
|
private final RectF bounds = new RectF();
|
||||||
|
|
||||||
public CornerMask(@NonNull View view) {
|
public CornerMask(@NonNull View view) {
|
||||||
|
this(view, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CornerMask(@NonNull View view, @Nullable CornerMask toClone) {
|
||||||
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
|
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
|
||||||
|
|
||||||
clearPaint.setColor(Color.BLACK);
|
clearPaint.setColor(Color.BLACK);
|
||||||
clearPaint.setStyle(Paint.Style.FILL);
|
clearPaint.setStyle(Paint.Style.FILL);
|
||||||
clearPaint.setAntiAlias(true);
|
clearPaint.setAntiAlias(true);
|
||||||
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
||||||
|
|
||||||
|
if (toClone != null) {
|
||||||
|
System.arraycopy(toClone.radii, 0, radii, 0, radii.length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void mask(Canvas canvas) {
|
public void mask(Canvas canvas) {
|
||||||
bounds.left = 0;
|
bounds.set(canvas.getClipBounds());
|
||||||
bounds.top = 0;
|
|
||||||
bounds.right = canvas.getWidth();
|
|
||||||
bounds.bottom = canvas.getHeight();
|
|
||||||
|
|
||||||
corners.reset();
|
corners.reset();
|
||||||
corners.addRoundRect(bounds, radii, Path.Direction.CW);
|
corners.addRoundRect(bounds, radii, Path.Direction.CW);
|
||||||
|
@ -72,4 +79,8 @@ public class CornerMask {
|
||||||
public void setBottomLeftRadius(int radius) {
|
public void setBottomLeftRadius(int radius) {
|
||||||
radii[6] = radii[7] = radius;
|
radii[6] = radii[7] = radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public float[] getRadii() {
|
||||||
|
return radii;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,13 @@ import android.view.ViewTreeObserver;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class MaskView extends View {
|
public class MaskView extends View {
|
||||||
|
|
||||||
private View target;
|
private MaskTarget maskTarget;
|
||||||
private ViewGroup activityContentView;
|
private ViewGroup activityContentView;
|
||||||
private Paint maskPaint;
|
private Paint maskPaint;
|
||||||
private Rect drawingRect = new Rect();
|
private Rect drawingRect = new Rect();
|
||||||
|
@ -50,15 +54,15 @@ public class MaskView extends View {
|
||||||
activityContentView = getRootView().findViewById(android.R.id.content);
|
activityContentView = getRootView().findViewById(android.R.id.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTarget(@Nullable View target) {
|
public void setTarget(@Nullable MaskTarget maskTarget) {
|
||||||
if (this.target != null) {
|
if (this.maskTarget != null) {
|
||||||
this.target.getViewTreeObserver().removeOnDrawListener(onDrawListener);
|
removeOnDrawListener(this.maskTarget, onDrawListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.target = target;
|
this.maskTarget = maskTarget;
|
||||||
|
|
||||||
if (this.target != null) {
|
if (this.maskTarget != null) {
|
||||||
this.target.getViewTreeObserver().addOnDrawListener(onDrawListener);
|
addOnDrawListener(maskTarget, onDrawListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidate();
|
invalidate();
|
||||||
|
@ -72,26 +76,77 @@ public class MaskView extends View {
|
||||||
protected void onDraw(@NonNull Canvas canvas) {
|
protected void onDraw(@NonNull Canvas canvas) {
|
||||||
super.onDraw(canvas);
|
super.onDraw(canvas);
|
||||||
|
|
||||||
if (target == null || !target.isAttachedToWindow()) {
|
if (nothingToMask(maskTarget)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
target.getDrawingRect(drawingRect);
|
maskTarget.getPrimaryTarget().getDrawingRect(drawingRect);
|
||||||
activityContentView.offsetDescendantRectToMyCoords(target, drawingRect);
|
activityContentView.offsetDescendantRectToMyCoords(maskTarget.getPrimaryTarget(), drawingRect);
|
||||||
|
|
||||||
drawingRect.top += targetParentTranslationY;
|
drawingRect.top += targetParentTranslationY;
|
||||||
drawingRect.bottom += targetParentTranslationY;
|
drawingRect.bottom += targetParentTranslationY;
|
||||||
|
|
||||||
Bitmap mask = Bitmap.createBitmap(target.getWidth(), drawingRect.height(), Bitmap.Config.ARGB_8888);
|
Bitmap mask = Bitmap.createBitmap(maskTarget.getPrimaryTarget().getWidth(), drawingRect.height(), Bitmap.Config.ARGB_8888);
|
||||||
Canvas maskCanvas = new Canvas(mask);
|
Canvas maskCanvas = new Canvas(mask);
|
||||||
|
|
||||||
target.draw(maskCanvas);
|
maskTarget.draw(maskCanvas);
|
||||||
|
|
||||||
canvas.clipRect(drawingRect.left, Math.max(drawingRect.top, getTop() + getPaddingTop()), drawingRect.right, Math.min(drawingRect.bottom, getBottom() - getPaddingBottom()));
|
canvas.clipRect(drawingRect.left, Math.max(drawingRect.top, getTop() + getPaddingTop()), drawingRect.right, Math.min(drawingRect.bottom, getBottom() - getPaddingBottom()));
|
||||||
|
|
||||||
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) target.getLayoutParams();
|
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) maskTarget.getPrimaryTarget().getLayoutParams();
|
||||||
canvas.drawBitmap(mask, params.leftMargin, drawingRect.top, maskPaint);
|
canvas.drawBitmap(mask, params.leftMargin, drawingRect.top, maskPaint);
|
||||||
|
|
||||||
mask.recycle();
|
mask.recycle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void removeOnDrawListener(@NonNull MaskTarget maskTarget, @NonNull ViewTreeObserver.OnDrawListener onDrawListener) {
|
||||||
|
for (View view : maskTarget.getAllTargets()) {
|
||||||
|
if (view != null) {
|
||||||
|
view.getViewTreeObserver().removeOnDrawListener(onDrawListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addOnDrawListener(@NonNull MaskTarget maskTarget, @NonNull ViewTreeObserver.OnDrawListener onDrawListener) {
|
||||||
|
for (View view : maskTarget.getAllTargets()) {
|
||||||
|
if (view != null) {
|
||||||
|
view.getViewTreeObserver().addOnDrawListener(onDrawListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean nothingToMask(@Nullable MaskTarget maskTarget) {
|
||||||
|
if (maskTarget == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (View view : maskTarget.getAllTargets()) {
|
||||||
|
if (view == null || !view.isAttachedToWindow()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class MaskTarget {
|
||||||
|
|
||||||
|
private final View primaryTarget;
|
||||||
|
|
||||||
|
public MaskTarget(@NonNull View primaryTarget) {
|
||||||
|
this.primaryTarget = primaryTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
final @NonNull View getPrimaryTarget() {
|
||||||
|
return primaryTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected @NonNull List<View> getAllTargets() {
|
||||||
|
return Collections.singletonList(primaryTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void draw(@NonNull Canvas canvas) {
|
||||||
|
primaryTarget.draw(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
|
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -48,7 +49,7 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final SimpleExoPlayer player;
|
private final SimpleExoPlayer player;
|
||||||
private final VoiceNoteQueueDataAdapter queueDataAdapter;
|
private final VoiceNoteQueueDataAdapter queueDataAdapter;
|
||||||
private final VoiceNoteMediaSourceFactory mediaSourceFactory;
|
private final AttachmentMediaSourceFactory mediaSourceFactory;
|
||||||
private final ConcatenatingMediaSource dataSource;
|
private final ConcatenatingMediaSource dataSource;
|
||||||
|
|
||||||
private boolean canLoadMore;
|
private boolean canLoadMore;
|
||||||
|
@ -57,7 +58,7 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
|
||||||
VoiceNotePlaybackPreparer(@NonNull Context context,
|
VoiceNotePlaybackPreparer(@NonNull Context context,
|
||||||
@NonNull SimpleExoPlayer player,
|
@NonNull SimpleExoPlayer player,
|
||||||
@NonNull VoiceNoteQueueDataAdapter queueDataAdapter,
|
@NonNull VoiceNoteQueueDataAdapter queueDataAdapter,
|
||||||
@NonNull VoiceNoteMediaSourceFactory mediaSourceFactory)
|
@NonNull AttachmentMediaSourceFactory mediaSourceFactory)
|
||||||
{
|
{
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.player = player;
|
this.player = player;
|
||||||
|
|
|
@ -34,6 +34,7 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager;
|
import com.google.android.exoplayer2.ui.PlayerNotificationManager;
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -87,7 +88,7 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
||||||
new VoiceNoteNotificationManagerListener(),
|
new VoiceNoteNotificationManagerListener(),
|
||||||
queueDataAdapter);
|
queueDataAdapter);
|
||||||
|
|
||||||
VoiceNoteMediaSourceFactory mediaSourceFactory = new VoiceNoteMediaSourceFactory(this);
|
AttachmentMediaSourceFactory mediaSourceFactory = new AttachmentMediaSourceFactory(this);
|
||||||
|
|
||||||
voiceNotePlaybackPreparer = new VoiceNotePlaybackPreparer(this, player, queueDataAdapter, mediaSourceFactory);
|
voiceNotePlaybackPreparer = new VoiceNotePlaybackPreparer(this, player, queueDataAdapter, mediaSourceFactory);
|
||||||
voiceNoteProximityManager = new VoiceNoteProximityManager(this, player, queueDataAdapter);
|
voiceNoteProximityManager = new VoiceNoteProximityManager(this, player, queueDataAdapter);
|
||||||
|
|
|
@ -112,6 +112,7 @@ import org.thoughtcrime.securesms.components.HidingLinearLayout;
|
||||||
import org.thoughtcrime.securesms.components.InputAwareLayout;
|
import org.thoughtcrime.securesms.components.InputAwareLayout;
|
||||||
import org.thoughtcrime.securesms.components.InputPanel;
|
import org.thoughtcrime.securesms.components.InputPanel;
|
||||||
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener;
|
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener;
|
||||||
|
import org.thoughtcrime.securesms.components.MaskView;
|
||||||
import org.thoughtcrime.securesms.components.SendButton;
|
import org.thoughtcrime.securesms.components.SendButton;
|
||||||
import org.thoughtcrime.securesms.components.TooltipPopup;
|
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||||
|
@ -3420,7 +3421,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleReaction(@NonNull View maskTarget,
|
public void handleReaction(@NonNull MaskView.MaskTarget maskTarget,
|
||||||
@NonNull MessageRecord messageRecord,
|
@NonNull MessageRecord messageRecord,
|
||||||
@NonNull Toolbar.OnMenuItemClickListener toolbarListener,
|
@NonNull Toolbar.OnMenuItemClickListener toolbarListener,
|
||||||
@NonNull ConversationReactionOverlay.OnHideListener onHideListener)
|
@NonNull ConversationReactionOverlay.OnHideListener onHideListener)
|
||||||
|
@ -3451,7 +3452,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleReactionDetails(@NonNull View maskTarget) {
|
public void handleReactionDetails(@NonNull MaskView.MaskTarget maskTarget) {
|
||||||
reactionDelegate.showMask(maskTarget, titleView.getMeasuredHeight(), inputAreaHeight());
|
reactionDelegate.showMask(maskTarget, titleView.getMeasuredHeight(), inputAreaHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,12 +36,18 @@ import androidx.recyclerview.widget.DiffUtil;
|
||||||
import androidx.recyclerview.widget.ListAdapter;
|
import androidx.recyclerview.widget.ListAdapter;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
|
||||||
import org.signal.core.util.ThreadUtil;
|
import org.signal.core.util.ThreadUtil;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.signal.paging.PagingController;
|
import org.signal.paging.PagingController;
|
||||||
import org.thoughtcrime.securesms.BindableConversationItem;
|
import org.thoughtcrime.securesms.BindableConversationItem;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.components.MaskView;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable;
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer;
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Projection;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||||
|
@ -50,6 +56,7 @@ import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
|
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
|
@ -105,6 +112,7 @@ public class ConversationAdapter
|
||||||
private final Set<Long> releasedFastRecords;
|
private final Set<Long> releasedFastRecords;
|
||||||
private final Calendar calendar;
|
private final Calendar calendar;
|
||||||
private final MessageDigest digest;
|
private final MessageDigest digest;
|
||||||
|
private final AttachmentMediaSourceFactory attachmentMediaSourceFactory;
|
||||||
|
|
||||||
private String searchQuery;
|
private String searchQuery;
|
||||||
private ConversationMessage recordToPulse;
|
private ConversationMessage recordToPulse;
|
||||||
|
@ -113,12 +121,14 @@ public class ConversationAdapter
|
||||||
private PagingController pagingController;
|
private PagingController pagingController;
|
||||||
private boolean hasWallpaper;
|
private boolean hasWallpaper;
|
||||||
private boolean isMessageRequestAccepted;
|
private boolean isMessageRequestAccepted;
|
||||||
|
private ConversationMessage inlineContent;
|
||||||
|
|
||||||
ConversationAdapter(@NonNull LifecycleOwner lifecycleOwner,
|
ConversationAdapter(@NonNull LifecycleOwner lifecycleOwner,
|
||||||
@NonNull GlideRequests glideRequests,
|
@NonNull GlideRequests glideRequests,
|
||||||
@NonNull Locale locale,
|
@NonNull Locale locale,
|
||||||
@Nullable ItemClickListener clickListener,
|
@Nullable ItemClickListener clickListener,
|
||||||
@NonNull Recipient recipient)
|
@NonNull Recipient recipient,
|
||||||
|
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory)
|
||||||
{
|
{
|
||||||
super(new DiffUtil.ItemCallback<ConversationMessage>() {
|
super(new DiffUtil.ItemCallback<ConversationMessage>() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -145,6 +155,7 @@ public class ConversationAdapter
|
||||||
this.digest = getMessageDigestOrThrow();
|
this.digest = getMessageDigestOrThrow();
|
||||||
this.hasWallpaper = recipient.hasWallpaper();
|
this.hasWallpaper = recipient.hasWallpaper();
|
||||||
this.isMessageRequestAccepted = true;
|
this.isMessageRequestAccepted = true;
|
||||||
|
this.attachmentMediaSourceFactory = attachmentMediaSourceFactory;
|
||||||
|
|
||||||
setHasStableIds(true);
|
setHasStableIds(true);
|
||||||
}
|
}
|
||||||
|
@ -257,7 +268,9 @@ public class ConversationAdapter
|
||||||
searchQuery,
|
searchQuery,
|
||||||
conversationMessage == recordToPulse,
|
conversationMessage == recordToPulse,
|
||||||
hasWallpaper,
|
hasWallpaper,
|
||||||
isMessageRequestAccepted);
|
isMessageRequestAccepted,
|
||||||
|
attachmentMediaSourceFactory,
|
||||||
|
conversationMessage == inlineContent);
|
||||||
|
|
||||||
if (conversationMessage == recordToPulse) {
|
if (conversationMessage == recordToPulse) {
|
||||||
recordToPulse = null;
|
recordToPulse = null;
|
||||||
|
@ -610,7 +623,14 @@ public class ConversationAdapter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static class ConversationViewHolder extends RecyclerView.ViewHolder {
|
public void playInlineContent(@Nullable ConversationMessage conversationMessage) {
|
||||||
|
if (this.inlineContent != conversationMessage) {
|
||||||
|
this.inlineContent = conversationMessage;
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final static class ConversationViewHolder extends RecyclerView.ViewHolder implements GiphyMp4Playable {
|
||||||
public ConversationViewHolder(final @NonNull View itemView) {
|
public ConversationViewHolder(final @NonNull View itemView) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
}
|
}
|
||||||
|
@ -618,6 +638,36 @@ public class ConversationAdapter
|
||||||
public BindableConversationItem getBindable() {
|
public BindableConversationItem getBindable() {
|
||||||
return (BindableConversationItem) itemView;
|
return (BindableConversationItem) itemView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showProjectionArea() {
|
||||||
|
getBindable().showProjectionArea();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hideProjectionArea() {
|
||||||
|
getBindable().hideProjectionArea();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable MediaSource getMediaSource() {
|
||||||
|
return getBindable().getMediaSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable GiphyMp4PlaybackPolicyEnforcer getPlaybackPolicyEnforcer() {
|
||||||
|
return getBindable().getPlaybackPolicyEnforcer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public @Override GiphyMp4Projection getProjection(@NonNull RecyclerView recyclerView) {
|
||||||
|
return getBindable().getProjection(recyclerView);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canPlayContent() {
|
||||||
|
return getBindable().canPlayContent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static class StickyHeaderViewHolder extends RecyclerView.ViewHolder {
|
static class StickyHeaderViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
@ -688,6 +738,6 @@ public class ConversationAdapter
|
||||||
|
|
||||||
interface ItemClickListener extends BindableConversationItem.EventListener {
|
interface ItemClickListener extends BindableConversationItem.EventListener {
|
||||||
void onItemClick(ConversationMessage item);
|
void onItemClick(ConversationMessage item);
|
||||||
void onItemLongClick(View maskTarget, ConversationMessage item);
|
void onItemLongClick(View itemView, ConversationMessage item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,6 +76,7 @@ import org.thoughtcrime.securesms.VerifyIdentityActivity;
|
||||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||||
import org.thoughtcrime.securesms.components.ConversationScrollToView;
|
import org.thoughtcrime.securesms.components.ConversationScrollToView;
|
||||||
import org.thoughtcrime.securesms.components.ConversationTypingView;
|
import org.thoughtcrime.securesms.components.ConversationTypingView;
|
||||||
|
import org.thoughtcrime.securesms.components.MaskView;
|
||||||
import org.thoughtcrime.securesms.components.TooltipPopup;
|
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
||||||
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
|
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
|
||||||
|
@ -97,6 +98,10 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy;
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ProjectionPlayerHolder;
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackController;
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ProjectionRecycler;
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
|
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
|
||||||
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite.GroupLinkInviteFriendsBottomSheetDialogFragment;
|
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite.GroupLinkInviteFriendsBottomSheetDialogFragment;
|
||||||
|
@ -148,6 +153,7 @@ import org.thoughtcrime.securesms.util.WindowUtil;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||||
import org.thoughtcrime.securesms.util.views.AdaptiveActionsToolbar;
|
import org.thoughtcrime.securesms.util.views.AdaptiveActionsToolbar;
|
||||||
|
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
|
||||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
|
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
|
@ -177,6 +183,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||||
private boolean isReacting;
|
private boolean isReacting;
|
||||||
private ActionMode actionMode;
|
private ActionMode actionMode;
|
||||||
private Locale locale;
|
private Locale locale;
|
||||||
|
private FrameLayout videoContainer;
|
||||||
private RecyclerView list;
|
private RecyclerView list;
|
||||||
private RecyclerView.ItemDecoration lastSeenDecoration;
|
private RecyclerView.ItemDecoration lastSeenDecoration;
|
||||||
private RecyclerView.ItemDecoration inlineDateDecoration;
|
private RecyclerView.ItemDecoration inlineDateDecoration;
|
||||||
|
@ -204,6 +211,8 @@ public class ConversationFragment extends LoggingFragment {
|
||||||
private View toolbarShadow;
|
private View toolbarShadow;
|
||||||
private Stopwatch startupStopwatch;
|
private Stopwatch startupStopwatch;
|
||||||
|
|
||||||
|
private GiphyMp4ProjectionRecycler giphyMp4ProjectionRecycler;
|
||||||
|
|
||||||
public static void prepare(@NonNull Context context) {
|
public static void prepare(@NonNull Context context) {
|
||||||
FrameLayout parent = new FrameLayout(context);
|
FrameLayout parent = new FrameLayout(context);
|
||||||
parent.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT));
|
parent.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT));
|
||||||
|
@ -226,6 +235,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
||||||
final View view = inflater.inflate(R.layout.conversation_fragment, container, false);
|
final View view = inflater.inflate(R.layout.conversation_fragment, container, false);
|
||||||
|
videoContainer = view.findViewById(R.id.video_container);
|
||||||
list = view.findViewById(android.R.id.list);
|
list = view.findViewById(android.R.id.list);
|
||||||
composeDivider = view.findViewById(R.id.compose_divider);
|
composeDivider = view.findViewById(R.id.compose_divider);
|
||||||
|
|
||||||
|
@ -250,13 +260,16 @@ public class ConversationFragment extends LoggingFragment {
|
||||||
|
|
||||||
typingView = (ConversationTypingView) inflater.inflate(R.layout.conversation_typing_view, container, false);
|
typingView = (ConversationTypingView) inflater.inflate(R.layout.conversation_typing_view, container, false);
|
||||||
|
|
||||||
|
giphyMp4ProjectionRecycler = initializeGiphyMp4();
|
||||||
|
|
||||||
new ConversationItemSwipeCallback(
|
new ConversationItemSwipeCallback(
|
||||||
conversationMessage -> actionMode == null &&
|
conversationMessage -> actionMode == null &&
|
||||||
MenuState.canReplyToMessage(recipient.get(),
|
MenuState.canReplyToMessage(recipient.get(),
|
||||||
MenuState.isActionMessage(conversationMessage.getMessageRecord()),
|
MenuState.isActionMessage(conversationMessage.getMessageRecord()),
|
||||||
conversationMessage.getMessageRecord(),
|
conversationMessage.getMessageRecord(),
|
||||||
messageRequestViewModel.shouldShowMessageRequest()),
|
messageRequestViewModel.shouldShowMessageRequest()),
|
||||||
this::handleReplyMessage
|
this::handleReplyMessage,
|
||||||
|
giphyMp4ProjectionRecycler
|
||||||
).attachToRecyclerView(list);
|
).attachToRecyclerView(list);
|
||||||
|
|
||||||
setupListLayoutListeners();
|
setupListLayoutListeners();
|
||||||
|
@ -297,6 +310,26 @@ public class ConversationFragment extends LoggingFragment {
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private @NonNull GiphyMp4ProjectionRecycler initializeGiphyMp4() {
|
||||||
|
int maxPlayback = GiphyMp4PlaybackPolicy.maxSimultaneousPlaybackInConversation();
|
||||||
|
List<GiphyMp4ProjectionPlayerHolder> holders = GiphyMp4ProjectionPlayerHolder.injectVideoViews(requireContext(),
|
||||||
|
getViewLifecycleOwner().getLifecycle(),
|
||||||
|
videoContainer,
|
||||||
|
maxPlayback);
|
||||||
|
GiphyMp4ProjectionRecycler callback = new GiphyMp4ProjectionRecycler(holders);
|
||||||
|
|
||||||
|
GiphyMp4PlaybackController.attach(list, callback, maxPlayback);
|
||||||
|
|
||||||
|
return callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NonNull MaskView.MaskTarget getMaskTarget(@NonNull View itemView) {
|
||||||
|
int adapterPosition = list.getChildAdapterPosition(itemView);
|
||||||
|
View videoPlayer = giphyMp4ProjectionRecycler.getVideoPlayerAtAdapterPosition(adapterPosition);
|
||||||
|
|
||||||
|
return new ConversationItemMaskTarget((ConversationItem) itemView, videoPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
private void setupListLayoutListeners() {
|
private void setupListLayoutListeners() {
|
||||||
list.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> setListVerticalTranslation());
|
list.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> setListVerticalTranslation());
|
||||||
|
|
||||||
|
@ -557,7 +590,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||||
private void initializeListAdapter() {
|
private void initializeListAdapter() {
|
||||||
if (this.recipient != null && this.threadId != -1) {
|
if (this.recipient != null && this.threadId != -1) {
|
||||||
Log.d(TAG, "Initializing adapter for " + recipient.getId());
|
Log.d(TAG, "Initializing adapter for " + recipient.getId());
|
||||||
ConversationAdapter adapter = new ConversationAdapter(this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get());
|
ConversationAdapter adapter = new ConversationAdapter(this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get(), new AttachmentMediaSourceFactory(requireContext()));
|
||||||
adapter.setPagingController(conversationViewModel.getPagingController());
|
adapter.setPagingController(conversationViewModel.getPagingController());
|
||||||
list.setAdapter(adapter);
|
list.setAdapter(adapter);
|
||||||
setInlineDateDecoration(adapter);
|
setInlineDateDecoration(adapter);
|
||||||
|
@ -1191,14 +1224,14 @@ public class ConversationFragment extends LoggingFragment {
|
||||||
void onMessageActionToolbarOpened();
|
void onMessageActionToolbarOpened();
|
||||||
void onForwardClicked();
|
void onForwardClicked();
|
||||||
void onMessageRequest(@NonNull MessageRequestViewModel viewModel);
|
void onMessageRequest(@NonNull MessageRequestViewModel viewModel);
|
||||||
void handleReaction(@NonNull View maskTarget,
|
void handleReaction(@NonNull MaskView.MaskTarget maskTarget,
|
||||||
@NonNull MessageRecord messageRecord,
|
@NonNull MessageRecord messageRecord,
|
||||||
@NonNull Toolbar.OnMenuItemClickListener toolbarListener,
|
@NonNull Toolbar.OnMenuItemClickListener toolbarListener,
|
||||||
@NonNull ConversationReactionOverlay.OnHideListener onHideListener);
|
@NonNull ConversationReactionOverlay.OnHideListener onHideListener);
|
||||||
void onCursorChanged();
|
void onCursorChanged();
|
||||||
void onListVerticalTranslationChanged(float translationY);
|
void onListVerticalTranslationChanged(float translationY);
|
||||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||||
void handleReactionDetails(@NonNull View maskTarget);
|
void handleReactionDetails(@NonNull MaskView.MaskTarget maskTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ConversationScrollListener extends OnScrollListener {
|
private class ConversationScrollListener extends OnScrollListener {
|
||||||
|
@ -1288,7 +1321,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onItemLongClick(View maskTarget, ConversationMessage conversationMessage) {
|
public void onItemLongClick(View itemView, ConversationMessage conversationMessage) {
|
||||||
|
|
||||||
if (actionMode != null) return;
|
if (actionMode != null) return;
|
||||||
|
|
||||||
|
@ -1304,7 +1337,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||||
{
|
{
|
||||||
isReacting = true;
|
isReacting = true;
|
||||||
list.setLayoutFrozen(true);
|
list.setLayoutFrozen(true);
|
||||||
listener.handleReaction(maskTarget, messageRecord, new ReactionsToolbarListener(conversationMessage), () -> {
|
listener.handleReaction(getMaskTarget(itemView), messageRecord, new ReactionsToolbarListener(conversationMessage), () -> {
|
||||||
isReacting = false;
|
isReacting = false;
|
||||||
list.setLayoutFrozen(false);
|
list.setLayoutFrozen(false);
|
||||||
WindowUtil.setLightStatusBarFromTheme(requireActivity());
|
WindowUtil.setLightStatusBarFromTheme(requireActivity());
|
||||||
|
@ -1452,7 +1485,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||||
public void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms) {
|
public void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms) {
|
||||||
if (getContext() == null) return;
|
if (getContext() == null) return;
|
||||||
|
|
||||||
listener.handleReactionDetails(reactionTarget);
|
listener.handleReactionDetails(getMaskTarget(reactionTarget));
|
||||||
ReactionsBottomSheetDialogFragment.create(messageId, isMms).show(requireFragmentManager(), null);
|
ReactionsBottomSheetDialogFragment.create(messageId, isMms).show(requireFragmentManager(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1571,6 +1604,11 @@ public class ConversationFragment extends LoggingFragment {
|
||||||
refreshList();
|
refreshList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayInlineContent(ConversationMessage conversationMessage) {
|
||||||
|
getListAdapter().playInlineContent(conversationMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refreshList() {
|
public void refreshList() {
|
||||||
|
|
|
@ -55,8 +55,10 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.BindableConversationItem;
|
import org.thoughtcrime.securesms.BindableConversationItem;
|
||||||
|
@ -70,6 +72,7 @@ import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||||
import org.thoughtcrime.securesms.components.BorderlessImageView;
|
import org.thoughtcrime.securesms.components.BorderlessImageView;
|
||||||
import org.thoughtcrime.securesms.components.ConversationItemFooter;
|
import org.thoughtcrime.securesms.components.ConversationItemFooter;
|
||||||
import org.thoughtcrime.securesms.components.ConversationItemThumbnail;
|
import org.thoughtcrime.securesms.components.ConversationItemThumbnail;
|
||||||
|
import org.thoughtcrime.securesms.components.CornerMask;
|
||||||
import org.thoughtcrime.securesms.components.DocumentView;
|
import org.thoughtcrime.securesms.components.DocumentView;
|
||||||
import org.thoughtcrime.securesms.components.LinkPreviewView;
|
import org.thoughtcrime.securesms.components.LinkPreviewView;
|
||||||
import org.thoughtcrime.securesms.components.Outliner;
|
import org.thoughtcrime.securesms.components.Outliner;
|
||||||
|
@ -87,6 +90,9 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.Quote;
|
import org.thoughtcrime.securesms.database.model.Quote;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy;
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer;
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Projection;
|
||||||
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
|
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
|
||||||
import org.thoughtcrime.securesms.jobs.MmsDownloadJob;
|
import org.thoughtcrime.securesms.jobs.MmsDownloadJob;
|
||||||
import org.thoughtcrime.securesms.jobs.MmsSendJob;
|
import org.thoughtcrime.securesms.jobs.MmsSendJob;
|
||||||
|
@ -100,6 +106,7 @@ import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||||
import org.thoughtcrime.securesms.mms.TextSlide;
|
import org.thoughtcrime.securesms.mms.TextSlide;
|
||||||
|
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||||
import org.thoughtcrime.securesms.reactions.ReactionsConversationView;
|
import org.thoughtcrime.securesms.reactions.ReactionsConversationView;
|
||||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
@ -120,6 +127,7 @@ import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.thoughtcrime.securesms.util.VibrateUtil;
|
import org.thoughtcrime.securesms.util.VibrateUtil;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
import org.thoughtcrime.securesms.util.views.Stub;
|
import org.thoughtcrime.securesms.util.views.Stub;
|
||||||
|
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -198,9 +206,13 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||||
private final LinkPreviewClickListener linkPreviewClickListener = new LinkPreviewClickListener();
|
private final LinkPreviewClickListener linkPreviewClickListener = new LinkPreviewClickListener();
|
||||||
private final ViewOnceMessageClickListener revealableClickListener = new ViewOnceMessageClickListener();
|
private final ViewOnceMessageClickListener revealableClickListener = new ViewOnceMessageClickListener();
|
||||||
private final UrlClickListener urlClickListener = new UrlClickListener();
|
private final UrlClickListener urlClickListener = new UrlClickListener();
|
||||||
|
private final Rect thumbnailMaskingRect = new Rect();
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
|
private MediaSource mediaSource;
|
||||||
|
private boolean canPlayContent;
|
||||||
|
|
||||||
public ConversationItem(Context context) {
|
public ConversationItem(Context context) {
|
||||||
this(context, null);
|
this(context, null);
|
||||||
}
|
}
|
||||||
|
@ -260,7 +272,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||||
@Nullable String searchQuery,
|
@Nullable String searchQuery,
|
||||||
boolean pulse,
|
boolean pulse,
|
||||||
boolean hasWallpaper,
|
boolean hasWallpaper,
|
||||||
boolean isMessageRequestAccepted)
|
boolean isMessageRequestAccepted,
|
||||||
|
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
|
||||||
|
boolean allowedToPlayInline)
|
||||||
{
|
{
|
||||||
if (this.recipient != null) this.recipient.removeForeverObserver(this);
|
if (this.recipient != null) this.recipient.removeForeverObserver(this);
|
||||||
if (this.conversationRecipient != null) this.conversationRecipient.removeForeverObserver(this);
|
if (this.conversationRecipient != null) this.conversationRecipient.removeForeverObserver(this);
|
||||||
|
@ -275,13 +289,15 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||||
this.conversationRecipient = conversationRecipient.live();
|
this.conversationRecipient = conversationRecipient.live();
|
||||||
this.groupThread = conversationRecipient.isGroup();
|
this.groupThread = conversationRecipient.isGroup();
|
||||||
this.recipient = messageRecord.getIndividualRecipient().live();
|
this.recipient = messageRecord.getIndividualRecipient().live();
|
||||||
|
this.canPlayContent = false;
|
||||||
|
this.mediaSource = null;
|
||||||
|
|
||||||
this.recipient.observeForever(this);
|
this.recipient.observeForever(this);
|
||||||
this.conversationRecipient.observeForever(this);
|
this.conversationRecipient.observeForever(this);
|
||||||
|
|
||||||
setGutterSizes(messageRecord, groupThread);
|
setGutterSizes(messageRecord, groupThread);
|
||||||
setMessageShape(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
|
setMessageShape(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
|
||||||
setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, groupThread, hasWallpaper, isMessageRequestAccepted);
|
setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, groupThread, hasWallpaper, isMessageRequestAccepted, attachmentMediaSourceFactory, allowedToPlayInline);
|
||||||
setBodyText(messageRecord, searchQuery, isMessageRequestAccepted);
|
setBodyText(messageRecord, searchQuery, isMessageRequestAccepted);
|
||||||
setBubbleState(messageRecord, hasWallpaper);
|
setBubbleState(messageRecord, hasWallpaper);
|
||||||
setInteractionState(conversationMessage, pulse);
|
setInteractionState(conversationMessage, pulse);
|
||||||
|
@ -680,7 +696,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||||
@NonNull Optional<MessageRecord> nextRecord,
|
@NonNull Optional<MessageRecord> nextRecord,
|
||||||
boolean isGroupThread,
|
boolean isGroupThread,
|
||||||
boolean hasWallpaper,
|
boolean hasWallpaper,
|
||||||
boolean messageRequestAccepted)
|
boolean messageRequestAccepted,
|
||||||
|
@Nullable AttachmentMediaSourceFactory attachmentMediaSourceFactory,
|
||||||
|
boolean allowedToPlayInline)
|
||||||
{
|
{
|
||||||
boolean showControls = !messageRecord.isFailed();
|
boolean showControls = !messageRecord.isFailed();
|
||||||
|
|
||||||
|
@ -865,6 +883,16 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||||
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
|
||||||
footer.setVisibility(VISIBLE);
|
footer.setVisibility(VISIBLE);
|
||||||
|
|
||||||
|
if (attachmentMediaSourceFactory != null &&
|
||||||
|
thumbnailSlides.size() == 1 &&
|
||||||
|
thumbnailSlides.get(0).isVideoGif() &&
|
||||||
|
thumbnailSlides.get(0) instanceof VideoSlide)
|
||||||
|
{
|
||||||
|
canPlayContent = GiphyMp4PlaybackPolicy.autoplay() || allowedToPlayInline;
|
||||||
|
mediaSource = attachmentMediaSourceFactory.createMediaSource(Objects.requireNonNull(thumbnailSlides.get(0).getUri()));
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
|
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
|
||||||
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
|
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
|
||||||
|
@ -1399,6 +1427,68 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||||
return span;
|
return span;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showProjectionArea() {
|
||||||
|
if (mediaThumbnailStub != null && mediaThumbnailStub.resolved()) {
|
||||||
|
mediaThumbnailStub.get().showThumbnailView();
|
||||||
|
bodyBubble.setMask(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hideProjectionArea() {
|
||||||
|
if (mediaThumbnailStub != null && mediaThumbnailStub.resolved()) {
|
||||||
|
mediaThumbnailStub.get().hideThumbnailView();
|
||||||
|
mediaThumbnailStub.get().getDrawingRect(thumbnailMaskingRect);
|
||||||
|
bodyBubble.setMask(thumbnailMaskingRect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable MediaSource getMediaSource() {
|
||||||
|
return mediaSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable GiphyMp4PlaybackPolicyEnforcer getPlaybackPolicyEnforcer() {
|
||||||
|
if (GiphyMp4PlaybackPolicy.autoplay()) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return new GiphyMp4PlaybackPolicyEnforcer(() -> {
|
||||||
|
eventListener.onPlayInlineContent(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAdapterPosition() {
|
||||||
|
throw new UnsupportedOperationException("Do not delegate to this method");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull GiphyMp4Projection getProjection(@NonNull RecyclerView recyclerView) {
|
||||||
|
return GiphyMp4Projection.forView(recyclerView, mediaThumbnailStub.get(), mediaThumbnailStub.get().getCornerMask())
|
||||||
|
.translateX(bodyBubble.getTranslationX());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canPlayContent() {
|
||||||
|
return mediaThumbnailStub != null && canPlayContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull Rect getThumbnailMaskingRect(@NonNull ViewGroup parent) {
|
||||||
|
Rect rect = new Rect();
|
||||||
|
rect.set(thumbnailMaskingRect);
|
||||||
|
|
||||||
|
parent.offsetDescendantRectToMyCoords(mediaThumbnailStub.get(), rect);
|
||||||
|
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull CornerMask getThumbnailCornerMask(@NonNull View view) {
|
||||||
|
return new CornerMask(view, mediaThumbnailStub.get().getCornerMask());
|
||||||
|
}
|
||||||
|
|
||||||
private class SharedContactEventListener implements SharedContactView.EventListener {
|
private class SharedContactEventListener implements SharedContactView.EventListener {
|
||||||
@Override
|
@Override
|
||||||
public void onAddToContactsClicked(@NonNull Contact contact) {
|
public void onAddToContactsClicked(@NonNull Contact contact) {
|
||||||
|
@ -1526,6 +1616,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||||
public void onClick(final View v, final Slide slide) {
|
public void onClick(final View v, final Slide slide) {
|
||||||
if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) {
|
if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) {
|
||||||
performClick();
|
performClick();
|
||||||
|
} else if (!canPlayContent && mediaSource != null && eventListener != null) {
|
||||||
|
eventListener.onPlayInlineContent(conversationMessage);
|
||||||
} else if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
|
} else if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
|
||||||
Intent intent = new Intent(context, MediaPreviewActivity.class);
|
Intent intent = new Intent(context, MediaPreviewActivity.class);
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
|
|
@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.conversation;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
@ -19,6 +21,9 @@ public class ConversationItemBodyBubble extends LinearLayout {
|
||||||
@Nullable private List<Outliner> outliners = Collections.emptyList();
|
@Nullable private List<Outliner> outliners = Collections.emptyList();
|
||||||
@Nullable private OnSizeChangedListener sizeChangedListener;
|
@Nullable private OnSizeChangedListener sizeChangedListener;
|
||||||
|
|
||||||
|
private MaskDrawable maskDrawable;
|
||||||
|
private Rect mask;
|
||||||
|
|
||||||
public ConversationItemBodyBubble(Context context) {
|
public ConversationItemBodyBubble(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
}
|
}
|
||||||
|
@ -39,6 +44,18 @@ public class ConversationItemBodyBubble extends LinearLayout {
|
||||||
this.sizeChangedListener = listener;
|
this.sizeChangedListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBackground(Drawable background) {
|
||||||
|
maskDrawable = new MaskDrawable(background);
|
||||||
|
maskDrawable.setMask(mask);
|
||||||
|
super.setBackground(maskDrawable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMask(@Nullable Rect mask) {
|
||||||
|
this.mask = mask;
|
||||||
|
maskDrawable.setMask(mask);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDraw(Canvas canvas) {
|
protected void onDraw(Canvas canvas) {
|
||||||
super.onDraw(canvas);
|
super.onDraw(canvas);
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
package org.thoughtcrime.securesms.conversation;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.components.CornerMask;
|
||||||
|
import org.thoughtcrime.securesms.components.MaskView;
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Projection;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class ConversationItemMaskTarget extends MaskView.MaskTarget {
|
||||||
|
|
||||||
|
private final ConversationItem conversationItem;
|
||||||
|
private final View videoContainer;
|
||||||
|
|
||||||
|
public ConversationItemMaskTarget(@NonNull ConversationItem conversationItem,
|
||||||
|
@Nullable View videoContainer)
|
||||||
|
{
|
||||||
|
super(conversationItem);
|
||||||
|
this.conversationItem = conversationItem;
|
||||||
|
this.videoContainer = videoContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected @NonNull List<View> getAllTargets() {
|
||||||
|
if (videoContainer == null) {
|
||||||
|
return super.getAllTargets();
|
||||||
|
} else {
|
||||||
|
return Arrays.asList(conversationItem, videoContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void draw(@NonNull Canvas canvas) {
|
||||||
|
super.draw(canvas);
|
||||||
|
|
||||||
|
if (videoContainer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GiphyMp4Projection projection = conversationItem.getProjection((RecyclerView) conversationItem.getParent());
|
||||||
|
CornerMask cornerMask = projection.getCornerMask();
|
||||||
|
|
||||||
|
canvas.clipRect(conversationItem.bodyBubble.getLeft(),
|
||||||
|
conversationItem.bodyBubble.getTop(),
|
||||||
|
conversationItem.bodyBubble.getRight(),
|
||||||
|
conversationItem.bodyBubble.getTop() + projection.getHeight());
|
||||||
|
|
||||||
|
canvas.drawColor(Color.BLACK);
|
||||||
|
cornerMask.mask(canvas);
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,8 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4DisplayUpdater;
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable;
|
||||||
import org.thoughtcrime.securesms.util.AccessibilityUtil;
|
import org.thoughtcrime.securesms.util.AccessibilityUtil;
|
||||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||||
|
|
||||||
|
@ -28,14 +30,17 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||||
private final SwipeAvailabilityProvider swipeAvailabilityProvider;
|
private final SwipeAvailabilityProvider swipeAvailabilityProvider;
|
||||||
private final ConversationItemTouchListener itemTouchListener;
|
private final ConversationItemTouchListener itemTouchListener;
|
||||||
private final OnSwipeListener onSwipeListener;
|
private final OnSwipeListener onSwipeListener;
|
||||||
|
private final GiphyMp4DisplayUpdater giphyMp4DisplayUpdater;
|
||||||
|
|
||||||
ConversationItemSwipeCallback(@NonNull SwipeAvailabilityProvider swipeAvailabilityProvider,
|
ConversationItemSwipeCallback(@NonNull SwipeAvailabilityProvider swipeAvailabilityProvider,
|
||||||
@NonNull OnSwipeListener onSwipeListener)
|
@NonNull OnSwipeListener onSwipeListener,
|
||||||
|
@NonNull GiphyMp4DisplayUpdater giphyMp4DisplayUpdater)
|
||||||
{
|
{
|
||||||
super(0, ItemTouchHelper.END);
|
super(0, ItemTouchHelper.END);
|
||||||
this.itemTouchListener = new ConversationItemTouchListener(this::updateLatestDownCoordinate);
|
this.itemTouchListener = new ConversationItemTouchListener(this::updateLatestDownCoordinate);
|
||||||
this.swipeAvailabilityProvider = swipeAvailabilityProvider;
|
this.swipeAvailabilityProvider = swipeAvailabilityProvider;
|
||||||
this.onSwipeListener = onSwipeListener;
|
this.onSwipeListener = onSwipeListener;
|
||||||
|
this.giphyMp4DisplayUpdater = giphyMp4DisplayUpdater;
|
||||||
this.shouldTriggerSwipeFeedback = true;
|
this.shouldTriggerSwipeFeedback = true;
|
||||||
this.canTriggerSwipe = true;
|
this.canTriggerSwipe = true;
|
||||||
}
|
}
|
||||||
|
@ -88,12 +93,14 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||||
|
|
||||||
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE && isCorrectSwipeDir) {
|
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE && isCorrectSwipeDir) {
|
||||||
ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView, Math.abs(dx), sign);
|
ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView, Math.abs(dx), sign);
|
||||||
|
updateVideoPlayer(recyclerView, viewHolder);
|
||||||
handleSwipeFeedback((ConversationItem) viewHolder.itemView, Math.abs(dx));
|
handleSwipeFeedback((ConversationItem) viewHolder.itemView, Math.abs(dx));
|
||||||
if (canTriggerSwipe) {
|
if (canTriggerSwipe) {
|
||||||
setTouchListener(recyclerView, viewHolder, Math.abs(dx));
|
setTouchListener(recyclerView, viewHolder, Math.abs(dx));
|
||||||
}
|
}
|
||||||
} else if (actionState == ItemTouchHelper.ACTION_STATE_IDLE || dx == 0) {
|
} else if (actionState == ItemTouchHelper.ACTION_STATE_IDLE || dx == 0) {
|
||||||
ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView, 0, 1);
|
ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView, 0, 1);
|
||||||
|
updateVideoPlayer(recyclerView, viewHolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dx == 0) {
|
if (dx == 0) {
|
||||||
|
@ -102,6 +109,12 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateVideoPlayer(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||||
|
if (viewHolder instanceof GiphyMp4Playable) {
|
||||||
|
giphyMp4DisplayUpdater.updateDisplay(recyclerView, (GiphyMp4Playable) viewHolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void handleSwipeFeedback(@NonNull ConversationItem item, float dx) {
|
private void handleSwipeFeedback(@NonNull ConversationItem item, float dx) {
|
||||||
if (dx > SWIPE_SUCCESS_DX && shouldTriggerSwipeFeedback) {
|
if (dx > SWIPE_SUCCESS_DX && shouldTriggerSwipeFeedback) {
|
||||||
vibrate(item.getContext());
|
vibrate(item.getContext());
|
||||||
|
@ -134,7 +147,7 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||||
case MotionEvent.ACTION_CANCEL:
|
case MotionEvent.ACTION_CANCEL:
|
||||||
swipeBack = true;
|
swipeBack = true;
|
||||||
shouldTriggerSwipeFeedback = false;
|
shouldTriggerSwipeFeedback = false;
|
||||||
resetProgressIfAnimationsDisabled(viewHolder);
|
resetProgressIfAnimationsDisabled(recyclerView, viewHolder);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -156,11 +169,12 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||||
recyclerView.cancelPendingInputEvents();
|
recyclerView.cancelPendingInputEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void resetProgressIfAnimationsDisabled(RecyclerView.ViewHolder viewHolder) {
|
private void resetProgressIfAnimationsDisabled(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||||
if (AccessibilityUtil.areAnimationsDisabled(viewHolder.itemView.getContext())) {
|
if (AccessibilityUtil.areAnimationsDisabled(viewHolder.itemView.getContext())) {
|
||||||
ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView,
|
ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView,
|
||||||
0f,
|
0f,
|
||||||
getSignFromDirection(viewHolder.itemView));
|
getSignFromDirection(viewHolder.itemView));
|
||||||
|
updateVideoPlayer(recyclerView, viewHolder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import android.view.View;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.components.MaskView;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.views.Stub;
|
import org.thoughtcrime.securesms.util.views.Stub;
|
||||||
|
@ -38,7 +39,7 @@ final class ConversationReactionDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
void show(@NonNull Activity activity,
|
void show(@NonNull Activity activity,
|
||||||
@NonNull View maskTarget,
|
@NonNull MaskView.MaskTarget maskTarget,
|
||||||
@NonNull Recipient conversationRecipient,
|
@NonNull Recipient conversationRecipient,
|
||||||
@NonNull MessageRecord messageRecord,
|
@NonNull MessageRecord messageRecord,
|
||||||
int maskPaddingBottom)
|
int maskPaddingBottom)
|
||||||
|
@ -46,7 +47,7 @@ final class ConversationReactionDelegate {
|
||||||
resolveOverlay().show(activity, maskTarget, conversationRecipient, messageRecord, maskPaddingBottom, lastSeenDownPoint);
|
resolveOverlay().show(activity, maskTarget, conversationRecipient, messageRecord, maskPaddingBottom, lastSeenDownPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showMask(@NonNull View maskTarget, int maskPaddingTop, int maskPaddingBottom) {
|
void showMask(@NonNull MaskView.MaskTarget maskTarget, int maskPaddingTop, int maskPaddingBottom) {
|
||||||
resolveOverlay().showMask(maskTarget, maskPaddingTop, maskPaddingBottom);
|
resolveOverlay().showMask(maskTarget, maskPaddingTop, maskPaddingBottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -145,7 +145,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void show(@NonNull Activity activity,
|
public void show(@NonNull Activity activity,
|
||||||
@NonNull View maskTarget,
|
@NonNull MaskView.MaskTarget maskTarget,
|
||||||
@NonNull Recipient conversationRecipient,
|
@NonNull Recipient conversationRecipient,
|
||||||
@NonNull MessageRecord messageRecord,
|
@NonNull MessageRecord messageRecord,
|
||||||
int maskPaddingBottom,
|
int maskPaddingBottom,
|
||||||
|
@ -209,7 +209,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showMask(@NonNull View maskTarget, int maskPaddingTop, int maskPaddingBottom) {
|
public void showMask(@NonNull MaskView.MaskTarget maskTarget, int maskPaddingTop, int maskPaddingBottom) {
|
||||||
maskView.setPadding(0, maskPaddingTop, 0, maskPaddingBottom);
|
maskView.setPadding(0, maskPaddingTop, 0, maskPaddingBottom);
|
||||||
maskView.setTarget(maskTarget);
|
maskView.setTarget(maskTarget);
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import androidx.core.content.ContextCompat;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.Observer;
|
import androidx.lifecycle.Observer;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.google.android.material.button.MaterialButton;
|
import com.google.android.material.button.MaterialButton;
|
||||||
|
|
||||||
|
@ -29,6 +30,7 @@ import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil;
|
||||||
import org.thoughtcrime.securesms.database.model.LiveUpdateMessage;
|
import org.thoughtcrime.securesms.database.model.LiveUpdateMessage;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.UpdateDescription;
|
import org.thoughtcrime.securesms.database.model.UpdateDescription;
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Projection;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
@ -40,6 +42,7 @@ import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||||
|
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
@ -102,7 +105,9 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||||
@Nullable String searchQuery,
|
@Nullable String searchQuery,
|
||||||
boolean pulseMention,
|
boolean pulseMention,
|
||||||
boolean hasWallpaper,
|
boolean hasWallpaper,
|
||||||
boolean isMessageRequestAccepted)
|
boolean isMessageRequestAccepted,
|
||||||
|
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
|
||||||
|
boolean allowedToPlayInline)
|
||||||
{
|
{
|
||||||
this.batchSelected = batchSelected;
|
this.batchSelected = batchSelected;
|
||||||
|
|
||||||
|
@ -182,6 +187,30 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||||
public void unbind() {
|
public void unbind() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showProjectionArea() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hideProjectionArea() {
|
||||||
|
throw new UnsupportedOperationException("Call makes no sense for a conversation update item");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAdapterPosition() {
|
||||||
|
throw new UnsupportedOperationException("Don't delegate to this method.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull GiphyMp4Projection getProjection(@NonNull RecyclerView recyclerView) {
|
||||||
|
throw new UnsupportedOperationException("ConversationUpdateItems cannot be projected into.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canPlayContent() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static final class RecipientObserverManager {
|
static final class RecipientObserverManager {
|
||||||
|
|
||||||
private final Observer<Recipient> recipientObserver;
|
private final Observer<Recipient> recipientObserver;
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
package org.thoughtcrime.securesms.conversation;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.ColorFilter;
|
||||||
|
import android.graphics.Path;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.graphics.Region;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drawable which lets you punch a hole through another drawable.
|
||||||
|
*/
|
||||||
|
public final class MaskDrawable extends Drawable {
|
||||||
|
|
||||||
|
private final RectF bounds = new RectF();
|
||||||
|
private final Path clipPath = new Path();
|
||||||
|
|
||||||
|
private Rect clipRect;
|
||||||
|
private float[] clipPathRadii;
|
||||||
|
|
||||||
|
private final Drawable wrapped;
|
||||||
|
|
||||||
|
public MaskDrawable(@NonNull Drawable wrapped) {
|
||||||
|
this.wrapped = wrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(@NonNull Canvas canvas) {
|
||||||
|
if (clipRect == null) {
|
||||||
|
wrapped.draw(canvas);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.save();
|
||||||
|
|
||||||
|
if (clipPathRadii != null) {
|
||||||
|
clipPath.reset();
|
||||||
|
bounds.set(clipRect);
|
||||||
|
clipPath.addRoundRect(bounds, clipPathRadii, Path.Direction.CW);
|
||||||
|
canvas.clipPath(clipPath, Region.Op.DIFFERENCE);
|
||||||
|
} else {
|
||||||
|
canvas.clipRect(clipRect, Region.Op.DIFFERENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapped.draw(canvas);
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAlpha(int alpha) {
|
||||||
|
wrapped.setAlpha(alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setColorFilter(@Nullable ColorFilter colorFilter) {
|
||||||
|
wrapped.setColorFilter(colorFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOpacity() {
|
||||||
|
return wrapped.getOpacity();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBounds(int left, int top, int right, int bottom) {
|
||||||
|
super.setBounds(left, top, right, bottom);
|
||||||
|
wrapped.setBounds(left, top, right, bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getPadding(@NonNull Rect padding) {
|
||||||
|
return wrapped.getPadding(padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMask(@Nullable Rect mask) {
|
||||||
|
this.clipRect = new Rect(mask);
|
||||||
|
|
||||||
|
invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCorners(@Nullable float[] clipPathRadii) {
|
||||||
|
this.clipPathRadii = clipPathRadii;
|
||||||
|
|
||||||
|
invalidateSelf();
|
||||||
|
}
|
||||||
|
}
|
|
@ -57,7 +57,7 @@ public final class ThreadBodyUtil {
|
||||||
for (Slide slide : record.getSlideDeck().getSlides()) {
|
for (Slide slide : record.getSlideDeck().getSlides()) {
|
||||||
hasVideo |= slide.hasVideo();
|
hasVideo |= slide.hasVideo();
|
||||||
hasImage |= slide.hasImage();
|
hasImage |= slide.hasImage();
|
||||||
hasGif |= slide instanceof GifSlide;
|
hasGif |= slide instanceof GifSlide || slide.isVideoGif();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasGif) {
|
if (hasGif) {
|
||||||
|
|
|
@ -1,116 +0,0 @@
|
||||||
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,12 @@
|
||||||
|
package org.thoughtcrime.securesms.giph.mp4;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the position and size of a GiphyMp4VideoPlayer. For use with gestures which
|
||||||
|
* move around the projectable areas videos should play back in.
|
||||||
|
*/
|
||||||
|
public interface GiphyMp4DisplayUpdater {
|
||||||
|
void updateDisplay(@NonNull RecyclerView recyclerView, @NonNull GiphyMp4Playable holder);
|
||||||
|
}
|
|
@ -1,9 +1,7 @@
|
||||||
package org.thoughtcrime.securesms.giph.mp4;
|
package org.thoughtcrime.securesms.giph.mp4;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
@ -15,13 +13,9 @@ import androidx.lifecycle.ViewModelProviders;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
|
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.R;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -55,44 +49,22 @@ public class GiphyMp4Fragment extends Fragment {
|
||||||
GiphyMp4ViewModel viewModel = ViewModelProviders.of(requireActivity(), new GiphyMp4ViewModel.Factory(isForMms)).get(GiphyMp4ViewModel.class);
|
GiphyMp4ViewModel viewModel = ViewModelProviders.of(requireActivity(), new GiphyMp4ViewModel.Factory(isForMms)).get(GiphyMp4ViewModel.class);
|
||||||
GiphyMp4MediaSourceFactory mediaSourceFactory = new GiphyMp4MediaSourceFactory(ApplicationDependencies.getOkHttpClient());
|
GiphyMp4MediaSourceFactory mediaSourceFactory = new GiphyMp4MediaSourceFactory(ApplicationDependencies.getOkHttpClient());
|
||||||
GiphyMp4Adapter adapter = new GiphyMp4Adapter(mediaSourceFactory, viewModel::saveToBlob);
|
GiphyMp4Adapter adapter = new GiphyMp4Adapter(mediaSourceFactory, viewModel::saveToBlob);
|
||||||
List<GiphyMp4PlayerHolder> holders = injectVideoViews(frameLayout);
|
List<GiphyMp4ProjectionPlayerHolder> holders = GiphyMp4ProjectionPlayerHolder.injectVideoViews(requireContext(),
|
||||||
GiphyMp4AdapterPlaybackControllerCallback callback = new GiphyMp4AdapterPlaybackControllerCallback(holders);
|
getViewLifecycleOwner().getLifecycle(),
|
||||||
|
frameLayout,
|
||||||
|
GiphyMp4PlaybackPolicy.maxSimultaneousPlaybackInSearchResults());
|
||||||
|
GiphyMp4ProjectionRecycler callback = new GiphyMp4ProjectionRecycler(holders);
|
||||||
|
|
||||||
recycler.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));
|
recycler.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));
|
||||||
recycler.setAdapter(adapter);
|
recycler.setAdapter(adapter);
|
||||||
recycler.setItemAnimator(null);
|
recycler.setItemAnimator(null);
|
||||||
progressBar.show();
|
progressBar.show();
|
||||||
|
|
||||||
GiphyMp4AdapterPlaybackController.attach(recycler, callback, GiphyMp4PlaybackPolicy.maxSimultaneousPlaybackInSearchResults());
|
GiphyMp4PlaybackController.attach(recycler, callback, GiphyMp4PlaybackPolicy.maxSimultaneousPlaybackInSearchResults());
|
||||||
viewModel.getImages().observe(getViewLifecycleOwner(), images -> {
|
viewModel.getImages().observe(getViewLifecycleOwner(), images -> {
|
||||||
nothingFound.setVisibility(images.isEmpty() ? View.VISIBLE : View.INVISIBLE);
|
nothingFound.setVisibility(images.isEmpty() ? View.VISIBLE : View.INVISIBLE);
|
||||||
adapter.submitList(images, progressBar::hide);
|
adapter.submitList(images, progressBar::hide);
|
||||||
});
|
});
|
||||||
viewModel.getPagingController().observe(getViewLifecycleOwner(), adapter::setPagingController);
|
viewModel.getPagingController().observe(getViewLifecycleOwner(), adapter::setPagingController);
|
||||||
}
|
}
|
||||||
|
|
||||||
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,50 @@
|
||||||
|
package org.thoughtcrime.securesms.giph.mp4;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
|
||||||
|
public interface GiphyMp4Playable {
|
||||||
|
/**
|
||||||
|
* Shows the area in which a video would be projected. Called when a video will not
|
||||||
|
* play back.
|
||||||
|
*/
|
||||||
|
void showProjectionArea();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the area in which a video would be projected. Called when a video is ready
|
||||||
|
* to play back.
|
||||||
|
*/
|
||||||
|
void hideProjectionArea();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The MediaSource to play back in the given VideoPlayer
|
||||||
|
*/
|
||||||
|
default @Nullable MediaSource getMediaSource() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Playback policy enforcer, or null to loop forever.
|
||||||
|
*/
|
||||||
|
default @Nullable GiphyMp4PlaybackPolicyEnforcer getPlaybackPolicyEnforcer() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The position this item is in it's corresponding adapter
|
||||||
|
*/
|
||||||
|
int getAdapterPosition();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Width, height, and (x,y) of view which video player will "project" into
|
||||||
|
*/
|
||||||
|
@NonNull GiphyMp4Projection getProjection(@NonNull RecyclerView recyclerview);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies whether the content can start playing.
|
||||||
|
*/
|
||||||
|
boolean canPlayContent();
|
||||||
|
}
|
|
@ -3,33 +3,39 @@ package org.thoughtcrime.securesms.giph.mp4;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.VisibleForTesting;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
|
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
|
||||||
|
|
||||||
|
import com.annimon.stream.Collectors;
|
||||||
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controls playback of gifs in a {@link GiphyMp4Adapter}. The maximum number of gifs that will play back at any one
|
* Controls playback of gifs in a {@link RecyclerView}. 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
|
* time is determined by the passed parameter, and the exact gifs that play back is algorithmically determined, starting
|
||||||
* with the center-most gifs.
|
* with the center-most gifs.
|
||||||
* <p>
|
* <p>
|
||||||
* This algorithm is devised to play back only those gifs which the user is most likely looking at.
|
* 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 {
|
public final class GiphyMp4PlaybackController extends RecyclerView.OnScrollListener implements View.OnLayoutChangeListener {
|
||||||
|
|
||||||
private final int maxSimultaneousPlayback;
|
private final int maxSimultaneousPlayback;
|
||||||
private final Callback callback;
|
private final Callback callback;
|
||||||
|
|
||||||
private GiphyMp4AdapterPlaybackController(@NonNull Callback callback, int maxSimultaneousPlayback) {
|
private GiphyMp4PlaybackController(@NonNull Callback callback, int maxSimultaneousPlayback) {
|
||||||
this.maxSimultaneousPlayback = maxSimultaneousPlayback;
|
this.maxSimultaneousPlayback = maxSimultaneousPlayback;
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void attach(@NonNull RecyclerView recyclerView, @NonNull Callback callback, int maxSimultaneousPlayback) {
|
public static void attach(@NonNull RecyclerView recyclerView, @NonNull Callback callback, int maxSimultaneousPlayback) {
|
||||||
GiphyMp4AdapterPlaybackController controller = new GiphyMp4AdapterPlaybackController(callback, maxSimultaneousPlayback);
|
GiphyMp4PlaybackController controller = new GiphyMp4PlaybackController(callback, maxSimultaneousPlayback);
|
||||||
|
|
||||||
recyclerView.addOnScrollListener(controller);
|
recyclerView.addOnScrollListener(controller);
|
||||||
recyclerView.addOnLayoutChangeListener(controller);
|
recyclerView.addOnLayoutChangeListener(controller);
|
||||||
|
@ -57,24 +63,30 @@ final class GiphyMp4AdapterPlaybackController extends RecyclerView.OnScrollListe
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int[] firstVisiblePositions = findFirstVisibleItemPositions(layoutManager);
|
List<GiphyMp4Playable> playables = new LinkedList<>();
|
||||||
int[] lastVisiblePositions = findLastVisibleItemPositions(layoutManager);
|
Set<Integer> playablePositions = new HashSet<>();
|
||||||
|
|
||||||
GiphyMp4PlaybackRange playbackRange = getPlaybackRangeForMaximumDistance(firstVisiblePositions, lastVisiblePositions);
|
|
||||||
|
|
||||||
if (playbackRange != null) {
|
|
||||||
List<GiphyMp4ViewHolder> holders = new LinkedList<>();
|
|
||||||
|
|
||||||
for (int i = 0; i < recyclerView.getChildCount(); i++) {
|
for (int i = 0; i < recyclerView.getChildCount(); i++) {
|
||||||
GiphyMp4ViewHolder viewHolder = (GiphyMp4ViewHolder) recyclerView.getChildViewHolder(recyclerView.getChildAt(i));
|
RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(recyclerView.getChildAt(i));
|
||||||
holders.add(viewHolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback.update(holders, playbackRange);
|
if (holder instanceof GiphyMp4Playable) {
|
||||||
|
GiphyMp4Playable playable = (GiphyMp4Playable) holder;
|
||||||
|
playables.add(playable);
|
||||||
|
|
||||||
|
if (playable.canPlayContent()) {
|
||||||
|
playablePositions.add(playable.getAdapterPosition());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable GiphyMp4PlaybackRange getPlaybackRangeForMaximumDistance(int[] firstVisiblePositions, int[] lastVisiblePositions) {
|
int[] firstVisiblePositions = findFirstVisibleItemPositions(layoutManager);
|
||||||
|
int[] lastVisiblePositions = findLastVisibleItemPositions(layoutManager);
|
||||||
|
Set<Integer> playbackSet = getPlaybackSetForMaximumDistance(playablePositions, firstVisiblePositions, lastVisiblePositions);
|
||||||
|
|
||||||
|
callback.update(recyclerView, playables, playbackSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NonNull Set<Integer> getPlaybackSetForMaximumDistance(@NonNull Set<Integer> playablePositions, int[] firstVisiblePositions, int[] lastVisiblePositions) {
|
||||||
int firstVisiblePosition = Integer.MAX_VALUE;
|
int firstVisiblePosition = Integer.MAX_VALUE;
|
||||||
int lastVisiblePosition = Integer.MIN_VALUE;
|
int lastVisiblePosition = Integer.MIN_VALUE;
|
||||||
|
|
||||||
|
@ -83,28 +95,15 @@ final class GiphyMp4AdapterPlaybackController extends RecyclerView.OnScrollListe
|
||||||
lastVisiblePosition = Math.max(lastVisiblePosition, lastVisiblePositions[i]);
|
lastVisiblePosition = Math.max(lastVisiblePosition, lastVisiblePositions[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return getPlaybackRange(firstVisiblePosition, lastVisiblePosition);
|
return getPlaybackSet(playablePositions, firstVisiblePosition, lastVisiblePosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable GiphyMp4PlaybackRange getPlaybackRange(int firstVisiblePosition, int lastVisiblePosition) {
|
private @NonNull Set<Integer> getPlaybackSet(@NonNull Set<Integer> playablePositions, int firstVisiblePosition, int lastVisiblePosition) {
|
||||||
int distance = lastVisiblePosition - firstVisiblePosition;
|
return Stream.rangeClosed(firstVisiblePosition, lastVisiblePosition)
|
||||||
|
.sorted(new RangeComparator(firstVisiblePosition, lastVisiblePosition))
|
||||||
if (maxSimultaneousPlayback == 0) {
|
.filter(playablePositions::contains)
|
||||||
return null;
|
.limit(maxSimultaneousPlayback)
|
||||||
}
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
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) {
|
private static int[] findFirstVisibleItemPositions(@NonNull RecyclerView.LayoutManager layoutManager) {
|
||||||
|
@ -128,6 +127,31 @@ final class GiphyMp4AdapterPlaybackController extends RecyclerView.OnScrollListe
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
void update(@NonNull List<GiphyMp4ViewHolder> holders, @NonNull GiphyMp4PlaybackRange range);
|
void update(@NonNull RecyclerView recyclerView, @NonNull List<GiphyMp4Playable> holders, @NonNull Set<Integer> playbackSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static final class RangeComparator implements Comparator<Integer> {
|
||||||
|
|
||||||
|
private final int center;
|
||||||
|
|
||||||
|
RangeComparator(int firstVisiblePosition, int lastVisiblePosition) {
|
||||||
|
int delta = lastVisiblePosition - firstVisiblePosition;
|
||||||
|
|
||||||
|
center = firstVisiblePosition + (delta / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compare(Integer o1, Integer o2) {
|
||||||
|
int distance1 = Math.abs(o1 - center);
|
||||||
|
int distance2 = Math.abs(o2 - center);
|
||||||
|
int comp = Integer.compare(distance1, distance2);
|
||||||
|
|
||||||
|
if (comp == 0) {
|
||||||
|
return Integer.compare(o1, o2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return comp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,11 +4,9 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil;
|
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.util.DeviceProperties;
|
import org.thoughtcrime.securesms.util.DeviceProperties;
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@ -17,28 +15,44 @@ import java.util.concurrent.TimeUnit;
|
||||||
*/
|
*/
|
||||||
public final class GiphyMp4PlaybackPolicy {
|
public final class GiphyMp4PlaybackPolicy {
|
||||||
|
|
||||||
|
private static final int MAXIMUM_SUPPORTED_PLAYBACK_PRE_23 = 6;
|
||||||
|
private static final int MAXIMUM_SUPPORTED_PLAYBACK_PRE_23_LOW_MEM = 3;
|
||||||
|
private static final float SEARCH_RESULT_RATIO = 0.75f;
|
||||||
|
|
||||||
private GiphyMp4PlaybackPolicy() { }
|
private GiphyMp4PlaybackPolicy() { }
|
||||||
|
|
||||||
public static boolean sendAsMp4() {
|
public static boolean sendAsMp4() {
|
||||||
return FeatureFlags.mp4GifSendSupport();
|
return FeatureFlags.mp4GifSendSupport();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean autoplay() {
|
||||||
|
return !DeviceProperties.isLowMemoryDevice(ApplicationDependencies.getApplication());
|
||||||
|
}
|
||||||
|
|
||||||
public static int maxRepeatsOfSinglePlayback() {
|
public static int maxRepeatsOfSinglePlayback() {
|
||||||
return 3;
|
return 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long maxDurationOfSinglePlayback() {
|
public static long maxDurationOfSinglePlayback() {
|
||||||
return TimeUnit.SECONDS.toMillis(6);
|
return TimeUnit.SECONDS.toMillis(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int maxSimultaneousPlaybackInConversation() {
|
||||||
|
return maxSimultaneousPlaybackWithRatio(1f - SEARCH_RESULT_RATIO);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int maxSimultaneousPlaybackInSearchResults() {
|
public static int maxSimultaneousPlaybackInSearchResults() {
|
||||||
|
return maxSimultaneousPlaybackWithRatio(SEARCH_RESULT_RATIO);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int maxSimultaneousPlaybackWithRatio(float ratio) {
|
||||||
int maxInstances = 0;
|
int maxInstances = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
MediaCodecInfo info = MediaCodecUtil.getDecoderInfo(MimeTypes.VIDEO_H264, false);
|
MediaCodecInfo info = MediaCodecUtil.getDecoderInfo(MimeTypes.VIDEO_H264, false);
|
||||||
|
|
||||||
if (info != null) {
|
if (info != null && info.getMaxSupportedInstances() > 0) {
|
||||||
maxInstances = (int) (info.getMaxSupportedInstances() * 0.75f);
|
maxInstances = (int) (info.getMaxSupportedInstances() * ratio);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (MediaCodecUtil.DecoderQueryException ignored) {
|
} catch (MediaCodecUtil.DecoderQueryException ignored) {
|
||||||
|
@ -49,9 +63,9 @@ public final class GiphyMp4PlaybackPolicy {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DeviceProperties.isLowMemoryDevice(ApplicationDependencies.getApplication())) {
|
if (DeviceProperties.isLowMemoryDevice(ApplicationDependencies.getApplication())) {
|
||||||
return 2;
|
return (int) (MAXIMUM_SUPPORTED_PLAYBACK_PRE_23_LOW_MEM * ratio);
|
||||||
} else {
|
} else {
|
||||||
return 6;
|
return (int) (MAXIMUM_SUPPORTED_PLAYBACK_PRE_23 * ratio);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
package org.thoughtcrime.securesms.giph.mp4;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enforces a video player to play back a specified number of loops given
|
||||||
|
* video length and device policy.
|
||||||
|
*/
|
||||||
|
public final class GiphyMp4PlaybackPolicyEnforcer {
|
||||||
|
|
||||||
|
private final Callback callback;
|
||||||
|
private final long maxDurationOfSinglePlayback;
|
||||||
|
private final long maxRepeatsOfSinglePlayback;
|
||||||
|
|
||||||
|
private long loopsRemaining = -1;
|
||||||
|
|
||||||
|
public GiphyMp4PlaybackPolicyEnforcer(@NonNull Callback callback) {
|
||||||
|
this(callback,
|
||||||
|
GiphyMp4PlaybackPolicy.maxDurationOfSinglePlayback(),
|
||||||
|
GiphyMp4PlaybackPolicy.maxRepeatsOfSinglePlayback());
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
GiphyMp4PlaybackPolicyEnforcer(@NonNull Callback callback,
|
||||||
|
long maxDurationOfSinglePlayback,
|
||||||
|
long maxRepeatsOfSinglePlayback)
|
||||||
|
{
|
||||||
|
this.callback = callback;
|
||||||
|
this.maxDurationOfSinglePlayback = maxDurationOfSinglePlayback;
|
||||||
|
this.maxRepeatsOfSinglePlayback = maxRepeatsOfSinglePlayback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setMediaDuration(long duration) {
|
||||||
|
long maxLoopsByDuration = Math.max(1, maxDurationOfSinglePlayback / duration);
|
||||||
|
|
||||||
|
loopsRemaining = Math.min(maxLoopsByDuration, maxRepeatsOfSinglePlayback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean endPlayback() {
|
||||||
|
if (loopsRemaining < 0) throw new IllegalStateException("Must call setMediaDuration before calling this method.");
|
||||||
|
else if (loopsRemaining == 0) return true;
|
||||||
|
else {
|
||||||
|
loopsRemaining--;
|
||||||
|
if (loopsRemaining == 0) {
|
||||||
|
callback.onPlaybackWillEnd();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public interface Callback {
|
||||||
|
void onPlaybackWillEnd();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,46 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
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,63 @@
|
||||||
|
package org.thoughtcrime.securesms.giph.mp4;
|
||||||
|
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewParent;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.components.CornerMask;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the position and size of the area where a video should play.
|
||||||
|
*/
|
||||||
|
public final class GiphyMp4Projection {
|
||||||
|
|
||||||
|
private final float x;
|
||||||
|
private final float y;
|
||||||
|
private final int width;
|
||||||
|
private final int height;
|
||||||
|
private final CornerMask cornerMask;
|
||||||
|
|
||||||
|
public GiphyMp4Projection(float x, float y, int width, int height, @Nullable CornerMask cornerMask) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.cornerMask = cornerMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getX() {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getY() {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getWidth() {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHeight() {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable CornerMask getCornerMask() {
|
||||||
|
return cornerMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull GiphyMp4Projection translateX(float xTranslation) {
|
||||||
|
return new GiphyMp4Projection(x + xTranslation, y, width, height, cornerMask);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NonNull GiphyMp4Projection forView(@NonNull RecyclerView recyclerView, @NonNull View view, @Nullable CornerMask cornerMask) {
|
||||||
|
Rect viewBounds = new Rect();
|
||||||
|
|
||||||
|
view.getDrawingRect(viewBounds);
|
||||||
|
recyclerView.offsetDescendantRectToMyCoords(view, viewBounds);
|
||||||
|
return new GiphyMp4Projection(viewBounds.left, viewBounds.top, view.getWidth(), view.getHeight(), cornerMask);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
package org.thoughtcrime.securesms.giph.mp4;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.lifecycle.Lifecycle;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.ExoPlayer;
|
||||||
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||||
|
|
||||||
|
import org.signal.glide.Log;
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.components.CornerMask;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object which holds on to an injected video player.
|
||||||
|
*/
|
||||||
|
public final class GiphyMp4ProjectionPlayerHolder implements Player.EventListener {
|
||||||
|
private final FrameLayout container;
|
||||||
|
private final GiphyMp4VideoPlayer player;
|
||||||
|
|
||||||
|
private Runnable onPlaybackReady;
|
||||||
|
private MediaSource mediaSource;
|
||||||
|
private GiphyMp4PlaybackPolicyEnforcer policyEnforcer;
|
||||||
|
|
||||||
|
private GiphyMp4ProjectionPlayerHolder(@NonNull FrameLayout container, @NonNull GiphyMp4VideoPlayer player) {
|
||||||
|
this.container = container;
|
||||||
|
this.player = player;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull FrameLayout getContainer() {
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playContent(@NonNull MediaSource mediaSource, @Nullable GiphyMp4PlaybackPolicyEnforcer policyEnforcer) {
|
||||||
|
this.mediaSource = mediaSource;
|
||||||
|
this.policyEnforcer = policyEnforcer;
|
||||||
|
|
||||||
|
player.setVideoSource(mediaSource);
|
||||||
|
player.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearMedia() {
|
||||||
|
this.mediaSource = null;
|
||||||
|
this.policyEnforcer = null;
|
||||||
|
player.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable MediaSource getMediaSource() {
|
||||||
|
return mediaSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnPlaybackReady(@Nullable Runnable onPlaybackReady) {
|
||||||
|
this.onPlaybackReady = onPlaybackReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||||
|
if (playbackState == Player.STATE_READY) {
|
||||||
|
if (onPlaybackReady != null) {
|
||||||
|
if (policyEnforcer != null) {
|
||||||
|
policyEnforcer.setMediaDuration(player.getDuration());
|
||||||
|
}
|
||||||
|
onPlaybackReady.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPositionDiscontinuity(int reason) {
|
||||||
|
if (policyEnforcer != null && reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) {
|
||||||
|
if (policyEnforcer.endPlayback()) {
|
||||||
|
player.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NonNull List<GiphyMp4ProjectionPlayerHolder> injectVideoViews(@NonNull Context context,
|
||||||
|
@NonNull Lifecycle lifecycle,
|
||||||
|
@NonNull ViewGroup viewGroup,
|
||||||
|
int nPlayers)
|
||||||
|
{
|
||||||
|
List<GiphyMp4ProjectionPlayerHolder> holders = new ArrayList<>(nPlayers);
|
||||||
|
GiphyMp4ExoPlayerProvider playerProvider = new GiphyMp4ExoPlayerProvider(context);
|
||||||
|
|
||||||
|
for (int i = 0; i < nPlayers; i++) {
|
||||||
|
FrameLayout container = (FrameLayout) LayoutInflater.from(context)
|
||||||
|
.inflate(R.layout.giphy_mp4_player, viewGroup, false);
|
||||||
|
GiphyMp4VideoPlayer player = container.findViewById(R.id.video_player);
|
||||||
|
ExoPlayer exoPlayer = playerProvider.create();
|
||||||
|
GiphyMp4ProjectionPlayerHolder holder = new GiphyMp4ProjectionPlayerHolder(container, player);
|
||||||
|
|
||||||
|
lifecycle.addObserver(player);
|
||||||
|
player.setExoPlayer(exoPlayer);
|
||||||
|
player.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FILL);
|
||||||
|
exoPlayer.addListener(holder);
|
||||||
|
|
||||||
|
holders.add(holder);
|
||||||
|
viewGroup.addView(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
return holders;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCornerMask(@Nullable CornerMask cornerMask) {
|
||||||
|
player.setCornerMask(cornerMask);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
package org.thoughtcrime.securesms.giph.mp4;
|
||||||
|
|
||||||
|
import android.util.SparseArray;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logic for updating content and positioning of videos as the user scrolls the list of gifs.
|
||||||
|
*/
|
||||||
|
public final class GiphyMp4ProjectionRecycler implements GiphyMp4PlaybackController.Callback, GiphyMp4DisplayUpdater {
|
||||||
|
|
||||||
|
private final List<GiphyMp4ProjectionPlayerHolder> holders;
|
||||||
|
private final SparseArray<GiphyMp4ProjectionPlayerHolder> playing;
|
||||||
|
private final SparseArray<GiphyMp4ProjectionPlayerHolder> notPlaying;
|
||||||
|
|
||||||
|
public GiphyMp4ProjectionRecycler(@NonNull List<GiphyMp4ProjectionPlayerHolder> holders) {
|
||||||
|
this.holders = holders;
|
||||||
|
this.playing = new SparseArray<>(holders.size());
|
||||||
|
this.notPlaying = new SparseArray<>(holders.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(@NonNull RecyclerView recyclerView,
|
||||||
|
@NonNull List<GiphyMp4Playable> holders,
|
||||||
|
@NonNull Set<Integer> playbackSet)
|
||||||
|
{
|
||||||
|
stopAndReleaseAssignedVideos(playbackSet);
|
||||||
|
|
||||||
|
for (final GiphyMp4Playable holder : holders) {
|
||||||
|
if (playbackSet.contains(holder.getAdapterPosition())) {
|
||||||
|
startPlayback(acquireHolderForPosition(holder.getAdapterPosition()), holder);
|
||||||
|
} else {
|
||||||
|
holder.showProjectionArea();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final GiphyMp4Playable holder : holders) {
|
||||||
|
updateDisplay(recyclerView, holder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateDisplay(@NonNull RecyclerView recyclerView, @NonNull GiphyMp4Playable holder) {
|
||||||
|
GiphyMp4ProjectionPlayerHolder playerHolder = getCurrentHolder(holder.getAdapterPosition());
|
||||||
|
if (playerHolder != null) {
|
||||||
|
updateDisplay(recyclerView, playerHolder, holder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable View getVideoPlayerAtAdapterPosition(int adapterPosition) {
|
||||||
|
GiphyMp4ProjectionPlayerHolder holder = getCurrentHolder(adapterPosition);
|
||||||
|
|
||||||
|
if (holder != null) return holder.getContainer();
|
||||||
|
else return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopAndReleaseAssignedVideos(@NonNull Set<Integer> playbackSet) {
|
||||||
|
List<Integer> markedForDeletion = new ArrayList<>(playing.size());
|
||||||
|
for (int i = 0; i < playing.size(); i++) {
|
||||||
|
if (!playbackSet.contains(playing.keyAt(i))) {
|
||||||
|
notPlaying.put(playing.keyAt(i), playing.valueAt(i));
|
||||||
|
playing.valueAt(i).clearMedia();
|
||||||
|
playing.valueAt(i).setOnPlaybackReady(null);
|
||||||
|
markedForDeletion.add(playing.keyAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final Integer key : markedForDeletion) {
|
||||||
|
playing.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDisplay(@NonNull RecyclerView recyclerView, @NonNull GiphyMp4ProjectionPlayerHolder holder, @NonNull GiphyMp4Playable giphyMp4Playable) {
|
||||||
|
GiphyMp4Projection projection = giphyMp4Playable.getProjection(recyclerView);
|
||||||
|
|
||||||
|
holder.getContainer().setX(projection.getX());
|
||||||
|
holder.getContainer().setY(projection.getY());
|
||||||
|
|
||||||
|
ViewGroup.LayoutParams params = holder.getContainer().getLayoutParams();
|
||||||
|
if (params.width != projection.getWidth() || params.height != projection.getHeight()) {
|
||||||
|
params.width = projection.getWidth();
|
||||||
|
params.height = projection.getHeight();
|
||||||
|
holder.getContainer().setLayoutParams(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.setCornerMask(projection.getCornerMask());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startPlayback(@NonNull GiphyMp4ProjectionPlayerHolder holder, @NonNull GiphyMp4Playable giphyMp4Playable) {
|
||||||
|
if (!Objects.equals(holder.getMediaSource(), giphyMp4Playable.getMediaSource())) {
|
||||||
|
holder.setOnPlaybackReady(null);
|
||||||
|
giphyMp4Playable.showProjectionArea();
|
||||||
|
|
||||||
|
holder.setOnPlaybackReady(giphyMp4Playable::hideProjectionArea);
|
||||||
|
holder.playContent(giphyMp4Playable.getMediaSource(), giphyMp4Playable.getPlaybackPolicyEnforcer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable GiphyMp4ProjectionPlayerHolder 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 GiphyMp4ProjectionPlayerHolder acquireHolderForPosition(int adapterPosition) {
|
||||||
|
GiphyMp4ProjectionPlayerHolder 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,17 @@
|
||||||
package org.thoughtcrime.securesms.giph.mp4;
|
package org.thoughtcrime.securesms.giph.mp4;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlayer;
|
import com.google.android.exoplayer2.ExoPlayer;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||||
|
@ -16,6 +19,7 @@ import com.google.android.exoplayer2.ui.PlayerView;
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.components.CornerMask;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Video Player class specifically created for the GiphyMp4Fragment.
|
* Video Player class specifically created for the GiphyMp4Fragment.
|
||||||
|
@ -27,6 +31,7 @@ public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLif
|
||||||
|
|
||||||
private final PlayerView exoView;
|
private final PlayerView exoView;
|
||||||
private ExoPlayer exoPlayer;
|
private ExoPlayer exoPlayer;
|
||||||
|
private CornerMask cornerMask;
|
||||||
|
|
||||||
public GiphyMp4VideoPlayer(Context context) {
|
public GiphyMp4VideoPlayer(Context context) {
|
||||||
this(context, null);
|
this(context, null);
|
||||||
|
@ -50,6 +55,14 @@ public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLif
|
||||||
super.onDetachedFromWindow();
|
super.onDetachedFromWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override protected void dispatchDraw(Canvas canvas) {
|
||||||
|
super.dispatchDraw(canvas);
|
||||||
|
|
||||||
|
if (cornerMask != null) {
|
||||||
|
cornerMask.mask(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void setExoPlayer(@NonNull ExoPlayer exoPlayer) {
|
void setExoPlayer(@NonNull ExoPlayer exoPlayer) {
|
||||||
exoView.setPlayer(exoPlayer);
|
exoView.setPlayer(exoPlayer);
|
||||||
this.exoPlayer = exoPlayer;
|
this.exoPlayer = exoPlayer;
|
||||||
|
@ -59,6 +72,11 @@ public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLif
|
||||||
exoPlayer.prepare(mediaSource);
|
exoPlayer.prepare(mediaSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setCornerMask(@Nullable CornerMask cornerMask) {
|
||||||
|
this.cornerMask = new CornerMask(this, cornerMask);
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
void play() {
|
void play() {
|
||||||
if (exoPlayer != null) {
|
if (exoPlayer != null) {
|
||||||
exoPlayer.setPlayWhenReady(true);
|
exoPlayer.setPlayWhenReady(true);
|
||||||
|
@ -71,6 +89,14 @@ public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long getDuration() {
|
||||||
|
if (exoPlayer != null) {
|
||||||
|
return exoPlayer.getDuration();
|
||||||
|
} else {
|
||||||
|
return C.LENGTH_UNSET;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void setResizeMode(@AspectRatioFrameLayout.ResizeMode int resizeMode) {
|
void setResizeMode(@AspectRatioFrameLayout.ResizeMode int resizeMode) {
|
||||||
exoView.setResizeMode(resizeMode);
|
exoView.setResizeMode(resizeMode);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ import org.thoughtcrime.securesms.util.Util;
|
||||||
/**
|
/**
|
||||||
* Holds a view which will either play back an MP4 gif or show its still.
|
* Holds a view which will either play back an MP4 gif or show its still.
|
||||||
*/
|
*/
|
||||||
final class GiphyMp4ViewHolder extends RecyclerView.ViewHolder {
|
final class GiphyMp4ViewHolder extends RecyclerView.ViewHolder implements GiphyMp4Playable {
|
||||||
|
|
||||||
private final AspectRatioFrameLayout container;
|
private final AspectRatioFrameLayout container;
|
||||||
private final ImageView stillImage;
|
private final ImageView stillImage;
|
||||||
|
@ -62,14 +62,31 @@ final class GiphyMp4ViewHolder extends RecyclerView.ViewHolder {
|
||||||
itemView.setOnClickListener(v -> listener.onClick(giphyImage));
|
itemView.setOnClickListener(v -> listener.onClick(giphyImage));
|
||||||
}
|
}
|
||||||
|
|
||||||
void show() {
|
@Override
|
||||||
|
public void showProjectionArea() {
|
||||||
container.setAlpha(1f);
|
container.setAlpha(1f);
|
||||||
}
|
}
|
||||||
|
|
||||||
void hide() {
|
@Override
|
||||||
|
public void hideProjectionArea() {
|
||||||
container.setAlpha(0f);
|
container.setAlpha(0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull MediaSource getMediaSource() {
|
||||||
|
return mediaSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull GiphyMp4Projection getProjection(@NonNull RecyclerView recyclerView) {
|
||||||
|
return GiphyMp4Projection.forView(recyclerView, itemView, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canPlayContent() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private void loadPlaceholderImage(@NonNull GiphyImage giphyImage) {
|
private void loadPlaceholderImage(@NonNull GiphyImage giphyImage) {
|
||||||
GlideApp.with(itemView)
|
GlideApp.with(itemView)
|
||||||
.load(new ChunkedImageUrl(giphyImage.getStillUrl()))
|
.load(new ChunkedImageUrl(giphyImage.getStillUrl()))
|
||||||
|
@ -78,8 +95,4 @@ final class GiphyMp4ViewHolder extends RecyclerView.ViewHolder {
|
||||||
.transition(DrawableTransitionOptions.withCrossFade())
|
.transition(DrawableTransitionOptions.withCrossFade())
|
||||||
.into(stillImage);
|
.into(stillImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull MediaSource getMediaSource() {
|
|
||||||
return mediaSource;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.content.Intent;
|
||||||
import android.graphics.drawable.ColorDrawable;
|
import android.graphics.drawable.ColorDrawable;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
|
@ -15,6 +16,9 @@ import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackController;
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ProjectionPlayerHolder;
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ProjectionRecycler;
|
||||||
import org.thoughtcrime.securesms.messagedetails.MessageDetailsAdapter.MessageDetailsViewState;
|
import org.thoughtcrime.securesms.messagedetails.MessageDetailsAdapter.MessageDetailsViewState;
|
||||||
import org.thoughtcrime.securesms.messagedetails.MessageDetailsViewModel.Factory;
|
import org.thoughtcrime.securesms.messagedetails.MessageDetailsViewModel.Factory;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
|
@ -65,6 +69,7 @@ public final class MessageDetailsActivity extends PassphraseRequiredActivity {
|
||||||
initializeList();
|
initializeList();
|
||||||
initializeViewModel();
|
initializeViewModel();
|
||||||
initializeActionBar();
|
initializeActionBar();
|
||||||
|
initializeVideoPlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -113,6 +118,15 @@ public final class MessageDetailsActivity extends PassphraseRequiredActivity {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void initializeVideoPlayer() {
|
||||||
|
FrameLayout videoContainer = findViewById(R.id.video_container);
|
||||||
|
RecyclerView recyclerView = findViewById(R.id.message_details_list);
|
||||||
|
List<GiphyMp4ProjectionPlayerHolder> holders = GiphyMp4ProjectionPlayerHolder.injectVideoViews(this, getLifecycle(), videoContainer, 1);
|
||||||
|
GiphyMp4ProjectionRecycler callback = new GiphyMp4ProjectionRecycler(holders);
|
||||||
|
|
||||||
|
GiphyMp4PlaybackController.attach(recyclerView, callback, 1);
|
||||||
|
}
|
||||||
|
|
||||||
private void initializeActionBar() {
|
private void initializeActionBar() {
|
||||||
requireSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
requireSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
requireSupportActionBar().setTitle(R.string.AndroidManifest__message_details);
|
requireSupportActionBar().setTitle(R.string.AndroidManifest__message_details);
|
||||||
|
|
|
@ -3,7 +3,11 @@ package org.thoughtcrime.securesms.messagedetails;
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
import android.content.ClipboardManager;
|
import android.content.ClipboardManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.drawable.ColorDrawable;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewStub;
|
import android.view.ViewStub;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
@ -12,16 +16,24 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
|
||||||
import org.signal.core.util.ThreadUtil;
|
import org.signal.core.util.ThreadUtil;
|
||||||
import org.signal.core.util.concurrent.SignalExecutors;
|
import org.signal.core.util.concurrent.SignalExecutors;
|
||||||
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationItem;
|
import org.thoughtcrime.securesms.conversation.ConversationItem;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||||
|
import org.thoughtcrime.securesms.conversation.MaskDrawable;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable;
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer;
|
||||||
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Projection;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||||
import org.thoughtcrime.securesms.util.DateUtils;
|
import org.thoughtcrime.securesms.util.DateUtils;
|
||||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||||
|
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
import java.sql.Date;
|
import java.sql.Date;
|
||||||
|
@ -29,7 +41,7 @@ import java.text.SimpleDateFormat;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
final class MessageHeaderViewHolder extends RecyclerView.ViewHolder {
|
final class MessageHeaderViewHolder extends RecyclerView.ViewHolder implements GiphyMp4Playable {
|
||||||
private final TextView sentDate;
|
private final TextView sentDate;
|
||||||
private final TextView receivedDate;
|
private final TextView receivedDate;
|
||||||
private final TextView expiresIn;
|
private final TextView expiresIn;
|
||||||
|
@ -42,6 +54,7 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder {
|
||||||
private final ViewStub updateStub;
|
private final ViewStub updateStub;
|
||||||
private final ViewStub sentStub;
|
private final ViewStub sentStub;
|
||||||
private final ViewStub receivedStub;
|
private final ViewStub receivedStub;
|
||||||
|
private final MaskDrawable maskDrawable;
|
||||||
|
|
||||||
private GlideRequests glideRequests;
|
private GlideRequests glideRequests;
|
||||||
private ConversationItem conversationItem;
|
private ConversationItem conversationItem;
|
||||||
|
@ -63,6 +76,9 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder {
|
||||||
updateStub = itemView.findViewById(R.id.message_details_header_message_view_update);
|
updateStub = itemView.findViewById(R.id.message_details_header_message_view_update);
|
||||||
sentStub = itemView.findViewById(R.id.message_details_header_message_view_sent_multimedia);
|
sentStub = itemView.findViewById(R.id.message_details_header_message_view_sent_multimedia);
|
||||||
receivedStub = itemView.findViewById(R.id.message_details_header_message_view_received_multimedia);
|
receivedStub = itemView.findViewById(R.id.message_details_header_message_view_received_multimedia);
|
||||||
|
|
||||||
|
maskDrawable = new MaskDrawable(itemView.getBackground());
|
||||||
|
itemView.setBackground(maskDrawable);
|
||||||
}
|
}
|
||||||
|
|
||||||
void bind(@NonNull LifecycleOwner lifecycleOwner, @Nullable ConversationMessage conversationMessage, boolean running) {
|
void bind(@NonNull LifecycleOwner lifecycleOwner, @Nullable ConversationMessage conversationMessage, boolean running) {
|
||||||
|
@ -88,7 +104,20 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder {
|
||||||
conversationItem = (ConversationItem) receivedStub.inflate();
|
conversationItem = (ConversationItem) receivedStub.inflate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
conversationItem.bind(lifecycleOwner, conversationMessage, Optional.absent(), Optional.absent(), glideRequests, Locale.getDefault(), new HashSet<>(), conversationMessage.getMessageRecord().getRecipient(), null, false, false, false);
|
conversationItem.bind(lifecycleOwner,
|
||||||
|
conversationMessage,
|
||||||
|
Optional.absent(),
|
||||||
|
Optional.absent(),
|
||||||
|
glideRequests,
|
||||||
|
Locale.getDefault(),
|
||||||
|
new HashSet<>(),
|
||||||
|
conversationMessage.getMessageRecord().getRecipient(),
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
new AttachmentMediaSourceFactory(conversationItem.getContext()),
|
||||||
|
true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void bindErrorState(MessageRecord messageRecord) {
|
private void bindErrorState(MessageRecord messageRecord) {
|
||||||
|
@ -181,6 +210,39 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder {
|
||||||
((ClipboardManager) itemView.getContext().getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("text", text));
|
((ClipboardManager) itemView.getContext().getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("text", text));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showProjectionArea() {
|
||||||
|
conversationItem.showProjectionArea();
|
||||||
|
maskDrawable.setMask(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hideProjectionArea() {
|
||||||
|
conversationItem.hideProjectionArea();
|
||||||
|
maskDrawable.setMask(conversationItem.getThumbnailMaskingRect((ViewGroup) itemView));
|
||||||
|
maskDrawable.setCorners(conversationItem.getThumbnailCornerMask(itemView).getRadii());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable MediaSource getMediaSource() {
|
||||||
|
return conversationItem.getMediaSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable GiphyMp4PlaybackPolicyEnforcer getPlaybackPolicyEnforcer() {
|
||||||
|
return conversationItem.getPlaybackPolicyEnforcer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull GiphyMp4Projection getProjection(@NonNull RecyclerView recyclerview) {
|
||||||
|
return conversationItem.getProjection(recyclerview);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public
|
||||||
|
boolean canPlayContent() {
|
||||||
|
return conversationItem.canPlayContent();
|
||||||
|
}
|
||||||
|
|
||||||
private class ExpiresUpdater implements Runnable {
|
private class ExpiresUpdater implements Runnable {
|
||||||
|
|
||||||
private final long expireStartedTimestamp;
|
private final long expireStartedTimestamp;
|
||||||
|
|
|
@ -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) {
|
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, false));
|
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_GIF, size, width, height, true, null, caption, null, null, null, false, borderless, true, false));
|
||||||
this.borderless = borderless;
|
this.borderless = borderless;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,4 +30,9 @@ public class GifSlide extends ImageSlide {
|
||||||
public boolean isBorderless() {
|
public boolean isBorderless() {
|
||||||
return borderless;
|
return borderless;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isVideoGif() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
package org.thoughtcrime.securesms.components.voice;
|
package org.thoughtcrime.securesms.video.exo;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
import android.support.v4.media.MediaDescriptionCompat;
|
import android.support.v4.media.MediaDescriptionCompat;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||||
|
@ -11,17 +12,20 @@ import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is responsible for creating a MediaSource object for a given MediaDescriptionCompat
|
* This class is responsible for creating a MediaSource object for a given Uri, using AttachmentDataSourceFactory
|
||||||
*/
|
*/
|
||||||
final class VoiceNoteMediaSourceFactory {
|
public final class AttachmentMediaSourceFactory {
|
||||||
|
|
||||||
private final Context context;
|
private final ExtractorMediaSource.Factory extractorMediaSourceFactory;
|
||||||
|
|
||||||
VoiceNoteMediaSourceFactory(Context context) {
|
public AttachmentMediaSourceFactory(@NonNull Context context) {
|
||||||
this.context = context;
|
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(context, "GenericUserAgent", null);
|
||||||
|
AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(context, defaultDataSourceFactory, null);
|
||||||
|
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);
|
||||||
|
|
||||||
|
extractorMediaSourceFactory = new ExtractorMediaSource.Factory(attachmentDataSourceFactory)
|
||||||
|
.setExtractorsFactory(extractorsFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,13 +35,14 @@ final class VoiceNoteMediaSourceFactory {
|
||||||
*
|
*
|
||||||
* @return A preparable MediaSource
|
* @return A preparable MediaSource
|
||||||
*/
|
*/
|
||||||
public @Nullable MediaSource createMediaSource(MediaDescriptionCompat description) {
|
public @NonNull MediaSource createMediaSource(MediaDescriptionCompat description) {
|
||||||
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(context, "GenericUserAgent", null);
|
return createMediaSource(description.getMediaUri());
|
||||||
AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(context, defaultDataSourceFactory, null);
|
}
|
||||||
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);
|
|
||||||
|
|
||||||
return new ExtractorMediaSource.Factory(attachmentDataSourceFactory)
|
/**
|
||||||
.setExtractorsFactory(extractorsFactory)
|
* Creates a MediaSource for a given Uri
|
||||||
.createMediaSource(description.getMediaUri());
|
*/
|
||||||
|
public @NonNull MediaSource createMediaSource(Uri uri) {
|
||||||
|
return extractorMediaSourceFactory.createMediaSource(uri);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -11,6 +11,15 @@
|
||||||
layout="@layout/conversation_item_banner"
|
layout="@layout/conversation_item_banner"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/video_container"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@android:id/list"
|
||||||
|
app:layout_constraintEnd_toEndOf="@android:id/list"
|
||||||
|
app:layout_constraintStart_toStartOf="@android:id/list"
|
||||||
|
app:layout_constraintTop_toTopOf="@android:id/list" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@android:id/list"
|
android:id="@android:id/list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
|
@ -15,5 +15,6 @@
|
||||||
app:conversationThumbnail_maxWidth="@dimen/media_bubble_max_width"
|
app:conversationThumbnail_maxWidth="@dimen/media_bubble_max_width"
|
||||||
app:conversationThumbnail_minHeight="@dimen/media_bubble_min_height"
|
app:conversationThumbnail_minHeight="@dimen/media_bubble_min_height"
|
||||||
app:conversationThumbnail_maxHeight="@dimen/media_bubble_max_height"
|
app:conversationThumbnail_maxHeight="@dimen/media_bubble_max_height"
|
||||||
|
app:conversationThumbnail_gifWidth="@dimen/media_bubble_gif_width"
|
||||||
tools:src="@drawable/ic_video_light"
|
tools:src="@drawable/ic_video_light"
|
||||||
tools:visibility="gone" />
|
tools:visibility="gone" />
|
||||||
|
|
|
@ -17,5 +17,6 @@
|
||||||
app:conversationThumbnail_maxWidth="@dimen/media_bubble_max_width"
|
app:conversationThumbnail_maxWidth="@dimen/media_bubble_max_width"
|
||||||
app:conversationThumbnail_minHeight="@dimen/media_bubble_min_height"
|
app:conversationThumbnail_minHeight="@dimen/media_bubble_min_height"
|
||||||
app:conversationThumbnail_maxHeight="@dimen/media_bubble_max_height"
|
app:conversationThumbnail_maxHeight="@dimen/media_bubble_max_height"
|
||||||
|
app:conversationThumbnail_gifWidth="@dimen/media_bubble_gif_width"
|
||||||
tools:src="@drawable/ic_video_light"
|
tools:src="@drawable/ic_video_light"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
|
@ -1,8 +1,18 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@drawable/preference_divider">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/video_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/message_details_list"
|
android:id="@+id/message_details_list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@drawable/preference_divider"
|
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||||
|
</FrameLayout>
|
|
@ -14,4 +14,6 @@
|
||||||
|
|
||||||
<dimen name="payment_recovery_phrase_adapter_margin">92dp</dimen>
|
<dimen name="payment_recovery_phrase_adapter_margin">92dp</dimen>
|
||||||
<dimen name="payment_recovery_phrase_outline_margin">48dp</dimen>
|
<dimen name="payment_recovery_phrase_outline_margin">48dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="media_bubble_gif_width">260dp</dimen>
|
||||||
</resources>
|
</resources>
|
|
@ -173,6 +173,7 @@
|
||||||
<attr name="conversationThumbnail_maxWidth" format="dimension" />
|
<attr name="conversationThumbnail_maxWidth" format="dimension" />
|
||||||
<attr name="conversationThumbnail_minHeight" format="dimension" />
|
<attr name="conversationThumbnail_minHeight" format="dimension" />
|
||||||
<attr name="conversationThumbnail_maxHeight" format="dimension" />
|
<attr name="conversationThumbnail_maxHeight" format="dimension" />
|
||||||
|
<attr name="conversationThumbnail_gifWidth" format="dimension" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
|
||||||
<declare-styleable name="TypingIndicatorView">
|
<declare-styleable name="TypingIndicatorView">
|
||||||
|
|
|
@ -47,6 +47,7 @@
|
||||||
<dimen name="media_bubble_min_height">100dp</dimen>
|
<dimen name="media_bubble_min_height">100dp</dimen>
|
||||||
<dimen name="media_bubble_max_height">320dp</dimen>
|
<dimen name="media_bubble_max_height">320dp</dimen>
|
||||||
<dimen name="media_bubble_sticker_dimens">175dp</dimen>
|
<dimen name="media_bubble_sticker_dimens">175dp</dimen>
|
||||||
|
<dimen name="media_bubble_gif_width">240dp</dimen>
|
||||||
<dimen name="message_audio_width">242dp</dimen>
|
<dimen name="message_audio_width">242dp</dimen>
|
||||||
|
|
||||||
<dimen name="media_picker_folder_width">175dp</dimen>
|
<dimen name="media_picker_folder_width">175dp</dimen>
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package org.thoughtcrime.securesms.giph.mp4
|
||||||
|
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class GiphyMp4PlaybackControllerRangeComparatorTest {
|
||||||
|
@Test
|
||||||
|
fun `Given a range of numbers, when I sort with comparator, then I expect an array sorted from the center out`() {
|
||||||
|
val testSubject = createComparator(0, 10)
|
||||||
|
|
||||||
|
val sorted = (0..10).sortedWith(testSubject).toIntArray()
|
||||||
|
val expected = intArrayOf(5, 4, 6, 3, 7, 2, 8, 1, 9, 0, 10)
|
||||||
|
|
||||||
|
Assert.assertArrayEquals(expected, sorted)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createComparator(start: Int, end: Int): GiphyMp4PlaybackController.RangeComparator =
|
||||||
|
GiphyMp4PlaybackController.RangeComparator(start, end)
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
package org.thoughtcrime.securesms.giph.mp4
|
||||||
|
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class GiphyMp4PlaybackPolicyEnforcerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Given a 1s video, when I have a max time of 8s and max repeats of 4, then I expect 4 loops`() {
|
||||||
|
val mediaDuration = TimeUnit.SECONDS.toMillis(1)
|
||||||
|
val maxDuration = TimeUnit.SECONDS.toMillis(8)
|
||||||
|
val maxRepeats = 4L
|
||||||
|
var ended = false
|
||||||
|
val testSubject = GiphyMp4PlaybackPolicyEnforcer({ ended = true }, maxDuration, maxRepeats)
|
||||||
|
|
||||||
|
testSubject.setMediaDuration(mediaDuration)
|
||||||
|
|
||||||
|
Assert.assertTrue((0..2).map { testSubject.endPlayback() }.all { !it })
|
||||||
|
Assert.assertFalse(ended)
|
||||||
|
Assert.assertTrue(testSubject.endPlayback())
|
||||||
|
Assert.assertTrue(ended)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Given a 3s video, when I have a max time of 8s and max repeats of 4, then I expect 2 loops`() {
|
||||||
|
val mediaDuration = TimeUnit.SECONDS.toMillis(3)
|
||||||
|
val maxDuration = TimeUnit.SECONDS.toMillis(8)
|
||||||
|
val maxRepeats = 4L
|
||||||
|
var ended = false
|
||||||
|
val testSubject = GiphyMp4PlaybackPolicyEnforcer({ ended = true }, maxDuration, maxRepeats)
|
||||||
|
|
||||||
|
testSubject.setMediaDuration(mediaDuration)
|
||||||
|
|
||||||
|
Assert.assertFalse(testSubject.endPlayback())
|
||||||
|
Assert.assertFalse(ended)
|
||||||
|
Assert.assertTrue(testSubject.endPlayback())
|
||||||
|
Assert.assertTrue(ended)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Given a 10s video, when I have a max time of 8s and max repeats of 4, then I expect 1 loop`() {
|
||||||
|
val mediaDuration = TimeUnit.SECONDS.toMillis(10)
|
||||||
|
val maxDuration = TimeUnit.SECONDS.toMillis(8)
|
||||||
|
val maxRepeats = 4L
|
||||||
|
var ended = false
|
||||||
|
val testSubject = GiphyMp4PlaybackPolicyEnforcer({ ended = true }, maxDuration, maxRepeats)
|
||||||
|
|
||||||
|
testSubject.setMediaDuration(mediaDuration)
|
||||||
|
|
||||||
|
Assert.assertTrue(testSubject.endPlayback())
|
||||||
|
Assert.assertTrue(ended)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue