Render gifs in gif search as MP4s.

This commit is contained in:
Alex Hart
2021-04-14 16:44:03 -03:00
committed by Greyson Parrelli
parent fcc5db2fe6
commit c31146e902
94 changed files with 2062 additions and 273 deletions

View File

@@ -3,12 +3,15 @@ package org.thoughtcrime.securesms.giph.model;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.fasterxml.jackson.annotation.JsonProperty;
public class GiphyImage {
private static final int MAX_SIZE = 1024 * 1024; // 1MB
@JsonProperty
private ImageTypes images;
@@ -24,6 +27,16 @@ public class GiphyImage {
return data != null ? data.url : null;
}
public String getMp4Url() {
ImageData data = getMp4Data();
return data != null ? data.mp4 : null;
}
public String getMp4PreviewUrl() {
ImageData data = getMp4PreviewData();
return data != null ? data.mp4 : null;
}
public long getGifSize() {
ImageData data = getGifData();
return data != null ? data.size : 0;
@@ -40,7 +53,7 @@ public class GiphyImage {
}
public float getGifAspectRatio() {
return (float)images.downsized.width / (float)images.downsized.height;
return (float)images.downsized_small.width / (float)images.downsized_small.height;
}
public int getGifWidth() {
@@ -63,16 +76,24 @@ public class GiphyImage {
return data != null ? data.size : 0;
}
private @Nullable ImageData getMp4Data() {
return getLargestMp4WithinSizeConstraint(images.fixed_width, images.fixed_height, images.fixed_width_small, images.fixed_height_small, images.downsized_small);
}
private @Nullable ImageData getMp4PreviewData() {
return images.preview;
}
private @Nullable ImageData getGifData() {
return getFirstNonEmpty(images.downsized, images.downsized_medium, images.fixed_height, images.fixed_width);
return getLargestGifWithinSizeConstraint(images.fixed_width, images.fixed_height, images.fixed_width_small, images.fixed_height_small);
}
private @Nullable ImageData getGifMmsData() {
return getFirstNonEmpty(images.fixed_height_downsampled, images.fixed_width_downsampled);
return getLargestGifWithinSizeConstraint(images.fixed_width_small, images.fixed_height_small);
}
private @Nullable ImageData getStillData() {
return getFirstNonEmpty(images.downsized_still, images.fixed_height_still, images.fixed_width_still);
return getFirstNonEmpty(images.fixed_width_small_still, images.fixed_height_small_still);
}
private static @Nullable ImageData getFirstNonEmpty(ImageData... data) {
@@ -85,27 +106,52 @@ public class GiphyImage {
return null;
}
private @Nullable ImageData getLargestGifWithinSizeConstraint(ImageData ... buckets) {
return getLargestWithinSizeConstraint(imageData -> imageData.size, buckets);
}
private @Nullable ImageData getLargestMp4WithinSizeConstraint(ImageData ... buckets) {
return getLargestWithinSizeConstraint(imageData -> imageData.mp4_size, buckets);
}
private @Nullable ImageData getLargestWithinSizeConstraint(@NonNull SizeFunction sizeFunction, ImageData ... buckets) {
ImageData data = null;
int size = 0;
for (final ImageData bucket : buckets) {
if (bucket == null) continue;
int bucketSize = sizeFunction.getSize(bucket);
if (bucketSize <= MAX_SIZE && bucketSize > size) {
data = bucket;
size = bucketSize;
}
}
return data;
}
private interface SizeFunction {
int getSize(@NonNull ImageData imageData);
}
public static class ImageTypes {
@JsonProperty
private ImageData fixed_height;
@JsonProperty
private ImageData fixed_height_still;
private ImageData fixed_height_small;
@JsonProperty
private ImageData fixed_height_downsampled;
private ImageData fixed_height_small_still;
@JsonProperty
private ImageData fixed_width;
@JsonProperty
private ImageData fixed_width_still;
@JsonProperty
private ImageData fixed_width_downsampled;
@JsonProperty
private ImageData fixed_width_small;
@JsonProperty
private ImageData downsized_medium;
private ImageData fixed_width_small_still;
@JsonProperty
private ImageData downsized;
private ImageData downsized_small;
@JsonProperty
private ImageData downsized_still;
private ImageData preview;
}
public static class ImageData {
@@ -126,6 +172,9 @@ public class GiphyImage {
@JsonProperty
private String webp;
@JsonProperty
private int mp4_size;
}
}

View File

@@ -0,0 +1,18 @@
package org.thoughtcrime.securesms.giph.model;
import com.fasterxml.jackson.annotation.JsonProperty;
public class GiphyPagination {
@JsonProperty
private int total_count;
@JsonProperty
private int count;
@JsonProperty
private int offset;
public int getTotalCount() {
return total_count;
}
}

View File

@@ -10,8 +10,14 @@ public class GiphyResponse {
@JsonProperty
private List<GiphyImage> data;
@JsonProperty
private GiphyPagination pagination;
public List<GiphyImage> getData() {
return data;
}
public GiphyPagination getPagination() {
return pagination;
}
}

View File

@@ -0,0 +1,79 @@
package org.thoughtcrime.securesms.giph.mp4;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import org.signal.paging.PagingController;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.giph.model.GiphyImage;
import java.util.Objects;
/**
* Maintains and displays a list of GiphyImage objects. This Adapter always displays gifs
* as MP4 videos.
*/
final class GiphyMp4Adapter extends ListAdapter<GiphyImage, GiphyMp4ViewHolder> {
private final Callback listener;
private final GiphyMp4MediaSourceFactory mediaSourceFactory;
private PagingController pagingController;
public GiphyMp4Adapter(@NonNull GiphyMp4MediaSourceFactory mediaSourceFactory, @Nullable Callback listener) {
super(new GiphyImageDiffUtilCallback());
this.listener = listener;
this.mediaSourceFactory = mediaSourceFactory;
}
@Override
public @NonNull GiphyMp4ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.giphy_mp4, parent, false);
return new GiphyMp4ViewHolder(itemView, listener, mediaSourceFactory);
}
@Override
public void onBindViewHolder(@NonNull GiphyMp4ViewHolder holder, int position) {
holder.onBind(getItem(position));
}
@Override
protected GiphyImage getItem(int position) {
if (pagingController != null) {
pagingController.onDataNeededAroundIndex(position);
}
return super.getItem(position);
}
void setPagingController(@Nullable PagingController pagingController) {
this.pagingController = pagingController;
}
interface Callback {
void onClick(@NonNull GiphyImage giphyImage);
}
private static final class GiphyImageDiffUtilCallback extends DiffUtil.ItemCallback<GiphyImage> {
@Override
public boolean areItemsTheSame(@NonNull GiphyImage oldItem, @NonNull GiphyImage newItem) {
return Objects.equals(oldItem.getMp4Url(), newItem.getMp4Url());
}
@Override
public boolean areContentsTheSame(@NonNull GiphyImage oldItem, @NonNull GiphyImage newItem) {
return areItemsTheSame(oldItem, newItem);
}
}
}

View File

@@ -0,0 +1,133 @@
package org.thoughtcrime.securesms.giph.mp4;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
import java.util.LinkedList;
import java.util.List;
/**
* Controls playback of gifs in a {@link GiphyMp4Adapter}. The maximum number of gifs that will play back at any one
* time is determined by the passed parameter, and the exact gifs that play back is algorithmically determined, starting
* with the center-most gifs.
* <p>
* This algorithm is devised to play back only those gifs which the user is most likely looking at.
*/
final class GiphyMp4AdapterPlaybackController extends RecyclerView.OnScrollListener implements View.OnLayoutChangeListener {
private final int maxSimultaneousPlayback;
private final Callback callback;
private GiphyMp4AdapterPlaybackController(@NonNull Callback callback, int maxSimultaneousPlayback) {
this.maxSimultaneousPlayback = maxSimultaneousPlayback;
this.callback = callback;
}
public static void attach(@NonNull RecyclerView recyclerView, @NonNull Callback callback, int maxSimultaneousPlayback) {
GiphyMp4AdapterPlaybackController controller = new GiphyMp4AdapterPlaybackController(callback, maxSimultaneousPlayback);
recyclerView.addOnScrollListener(controller);
recyclerView.addOnLayoutChangeListener(controller);
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
enqueuePlaybackUpdate(recyclerView);
}
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
RecyclerView recyclerView = (RecyclerView) v;
enqueuePlaybackUpdate(recyclerView);
}
private void enqueuePlaybackUpdate(@NonNull RecyclerView recyclerView) {
performPlaybackUpdate(recyclerView);
}
private void performPlaybackUpdate(@NonNull RecyclerView recyclerView) {
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
int[] firstVisiblePositions = findFirstVisibleItemPositions(layoutManager);
int[] lastVisiblePositions = findLastVisibleItemPositions(layoutManager);
GiphyMp4PlaybackRange playbackRange = getPlaybackRangeForMaximumDistance(firstVisiblePositions, lastVisiblePositions);
if (playbackRange != null) {
List<GiphyMp4ViewHolder> holders = new LinkedList<>();
for (int i = 0; i < recyclerView.getChildCount(); i++) {
GiphyMp4ViewHolder viewHolder = (GiphyMp4ViewHolder) recyclerView.getChildViewHolder(recyclerView.getChildAt(i));
holders.add(viewHolder);
}
callback.update(holders, playbackRange);
}
}
private @Nullable GiphyMp4PlaybackRange getPlaybackRangeForMaximumDistance(int[] firstVisiblePositions, int[] lastVisiblePositions) {
int firstVisiblePosition = Integer.MAX_VALUE;
int lastVisiblePosition = Integer.MIN_VALUE;
for (int i = 0; i < firstVisiblePositions.length; i++) {
firstVisiblePosition = Math.min(firstVisiblePosition, firstVisiblePositions[i]);
lastVisiblePosition = Math.max(lastVisiblePosition, lastVisiblePositions[i]);
}
return getPlaybackRange(firstVisiblePosition, lastVisiblePosition);
}
private @Nullable GiphyMp4PlaybackRange getPlaybackRange(int firstVisiblePosition, int lastVisiblePosition) {
int distance = lastVisiblePosition - firstVisiblePosition;
if (maxSimultaneousPlayback == 0) {
return null;
}
if (distance <= maxSimultaneousPlayback) {
return new GiphyMp4PlaybackRange(firstVisiblePosition, lastVisiblePosition);
} else {
int center = (distance / 2) + firstVisiblePosition;
if (maxSimultaneousPlayback == 1) {
return new GiphyMp4PlaybackRange(center, center);
} else {
int first = Math.max(center - maxSimultaneousPlayback / 2, firstVisiblePosition);
int last = Math.min(first + maxSimultaneousPlayback, lastVisiblePosition);
return new GiphyMp4PlaybackRange(first, last);
}
}
}
private static int[] findFirstVisibleItemPositions(@NonNull RecyclerView.LayoutManager layoutManager) {
if (layoutManager instanceof LinearLayoutManager) {
return new int[]{((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition()};
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
return ((StaggeredGridLayoutManager) layoutManager).findFirstVisibleItemPositions(null);
} else {
throw new IllegalStateException("Unsupported type: " + layoutManager.getClass().getName());
}
}
private static int[] findLastVisibleItemPositions(@NonNull RecyclerView.LayoutManager layoutManager) {
if (layoutManager instanceof LinearLayoutManager) {
return new int[]{((LinearLayoutManager) layoutManager).findLastVisibleItemPosition()};
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
return ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(null);
} else {
throw new IllegalStateException("Unsupported type: " + layoutManager.getClass().getName());
}
}
interface Callback {
void update(@NonNull List<GiphyMp4ViewHolder> holders, @NonNull GiphyMp4PlaybackRange range);
}
}

View File

@@ -0,0 +1,116 @@
package org.thoughtcrime.securesms.giph.mp4;
import android.util.SparseArray;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Logic for updating content and positioning of videos as the user scrolls the list of gifs.
*/
final class GiphyMp4AdapterPlaybackControllerCallback implements GiphyMp4AdapterPlaybackController.Callback {
private final List<GiphyMp4PlayerHolder> holders;
private final SparseArray<GiphyMp4PlayerHolder> playing;
private final SparseArray<GiphyMp4PlayerHolder> notPlaying;
GiphyMp4AdapterPlaybackControllerCallback(@NonNull List<GiphyMp4PlayerHolder> holders) {
this.holders = holders;
this.playing = new SparseArray<>(holders.size());
this.notPlaying = new SparseArray<>(holders.size());
}
@Override public void update(@NonNull List<GiphyMp4ViewHolder> holders,
@NonNull GiphyMp4PlaybackRange range)
{
stopAndReleaseAssignedVideos(range);
for (final GiphyMp4ViewHolder holder : holders) {
if (range.shouldPlayVideo(holder.getAdapterPosition())) {
startPlayback(acquireHolderForPosition(holder.getAdapterPosition()), holder);
} else {
holder.show();
}
}
for (final GiphyMp4ViewHolder holder : holders) {
GiphyMp4PlayerHolder playerHolder = getCurrentHolder(holder.getAdapterPosition());
if (playerHolder != null) {
updateDisplay(playerHolder, holder);
}
}
}
private void stopAndReleaseAssignedVideos(@NonNull GiphyMp4PlaybackRange playbackRange) {
List<Integer> markedForDeletion = new ArrayList<>(playing.size());
for (int i = 0; i < playing.size(); i++) {
if (!playbackRange.shouldPlayVideo(playing.keyAt(i))) {
notPlaying.put(playing.keyAt(i), playing.valueAt(i));
playing.valueAt(i).setMediaSource(null);
playing.valueAt(i).setOnPlaybackReady(null);
markedForDeletion.add(playing.keyAt(i));
}
}
for (final Integer key : markedForDeletion) {
playing.remove(key);
}
}
private void updateDisplay(@NonNull GiphyMp4PlayerHolder holder, @NonNull GiphyMp4ViewHolder giphyMp4ViewHolder) {
holder.getContainer().setX(giphyMp4ViewHolder.itemView.getX());
holder.getContainer().setY(giphyMp4ViewHolder.itemView.getY());
ViewGroup.LayoutParams params = holder.getContainer().getLayoutParams();
if (params.width != giphyMp4ViewHolder.itemView.getWidth() || params.height != giphyMp4ViewHolder.itemView.getHeight()) {
params.width = giphyMp4ViewHolder.itemView.getWidth();
params.height = giphyMp4ViewHolder.itemView.getHeight();
holder.getContainer().setLayoutParams(params);
}
}
private void startPlayback(@NonNull GiphyMp4PlayerHolder holder, @NonNull GiphyMp4ViewHolder giphyMp4ViewHolder) {
if (!Objects.equals(holder.getMediaSource(), giphyMp4ViewHolder.getMediaSource())) {
holder.setOnPlaybackReady(null);
giphyMp4ViewHolder.show();
holder.setOnPlaybackReady(giphyMp4ViewHolder::hide);
holder.setMediaSource(giphyMp4ViewHolder.getMediaSource());
}
}
private @Nullable GiphyMp4PlayerHolder getCurrentHolder(int adapterPosition) {
if (playing.get(adapterPosition) != null) {
return playing.get(adapterPosition);
} else if (notPlaying.get(adapterPosition) != null) {
return notPlaying.get(adapterPosition);
} else {
return null;
}
}
private @NonNull GiphyMp4PlayerHolder acquireHolderForPosition(int adapterPosition) {
GiphyMp4PlayerHolder holder = playing.get(adapterPosition);
if (holder == null) {
if (notPlaying.size() != 0) {
holder = notPlaying.get(adapterPosition);
if (holder == null) {
int key = notPlaying.keyAt(0);
holder = Objects.requireNonNull(notPlaying.get(key));
notPlaying.remove(key);
} else {
notPlaying.remove(adapterPosition);
}
} else {
holder = holders.remove(0);
}
playing.put(adapterPosition, holder);
}
return holder;
}
}

View File

@@ -0,0 +1,46 @@
package org.thoughtcrime.securesms.giph.mp4;
import android.content.Context;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector;
/**
* Provider which creates ExoPlayer instances for displaying Giphy content.
*/
final class GiphyMp4ExoPlayerProvider implements DefaultLifecycleObserver {
private final Context context;
private final TrackSelection.Factory videoTrackSelectionFactory;
private final DefaultRenderersFactory renderersFactory;
private final TrackSelector trackSelector;
private final LoadControl loadControl;
GiphyMp4ExoPlayerProvider(@NonNull Context context) {
this.context = context;
this.videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory();
this.renderersFactory = new DefaultRenderersFactory(context);
this.trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
this.loadControl = new DefaultLoadControl();
}
@MainThread final @NonNull ExoPlayer create() {
ExoPlayer exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector, loadControl);
exoPlayer.setRepeatMode(Player.REPEAT_MODE_ALL);
return exoPlayer;
}
}

View File

@@ -0,0 +1,110 @@
package org.thoughtcrime.securesms.giph.mp4;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Fragment which displays GyphyImages.
*/
public class GiphyMp4Fragment extends Fragment {
private static final String IS_FOR_MMS = "is_for_mms";
public GiphyMp4Fragment() {
super(R.layout.giphy_mp4_fragment);
}
public static Fragment create(boolean isForMms) {
Fragment fragment = new GiphyMp4Fragment();
Bundle bundle = new Bundle();
bundle.putBoolean(IS_FOR_MMS, isForMms);
fragment.setArguments(bundle);
return fragment;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
boolean isForMms = requireArguments().getBoolean(IS_FOR_MMS, false);
FrameLayout frameLayout = view.findViewById(R.id.giphy_parent);
RecyclerView recycler = view.findViewById(R.id.giphy_recycler);
GiphyMp4ViewModel viewModel = ViewModelProviders.of(requireActivity(), new GiphyMp4ViewModel.Factory(isForMms)).get(GiphyMp4ViewModel.class);
GiphyMp4MediaSourceFactory mediaSourceFactory = new GiphyMp4MediaSourceFactory(ApplicationDependencies.getOkHttpClient());
GiphyMp4Adapter adapter = new GiphyMp4Adapter(mediaSourceFactory, viewModel::saveToBlob);
List<GiphyMp4PlayerHolder> holders = injectVideoViews(frameLayout);
GiphyMp4AdapterPlaybackControllerCallback callback = new GiphyMp4AdapterPlaybackControllerCallback(holders);
recycler.setLayoutManager(getLayoutManager(TextSecurePreferences.isGifSearchInGridLayout(getContext())));
recycler.setAdapter(adapter);
recycler.setItemAnimator(null);
GiphyMp4AdapterPlaybackController.attach(recycler, callback, GiphyMp4PlaybackPolicy.maxSimultaneousPlaybackInSearchResults());
viewModel.getImages().observe(getViewLifecycleOwner(), adapter::submitList);
viewModel.getPagingController().observe(getViewLifecycleOwner(), adapter::setPagingController);
viewModel.isGridMode().observe(getViewLifecycleOwner(), isGridLayout -> updateGridLayout(recycler, isGridLayout));
}
private void updateGridLayout(@NonNull RecyclerView recyclerView, boolean isGridLayout) {
RecyclerView.LayoutManager oldLayoutManager = recyclerView.getLayoutManager();
RecyclerView.LayoutManager newLayoutManager = getLayoutManager(isGridLayout);
if (oldLayoutManager == null || !Objects.equals(oldLayoutManager.getClass(), newLayoutManager.getClass())) {
recyclerView.setLayoutManager(newLayoutManager);
}
}
private RecyclerView.LayoutManager getLayoutManager(boolean gridLayout) {
return gridLayout ? new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
: new LinearLayoutManager(requireContext());
}
private List<GiphyMp4PlayerHolder> injectVideoViews(@NonNull ViewGroup viewGroup) {
int nPlayers = GiphyMp4PlaybackPolicy.maxSimultaneousPlaybackInSearchResults();
List<GiphyMp4PlayerHolder> holders = new ArrayList<>(nPlayers);
GiphyMp4ExoPlayerProvider playerProvider = new GiphyMp4ExoPlayerProvider(requireContext());
for (int i = 0; i < nPlayers; i++) {
FrameLayout container = (FrameLayout) LayoutInflater.from(requireContext())
.inflate(R.layout.giphy_mp4_player, viewGroup, false);
GiphyMp4VideoPlayer player = container.findViewById(R.id.video_player);
ExoPlayer exoPlayer = playerProvider.create();
GiphyMp4PlayerHolder holder = new GiphyMp4PlayerHolder(container, player);
getViewLifecycleOwner().getLifecycle().addObserver(player);
player.setExoPlayer(exoPlayer);
player.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FILL);
exoPlayer.addListener(holder);
holders.add(holder);
viewGroup.addView(container);
}
return holders;
}
}

View File

@@ -0,0 +1,76 @@
package org.thoughtcrime.securesms.giph.mp4;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.MediaKeyboardProvider;
import org.thoughtcrime.securesms.mms.GlideRequests;
/**
* MediaKeyboardProvider for MP4 Gifs
*/
@SuppressWarnings("unused")
public final class GiphyMp4MediaKeyboardProvider implements MediaKeyboardProvider {
private final GiphyMp4MediaKeyboardPagerAdapter pagerAdapter;
private Controller controller;
public GiphyMp4MediaKeyboardProvider(@NonNull FragmentActivity fragmentActivity, boolean isForMms) {
pagerAdapter = new GiphyMp4MediaKeyboardPagerAdapter(fragmentActivity.getSupportFragmentManager(), isForMms);
}
@Override
public int getProviderIconView(boolean selected) {
if (selected) {
return R.layout.giphy_mp4_keyboard_icon_selected;
} else {
return R.layout.giphy_mp4_keyboard_icon;
}
}
@Override
public void requestPresentation(@NonNull Presenter presenter, boolean isSoloProvider) {
presenter.present(this, pagerAdapter, new GiphyMp4MediaKeyboardTabIconProvider(), null, null, null, 0);
}
@Override
public void setController(@Nullable Controller controller) {
this.controller = controller;
}
@Override
public void setCurrentPosition(int currentPosition) {
// ignored.
}
private static final class GiphyMp4MediaKeyboardPagerAdapter extends FragmentStatePagerAdapter {
private final boolean isForMms;
public GiphyMp4MediaKeyboardPagerAdapter(@NonNull FragmentManager fm, boolean isForMms) {
super(fm, FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
this.isForMms = isForMms;
}
@Override
public @NonNull Fragment getItem(int position) {
return GiphyMp4Fragment.create(isForMms);
}
@Override public int getCount() {
return 1;
}
}
private static final class GiphyMp4MediaKeyboardTabIconProvider implements TabIconProvider {
@Override public void loadCategoryTabIcon(@NonNull GlideRequests glideRequests, @NonNull ImageView imageView, int index) { }
}
}

View File

@@ -0,0 +1,35 @@
package org.thoughtcrime.securesms.giph.mp4;
import android.net.Uri;
import androidx.annotation.NonNull;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.upstream.DataSource;
import org.thoughtcrime.securesms.video.exo.ChunkedDataSourceFactory;
import okhttp3.OkHttpClient;
/**
* Factory which creates MediaSource objects for given Giphy URIs
*/
final class GiphyMp4MediaSourceFactory {
private final DataSource.Factory dataSourceFactory;
private final ExtractorsFactory extractorsFactory;
private final ExtractorMediaSource.Factory extractorMediaSourceFactory;
GiphyMp4MediaSourceFactory(@NonNull OkHttpClient okHttpClient) {
dataSourceFactory = new ChunkedDataSourceFactory(okHttpClient, null);
extractorsFactory = new DefaultExtractorsFactory();
extractorMediaSourceFactory = new ExtractorMediaSource.Factory(dataSourceFactory).setExtractorsFactory(extractorsFactory);
}
@NonNull MediaSource create(@NonNull Uri uri) {
return extractorMediaSourceFactory.createMediaSource(uri);
}
}

View File

@@ -0,0 +1,86 @@
package org.thoughtcrime.securesms.giph.mp4;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.signal.paging.PagedDataSource;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.giph.model.GiphyImage;
import org.thoughtcrime.securesms.giph.model.GiphyResponse;
import org.thoughtcrime.securesms.util.JsonUtils;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
/**
* Data source for GiphyImages.
*/
final class GiphyMp4PagedDataSource implements PagedDataSource<GiphyImage> {
private static final String TAG = Log.tag(GiphyMp4PagedDataSource.class);
private final String searchString;
private final OkHttpClient client;
GiphyMp4PagedDataSource(@Nullable String searchQuery) {
this.searchString = searchQuery;
this.client = ApplicationDependencies.getOkHttpClient();
}
@Override
public int size() {
try {
GiphyResponse response = performFetch(0, 1);
return response.getPagination().getTotalCount();
} catch (IOException e) {
return 0;
}
}
@Override
public @NonNull List<GiphyImage> load(int start, int length, @NonNull CancellationSignal cancellationSignal) {
try {
Log.d(TAG, "Loading from " + start + " to " + (start + length));
return new LinkedList<>(performFetch(start, length).getData());
} catch (IOException e) {
Log.w(TAG, e);
return new LinkedList<>();
}
}
private @NonNull GiphyResponse performFetch(int start, int length) throws IOException {
String url;
if (TextUtils.isEmpty(searchString)) url = getTrendingUrl(start, length);
else url = getSearchUrl(start, length, Uri.encode(searchString));
Request request = new Request.Builder().url(url).build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response);
}
return JsonUtils.fromJson(response.body().byteStream(), GiphyResponse.class);
}
}
private String getTrendingUrl(int start, int length) {
return "https://api.giphy.com/v1/gifs/trending?api_key=3o6ZsYH6U6Eri53TXy&offset=" + start + "&limit=" + length;
}
private String getSearchUrl(int start, int length, @NonNull String query) {
return "https://api.giphy.com/v1/gifs/search?api_key=3o6ZsYH6U6Eri53TXy&offset=" + start + "&limit=" + length + "&q=" + Uri.encode(query);
}
}

View File

@@ -0,0 +1,57 @@
package org.thoughtcrime.securesms.giph.mp4;
import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil;
import com.google.android.exoplayer2.util.MimeTypes;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.DeviceProperties;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.MediaUtil;
import java.util.concurrent.TimeUnit;
/**
* Central policy object for determining what kind of gifs to display, routing, etc.
*/
public final class GiphyMp4PlaybackPolicy {
private GiphyMp4PlaybackPolicy() { }
public static boolean sendAsMp4() {
return FeatureFlags.mp4GifSendSupport();
}
public static int maxRepeatsOfSinglePlayback() {
return 3;
}
public static long maxDurationOfSinglePlayback() {
return TimeUnit.SECONDS.toMillis(6);
}
public static int maxSimultaneousPlaybackInSearchResults() {
int maxInstances = 0;
try {
MediaCodecInfo info = MediaCodecUtil.getDecoderInfo(MimeTypes.VIDEO_H264, false);
if (info != null) {
maxInstances = (int) (info.getMaxSupportedInstances() * 0.75f);
}
} catch (MediaCodecUtil.DecoderQueryException ignored) {
}
if (maxInstances > 0) {
return maxInstances;
}
if (DeviceProperties.isLowMemoryDevice(ApplicationDependencies.getApplication())) {
return 2;
} else {
return 6;
}
}
}

View File

@@ -0,0 +1,46 @@
package org.thoughtcrime.securesms.giph.mp4;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.Objects;
/**
* Object describing the range of adapter positions for which playback should begin.
*/
final class GiphyMp4PlaybackRange {
private final int startPosition;
private final int endPosition;
GiphyMp4PlaybackRange(int startPosition, int endPosition) {
this.startPosition = startPosition;
this.endPosition = endPosition;
}
boolean shouldPlayVideo(int adapterPosition) {
if (adapterPosition == RecyclerView.NO_POSITION) return false;
return this.startPosition <= adapterPosition && this.endPosition > adapterPosition;
}
@Override
public @NonNull String toString() {
return "PlaybackRange{" +
"startPosition=" + startPosition +
", endPosition=" + endPosition +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final GiphyMp4PlaybackRange that = (GiphyMp4PlaybackRange) o;
return startPosition == that.startPosition &&
endPosition == that.endPosition;
}
@Override public int hashCode() {
return Objects.hash(startPosition, endPosition);
}
}

View File

@@ -0,0 +1,57 @@
package org.thoughtcrime.securesms.giph.mp4;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.MediaSource;
/**
* Object which holds on to an injected video player.
*/
final class GiphyMp4PlayerHolder implements Player.EventListener {
private final FrameLayout container;
private final GiphyMp4VideoPlayer player;
private Runnable onPlaybackReady;
private MediaSource mediaSource;
GiphyMp4PlayerHolder(@NonNull FrameLayout container, @NonNull GiphyMp4VideoPlayer player) {
this.container = container;
this.player = player;
}
@NonNull FrameLayout getContainer() {
return container;
}
public void setMediaSource(@Nullable MediaSource mediaSource) {
this.mediaSource = mediaSource;
if (mediaSource != null) {
player.setVideoSource(mediaSource);
player.play();
} else {
player.stop();
}
}
public @Nullable MediaSource getMediaSource() {
return mediaSource;
}
void setOnPlaybackReady(@Nullable Runnable onPlaybackReady) {
this.onPlaybackReady = onPlaybackReady;
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (playbackState == Player.STATE_READY) {
if (onPlaybackReady != null) {
onPlaybackReady.run();
}
}
}
}

View File

@@ -0,0 +1,82 @@
package org.thoughtcrime.securesms.giph.mp4;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import androidx.core.util.Consumer;
import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.giph.model.GiphyImage;
import org.thoughtcrime.securesms.net.ContentProxySelector;
import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.util.MediaUtil;
import java.io.IOException;
import java.util.concurrent.Executor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
/**
* Repository responsible for downloading gifs selected by the user in the appropriate format.
*/
final class GiphyMp4Repository {
private static final Executor EXECUTOR = SignalExecutors.BOUNDED;
private final OkHttpClient client;
GiphyMp4Repository() {
this.client = new OkHttpClient.Builder().proxySelector(new ContentProxySelector())
.addInterceptor(new StandardUserAgentInterceptor())
.dns(SignalServiceNetworkAccess.DNS)
.build();
}
void saveToBlob(@NonNull GiphyImage giphyImage, boolean isForMms, @NonNull Consumer<GiphyMp4SaveResult> resultConsumer) {
EXECUTOR.execute(() -> {
try {
Uri blob = saveToBlobInternal(giphyImage, isForMms);
resultConsumer.accept(new GiphyMp4SaveResult.Success(blob, giphyImage));
} catch (IOException e) {
resultConsumer.accept(new GiphyMp4SaveResult.Error(e));
}
});
}
@WorkerThread
private @NonNull Uri saveToBlobInternal(@NonNull GiphyImage giphyImage, boolean isForMms) throws IOException {
boolean sendAsMp4 = GiphyMp4PlaybackPolicy.sendAsMp4();
String url;
String mime;
if (sendAsMp4) {
url = giphyImage.getMp4Url();
mime = MediaUtil.VIDEO_MP4;
} else if (isForMms) {
url = giphyImage.getGifMmsUrl();
mime = MediaUtil.IMAGE_GIF;
} else {
url = giphyImage.getGifUrl();
mime = MediaUtil.IMAGE_GIF;
}
Request request = new Request.Builder().url(url).build();
try (Response response = client.newCall(request).execute()) {
if (response.code() >= 200 && response.code() < 300) {
return BlobProvider.getInstance()
.forData(response.body().byteStream(), response.body().contentLength())
.withMimeType(mime)
.createForSingleSessionOnDisk(ApplicationDependencies.getApplication());
} else {
throw new IOException("Unexpected response code: " + response.code());
}
}
}
}

View File

@@ -0,0 +1,56 @@
package org.thoughtcrime.securesms.giph.mp4;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.giph.model.GiphyImage;
/**
* Encapsulates the result of downloading a Giphy MP4 or GIF for
* sending to a user.
*/
public abstract class GiphyMp4SaveResult {
private GiphyMp4SaveResult() {}
public final static class Success extends GiphyMp4SaveResult {
private final Uri blobUri;
private final int width;
private final int height;
private final boolean isBorderless;
Success(@NonNull Uri blobUri, @NonNull GiphyImage giphyImage) {
this.blobUri = blobUri;
this.width = giphyImage.getGifWidth();
this.height = giphyImage.getGifHeight();
this.isBorderless = giphyImage.isSticker();
}
public int getHeight() {
return height;
}
public int getWidth() {
return width;
}
public @NonNull Uri getBlobUri() {
return blobUri;
}
public boolean isBorderless() {
return isBorderless;
}
}
public final static class InProgress extends GiphyMp4SaveResult {
}
public final static class Error extends GiphyMp4SaveResult {
private final Exception exception;
Error(@NonNull Exception exception) {
this.exception = exception;
}
}
}

View File

@@ -0,0 +1,83 @@
package org.thoughtcrime.securesms.giph.mp4;
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.PlayerView;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
/**
* Video Player class specifically created for the GiphyMp4Fragment.
*/
public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLifecycleObserver {
@SuppressWarnings("unused")
private static final String TAG = Log.tag(GiphyMp4VideoPlayer.class);
private final PlayerView exoView;
private ExoPlayer exoPlayer;
public GiphyMp4VideoPlayer(Context context) {
this(context, null);
}
public GiphyMp4VideoPlayer(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public GiphyMp4VideoPlayer(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflate(context, R.layout.gif_player, this);
this.exoView = findViewById(R.id.video_view);
}
@Override
protected void onDetachedFromWindow() {
Log.d(TAG, "onDetachedFromWindow");
super.onDetachedFromWindow();
}
void setExoPlayer(@NonNull ExoPlayer exoPlayer) {
exoView.setPlayer(exoPlayer);
this.exoPlayer = exoPlayer;
}
void setVideoSource(@NonNull MediaSource mediaSource) {
exoPlayer.prepare(mediaSource);
}
void play() {
if (exoPlayer != null) {
exoPlayer.setPlayWhenReady(true);
}
}
void stop() {
if (exoPlayer != null) {
exoPlayer.stop(true);
}
}
void setResizeMode(@AspectRatioFrameLayout.ResizeMode int resizeMode) {
exoView.setResizeMode(resizeMode);
}
@Override public void onDestroy(@NonNull LifecycleOwner owner) {
if (exoPlayer != null) {
exoPlayer.release();
}
}
}

View File

@@ -0,0 +1,85 @@
package org.thoughtcrime.securesms.giph.mp4;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
import org.thoughtcrime.securesms.giph.model.GiphyImage;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.util.Util;
/**
* Holds a view which will either play back an MP4 gif or show its still.
*/
final class GiphyMp4ViewHolder extends RecyclerView.ViewHolder {
private final AspectRatioFrameLayout container;
private final ImageView stillImage;
private final GiphyMp4Adapter.Callback listener;
private final Drawable placeholder;
private final GiphyMp4MediaSourceFactory mediaSourceFactory;
private float aspectRatio;
private MediaSource mediaSource;
GiphyMp4ViewHolder(@NonNull View itemView,
@Nullable GiphyMp4Adapter.Callback listener,
@NonNull GiphyMp4MediaSourceFactory mediaSourceFactory)
{
super(itemView);
this.container = (AspectRatioFrameLayout) itemView;
this.listener = listener;
this.stillImage = itemView.findViewById(R.id.still_image);
this.placeholder = new ColorDrawable(Util.getRandomElement(MaterialColor.values()).toConversationColor(itemView.getContext()));
this.mediaSourceFactory = mediaSourceFactory;
container.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH);
}
void onBind(@NonNull GiphyImage giphyImage) {
aspectRatio = giphyImage.getGifAspectRatio();
mediaSource = mediaSourceFactory.create(Uri.parse(giphyImage.getMp4PreviewUrl()));
container.setAspectRatio(aspectRatio);
container.setBackground(placeholder);
loadPlaceholderImage(giphyImage);
itemView.setOnClickListener(v -> listener.onClick(giphyImage));
}
void show() {
container.setAlpha(1f);
}
void hide() {
container.setAlpha(0f);
}
private void loadPlaceholderImage(@NonNull GiphyImage giphyImage) {
GlideApp.with(itemView)
.load(new ChunkedImageUrl(giphyImage.getStillUrl()))
.placeholder(placeholder)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.transition(DrawableTransitionOptions.withCrossFade())
.into(stillImage);
}
@NonNull MediaSource getMediaSource() {
return mediaSource;
}
}

View File

@@ -0,0 +1,109 @@
package org.thoughtcrime.securesms.giph.mp4;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import com.annimon.stream.Stream;
import org.signal.paging.PagedData;
import org.signal.paging.PagingConfig;
import org.signal.paging.PagingController;
import org.thoughtcrime.securesms.giph.model.GiphyImage;
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import java.util.List;
import java.util.Objects;
/**
* ViewModel which drives GiphyMp4Fragment. This is to be bound to the activity,
* and used as both a data provider and controller.
*/
public final class GiphyMp4ViewModel extends ViewModel {
private final GiphyMp4Repository repository;
private final MutableLiveData<PagedData<GiphyImage>> pagedData;
private final LiveData<List<GiphyImage>> images;
private final LiveData<PagingController> pagingController;
private final SingleLiveEvent<GiphyMp4SaveResult> saveResultEvents;
private final MutableLiveData<Boolean> isGridMode;
private final boolean isForMms;
private String query;
private GiphyMp4ViewModel(boolean isForMms) {
this.isForMms = isForMms;
this.repository = new GiphyMp4Repository();
this.pagedData = new DefaultValueLiveData<>(getGiphyImagePagedData(null));
this.saveResultEvents = new SingleLiveEvent<>();
this.isGridMode = new MutableLiveData<>();
this.pagingController = Transformations.map(pagedData, PagedData::getController);
this.images = Transformations.switchMap(pagedData, pagedData -> Transformations.map(pagedData.getData(),
data -> Stream.of(data)
.filter(g -> g != null)
.filterNot(g -> TextUtils.isEmpty(isForMms ? g.getGifMmsUrl() : g.getGifUrl()))
.filterNot(g -> TextUtils.isEmpty(g.getMp4PreviewUrl()))
.filterNot(g -> TextUtils.isEmpty(g.getStillUrl()))
.toList()));
}
public void updateSearchQuery(@Nullable String query) {
if (!Objects.equals(query, this.query)) {
this.query = query;
pagedData.setValue(getGiphyImagePagedData(query));
}
}
public void updateLayout(boolean isGridMode) {
this.isGridMode.setValue(isGridMode);
}
public void saveToBlob(@NonNull GiphyImage giphyImage) {
saveResultEvents.postValue(new GiphyMp4SaveResult.InProgress());
repository.saveToBlob(giphyImage, isForMms, saveResultEvents::postValue);
}
public @NonNull LiveData<GiphyMp4SaveResult> getSaveResultEvents() {
return saveResultEvents;
}
public @NonNull LiveData<List<GiphyImage>> getImages() {
return images;
}
public @NonNull LiveData<PagingController> getPagingController() {
return pagingController;
}
public @NonNull LiveData<Boolean> isGridMode() {
return isGridMode;
}
private PagedData<GiphyImage> getGiphyImagePagedData(@Nullable String query) {
return PagedData.create(new GiphyMp4PagedDataSource(query),
new PagingConfig.Builder().setPageSize(20)
.setBufferPages(1)
.build());
}
public static class Factory implements ViewModelProvider.Factory {
private final boolean isForMms;
public Factory(boolean isForMms) {
this.isForMms = isForMms;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return Objects.requireNonNull(modelClass.cast(new GiphyMp4ViewModel(isForMms)));
}
}
}

View File

@@ -1,24 +0,0 @@
package org.thoughtcrime.securesms.giph.net;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class GiphyGifLoader extends GiphyLoader {
public GiphyGifLoader(@NonNull Context context, @Nullable String searchString) {
super(context, searchString);
}
@Override
protected String getTrendingUrl() {
return "https://api.giphy.com/v1/gifs/trending?api_key=3o6ZsYH6U6Eri53TXy&offset=%d&limit=" + PAGE_SIZE;
}
@Override
protected String getSearchUrl() {
return "https://api.giphy.com/v1/gifs/search?api_key=3o6ZsYH6U6Eri53TXy&offset=%d&limit=" + PAGE_SIZE + "&q=%s";
}
}

View File

@@ -13,10 +13,12 @@ import android.widget.Toast;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.lifecycle.ViewModelProviders;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
@@ -24,12 +26,16 @@ import com.google.android.material.tabs.TabLayout;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Fragment;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4SaveResult;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ViewModel;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.WindowUtil;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
@@ -51,12 +57,15 @@ public class GiphyActivity extends PassphraseRequiredActivity
private final DynamicTheme dynamicTheme = new DynamicDarkToolbarTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
private GiphyGifFragment gifFragment;
private Fragment gifFragment;
private GiphyStickerFragment stickerFragment;
private boolean forMms;
private GiphyAdapter.GiphyViewHolder finishingImage;
private GiphyMp4ViewModel giphyMp4ViewModel;
private AlertDialog progressDialog;
@Override
public void onPreCreate() {
dynamicTheme.onCreate(this);
@@ -67,6 +76,11 @@ public class GiphyActivity extends PassphraseRequiredActivity
public void onCreate(Bundle bundle, boolean ready) {
setContentView(R.layout.giphy_activity);
forMms = getIntent().getBooleanExtra(EXTRA_IS_MMS, false);
giphyMp4ViewModel = ViewModelProviders.of(this, new GiphyMp4ViewModel.Factory(forMms)).get(GiphyMp4ViewModel.class);
giphyMp4ViewModel.getSaveResultEvents().observe(this, this::handleGiphyMp4SaveResult);
initializeToolbar();
initializeResources();
}
@@ -92,11 +106,9 @@ public class GiphyActivity extends PassphraseRequiredActivity
ViewPager viewPager = findViewById(R.id.giphy_pager);
TabLayout tabLayout = findViewById(R.id.tab_layout);
this.gifFragment = new GiphyGifFragment();
this.gifFragment = GiphyMp4Fragment.create(forMms);
this.stickerFragment = new GiphyStickerFragment();
this.forMms = getIntent().getBooleanExtra(EXTRA_IS_MMS, false);
gifFragment.setClickListener(this);
stickerFragment.setClickListener(this);
viewPager.setAdapter(new GiphyFragmentPagerAdapter(this, getSupportFragmentManager(),
@@ -105,19 +117,52 @@ public class GiphyActivity extends PassphraseRequiredActivity
tabLayout.setBackgroundColor(getConversationColor());
}
private void handleGiphyMp4SaveResult(@NonNull GiphyMp4SaveResult result) {
if (result instanceof GiphyMp4SaveResult.Success) {
hideProgressDialog();
handleGiphyMp4SuccessfulResult((GiphyMp4SaveResult.Success) result);
} else if (result instanceof GiphyMp4SaveResult.Error) {
hideProgressDialog();
handleGiphyMp4ErrorResult((GiphyMp4SaveResult.Error) result);
} else {
progressDialog = SimpleProgressDialog.show(this);
}
}
private void hideProgressDialog() {
if (progressDialog != null) {
progressDialog.dismiss();
}
}
private void handleGiphyMp4SuccessfulResult(@NonNull GiphyMp4SaveResult.Success success) {
Intent intent = new Intent();
intent.setData(success.getBlobUri());
intent.putExtra(EXTRA_WIDTH, success.getWidth());
intent.putExtra(EXTRA_HEIGHT, success.getHeight());
intent.putExtra(EXTRA_BORDERLESS, success.getBlobUri());
setResult(RESULT_OK, intent);
finish();
}
private void handleGiphyMp4ErrorResult(@NonNull GiphyMp4SaveResult.Error error) {
Toast.makeText(this, R.string.GiphyActivity_error_while_retrieving_full_resolution_gif, Toast.LENGTH_LONG).show();
}
private @ColorInt int getConversationColor() {
return getIntent().getIntExtra(EXTRA_COLOR, ActivityCompat.getColor(this, R.color.core_ultramarine));
}
@Override
public void onFilterChanged(String filter) {
this.gifFragment.setSearchString(filter);
giphyMp4ViewModel.updateSearchQuery(filter);
this.stickerFragment.setSearchString(filter);
}
@Override
public void onLayoutChanged(boolean gridLayout) {
gifFragment.setLayoutManager(gridLayout);
giphyMp4ViewModel.updateLayout(gridLayout);
stickerFragment.setLayoutManager(gridLayout);
}
@@ -164,14 +209,14 @@ public class GiphyActivity extends PassphraseRequiredActivity
private static class GiphyFragmentPagerAdapter extends FragmentPagerAdapter {
private final Context context;
private final GiphyGifFragment gifFragment;
private final GiphyStickerFragment stickerFragment;
private final Context context;
private final Fragment gifFragment;
private final Fragment stickerFragment;
private GiphyFragmentPagerAdapter(@NonNull Context context,
@NonNull FragmentManager fragmentManager,
@NonNull GiphyGifFragment gifFragment,
@NonNull GiphyStickerFragment stickerFragment)
@NonNull Fragment gifFragment,
@NonNull Fragment stickerFragment)
{
super(fragmentManager);
this.context = context.getApplicationContext();
@@ -182,7 +227,7 @@ public class GiphyActivity extends PassphraseRequiredActivity
@Override
public Fragment getItem(int position) {
if (position == 0) return gifFragment;
else return stickerFragment;
else return stickerFragment;
}
@Override
@@ -193,7 +238,7 @@ public class GiphyActivity extends PassphraseRequiredActivity
@Override
public CharSequence getPageTitle(int position) {
if (position == 0) return context.getString(R.string.GiphyFragmentPagerAdapter_gifs);
else return context.getString(R.string.GiphyFragmentPagerAdapter_stickers);
else return context.getString(R.string.GiphyFragmentPagerAdapter_stickers);
}
}

View File

@@ -1,21 +0,0 @@
package org.thoughtcrime.securesms.giph.ui;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.loader.content.Loader;
import org.thoughtcrime.securesms.giph.model.GiphyImage;
import org.thoughtcrime.securesms.giph.net.GiphyGifLoader;
import java.util.List;
public class GiphyGifFragment extends GiphyFragment {
@Override
public @NonNull Loader<List<GiphyImage>> onCreateLoader(int id, Bundle args) {
return new GiphyGifLoader(getActivity(), searchString);
}
}