Add support for inline video playback of gifs in Conversation.

This commit is contained in:
Alex Hart
2021-04-20 15:12:35 -03:00
committed by Greyson Parrelli
parent 32d79ead15
commit 281630e751
45 changed files with 1364 additions and 408 deletions

View File

@@ -12,7 +12,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.Lifecycle;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
@@ -20,6 +19,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.List;
@@ -33,6 +33,8 @@ public class ConversationItemThumbnail extends FrameLayout {
private Outliner outliner;
private Outliner pulseOutliner;
private boolean borderless;
private int[] normalBounds;
private int[] gifBounds;
public ConversationItemThumbnail(Context context) {
super(context);
@@ -61,14 +63,28 @@ public class ConversationItemThumbnail extends FrameLayout {
outliner.setColor(ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_20));
int gifWidth = ViewUtil.dpToPx(260);
if (attrs != null) {
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemThumbnail, 0, 0);
thumbnail.setBounds(typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minWidth, 0),
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxWidth, 0),
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minHeight, 0),
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxHeight, 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_minHeight, 0),
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxHeight, 0)
};
gifWidth = typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_gifWidth, gifWidth);
typedArray.recycle();
} else {
normalBounds = new int[]{0, 0, 0, 0};
}
gifBounds = new int[]{
gifWidth,
gifWidth,
1,
Integer.MAX_VALUE
};
}
@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) {
this.pulseOutliner = outliner;
}
@@ -138,6 +166,13 @@ public class ConversationItemThumbnail extends FrameLayout {
boolean showControls, boolean isPreview)
{
if (slides.size() == 1) {
Slide slide = slides.get(0);
if (slide.isVideoGif()) {
setThumbnailBounds(gifBounds);
} else {
setThumbnailBounds(normalBounds);
}
thumbnail.setVisibility(VISIBLE);
album.setVisibility(GONE);
@@ -168,4 +203,8 @@ public class ConversationItemThumbnail extends FrameLayout {
thumbnail.setDownloadClickListener(listener);
album.setDownloadClickListener(listener);
}
private void setThumbnailBounds(@NonNull int[] bounds) {
thumbnail.setBounds(bounds[0], bounds[1], bounds[2], bounds[3]);
}
}

View File

@@ -7,9 +7,11 @@ import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.graphics.drawable.shapes.RoundRectShape;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class CornerMask {
@@ -20,19 +22,24 @@ public class CornerMask {
private final RectF bounds = new RectF();
public CornerMask(@NonNull View view) {
this(view, null);
}
public CornerMask(@NonNull View view, @Nullable CornerMask toClone) {
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
clearPaint.setColor(Color.BLACK);
clearPaint.setStyle(Paint.Style.FILL);
clearPaint.setAntiAlias(true);
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
if (toClone != null) {
System.arraycopy(toClone.radii, 0, radii, 0, radii.length);
}
}
public void mask(Canvas canvas) {
bounds.left = 0;
bounds.top = 0;
bounds.right = canvas.getWidth();
bounds.bottom = canvas.getHeight();
bounds.set(canvas.getClipBounds());
corners.reset();
corners.addRoundRect(bounds, radii, Path.Direction.CW);
@@ -72,4 +79,8 @@ public class CornerMask {
public void setBottomLeftRadius(int radius) {
radii[6] = radii[7] = radius;
}
public float[] getRadii() {
return radii;
}
}

View File

@@ -15,13 +15,17 @@ import android.view.ViewTreeObserver;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class MaskView extends View {
private View target;
private ViewGroup activityContentView;
private Paint maskPaint;
private Rect drawingRect = new Rect();
private float targetParentTranslationY;
private MaskTarget maskTarget;
private ViewGroup activityContentView;
private Paint maskPaint;
private Rect drawingRect = new Rect();
private float targetParentTranslationY;
private final ViewTreeObserver.OnDrawListener onDrawListener = this::invalidate;
@@ -50,15 +54,15 @@ public class MaskView extends View {
activityContentView = getRootView().findViewById(android.R.id.content);
}
public void setTarget(@Nullable View target) {
if (this.target != null) {
this.target.getViewTreeObserver().removeOnDrawListener(onDrawListener);
public void setTarget(@Nullable MaskTarget maskTarget) {
if (this.maskTarget != null) {
removeOnDrawListener(this.maskTarget, onDrawListener);
}
this.target = target;
this.maskTarget = maskTarget;
if (this.target != null) {
this.target.getViewTreeObserver().addOnDrawListener(onDrawListener);
if (this.maskTarget != null) {
addOnDrawListener(maskTarget, onDrawListener);
}
invalidate();
@@ -72,26 +76,77 @@ public class MaskView extends View {
protected void onDraw(@NonNull Canvas canvas) {
super.onDraw(canvas);
if (target == null || !target.isAttachedToWindow()) {
if (nothingToMask(maskTarget)) {
return;
}
target.getDrawingRect(drawingRect);
activityContentView.offsetDescendantRectToMyCoords(target, drawingRect);
maskTarget.getPrimaryTarget().getDrawingRect(drawingRect);
activityContentView.offsetDescendantRectToMyCoords(maskTarget.getPrimaryTarget(), drawingRect);
drawingRect.top += 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);
target.draw(maskCanvas);
maskTarget.draw(maskCanvas);
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);
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);
}
}
}

View File

@@ -1,43 +0,0 @@
package org.thoughtcrime.securesms.components.voice;
import android.content.Context;
import android.support.v4.media.MediaDescriptionCompat;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory;
/**
* This class is responsible for creating a MediaSource object for a given MediaDescriptionCompat
*/
final class VoiceNoteMediaSourceFactory {
private final Context context;
VoiceNoteMediaSourceFactory(Context context) {
this.context = context;
}
/**
* Creates a MediaSource for a given MediaDescriptionCompat
*
* @param description The description to build from
*
* @return A preparable MediaSource
*/
public @Nullable MediaSource createMediaSource(MediaDescriptionCompat description) {
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(context, "GenericUserAgent", null);
AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(context, defaultDataSourceFactory, null);
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);
return new ExtractorMediaSource.Factory(attachmentDataSourceFactory)
.setExtractorsFactory(extractorsFactory)
.createMediaSource(description.getMediaUri());
}
}

View File

@@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
import java.util.Collections;
import java.util.List;
@@ -47,9 +48,9 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
private final Context context;
private final SimpleExoPlayer player;
private final VoiceNoteQueueDataAdapter queueDataAdapter;
private final VoiceNoteMediaSourceFactory mediaSourceFactory;
private final ConcatenatingMediaSource dataSource;
private final VoiceNoteQueueDataAdapter queueDataAdapter;
private final AttachmentMediaSourceFactory mediaSourceFactory;
private final ConcatenatingMediaSource dataSource;
private boolean canLoadMore;
private Uri latestUri = Uri.EMPTY;
@@ -57,7 +58,7 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
VoiceNotePlaybackPreparer(@NonNull Context context,
@NonNull SimpleExoPlayer player,
@NonNull VoiceNoteQueueDataAdapter queueDataAdapter,
@NonNull VoiceNoteMediaSourceFactory mediaSourceFactory)
@NonNull AttachmentMediaSourceFactory mediaSourceFactory)
{
this.context = context;
this.player = player;

View File

@@ -34,6 +34,7 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.ui.PlayerNotificationManager;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
import java.util.Collections;
import java.util.List;
@@ -87,7 +88,7 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
new VoiceNoteNotificationManagerListener(),
queueDataAdapter);
VoiceNoteMediaSourceFactory mediaSourceFactory = new VoiceNoteMediaSourceFactory(this);
AttachmentMediaSourceFactory mediaSourceFactory = new AttachmentMediaSourceFactory(this);
voiceNotePlaybackPreparer = new VoiceNotePlaybackPreparer(this, player, queueDataAdapter, mediaSourceFactory);
voiceNoteProximityManager = new VoiceNoteProximityManager(this, player, queueDataAdapter);