mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-03 06:22:19 +00:00
Add sticker support.
No sticker packs are available for use yet, but we now have the latent ability to send and receive.
This commit is contained in:
18
src/org/thoughtcrime/securesms/stickers/BlessedPacks.java
Normal file
18
src/org/thoughtcrime/securesms/stickers/BlessedPacks.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Maintains a list of "blessed" sticker packs that essentially serve as defaults.
|
||||
*/
|
||||
public final class BlessedPacks {
|
||||
|
||||
private static final Set<String> BLESSED_PACK_IDS = new HashSet<>();
|
||||
|
||||
public static boolean contains(@NonNull String packId) {
|
||||
return BLESSED_PACK_IDS.contains(packId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.Px;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Adapter for a specific page in the sticker keyboard. Shows the stickers in a grid.
|
||||
* @see StickerKeyboardPageFragment
|
||||
*/
|
||||
final class StickerKeyboardPageAdapter extends RecyclerView.Adapter<StickerKeyboardPageAdapter.StickerKeyboardPageViewHolder> {
|
||||
|
||||
private final GlideRequests glideRequests;
|
||||
private final EventListener eventListener;
|
||||
private final List<StickerRecord> stickers;
|
||||
|
||||
private int stickerSize;
|
||||
|
||||
StickerKeyboardPageAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.eventListener = eventListener;
|
||||
this.stickers = new ArrayList<>();
|
||||
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return stickers.get(position).getRowId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
StickerKeyboardPageViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
|
||||
return new StickerKeyboardPageViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.sticker_keyboard_page_list_item, viewGroup, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull StickerKeyboardPageViewHolder viewHolder, int i) {
|
||||
viewHolder.bind(glideRequests, eventListener, stickers.get(i), stickerSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull StickerKeyboardPageViewHolder holder) {
|
||||
holder.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return stickers.size();
|
||||
}
|
||||
|
||||
void setStickers(@NonNull List<StickerRecord> stickers, @Px int stickerSize) {
|
||||
this.stickers.clear();
|
||||
this.stickers.addAll(stickers);
|
||||
|
||||
this.stickerSize = stickerSize;
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
void setStickerSize(@Px int stickerSize) {
|
||||
this.stickerSize = stickerSize;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
static class StickerKeyboardPageViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ImageView image;
|
||||
|
||||
private StickerRecord currentSticker;
|
||||
|
||||
public StickerKeyboardPageViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
image = itemView.findViewById(R.id.sticker_keyboard_page_image);
|
||||
}
|
||||
|
||||
public void bind(@NonNull GlideRequests glideRequests,
|
||||
@Nullable EventListener eventListener,
|
||||
@NonNull StickerRecord sticker,
|
||||
@Px int size)
|
||||
{
|
||||
currentSticker = sticker;
|
||||
|
||||
itemView.getLayoutParams().height = size;
|
||||
itemView.getLayoutParams().width = size;
|
||||
itemView.requestLayout();
|
||||
|
||||
glideRequests.load(new DecryptableUri(sticker.getUri()))
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.into(image);
|
||||
|
||||
if (eventListener != null) {
|
||||
image.setOnClickListener(v -> eventListener.onStickerClicked(sticker));
|
||||
image.setOnLongClickListener(v -> {
|
||||
eventListener.onStickerLongClicked(v);
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
image.setOnClickListener(null);
|
||||
image.setOnLongClickListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
void recycle() {
|
||||
image.setOnClickListener(null);
|
||||
}
|
||||
|
||||
@Nullable StickerRecord getCurrentSticker() {
|
||||
return currentSticker;
|
||||
}
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
void onStickerClicked(@NonNull StickerRecord sticker);
|
||||
void onStickerLongClicked(@NonNull View targetView);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.arch.lifecycle.ViewModelProviders;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Point;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.Px;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.widget.GridLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.stickers.StickerKeyboardPageAdapter.StickerKeyboardPageViewHolder;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
/**
|
||||
* An individual page of stickers in the {@link StickerKeyboardProvider}.
|
||||
*/
|
||||
public final class StickerKeyboardPageFragment extends Fragment implements StickerKeyboardPageAdapter.EventListener {
|
||||
|
||||
private static final String TAG = Log.tag(StickerKeyboardPageFragment.class);
|
||||
|
||||
private static final String KEY_PACK_ID = "pack_id";
|
||||
|
||||
public static final String RECENT_PACK_ID = StickerKeyboardPageViewModel.RECENT_PACK_ID;
|
||||
|
||||
private RecyclerView list;
|
||||
private StickerKeyboardPageAdapter adapter;
|
||||
private GridLayoutManager layoutManager;
|
||||
|
||||
private StickerKeyboardPageViewModel viewModel;
|
||||
private EventListener eventListener;
|
||||
private ListTouchListener listTouchListener;
|
||||
|
||||
private String packId;
|
||||
|
||||
public static StickerKeyboardPageFragment newInstance(@NonNull String packId) {
|
||||
Bundle args = new Bundle();
|
||||
args.putString(KEY_PACK_ID, packId);
|
||||
|
||||
StickerKeyboardPageFragment fragment = new StickerKeyboardPageFragment();
|
||||
fragment.setArguments(args);
|
||||
fragment.packId = packId;
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.sticker_keyboard_page, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
GlideRequests glideRequests = GlideApp.with(this);
|
||||
|
||||
this.list = view.findViewById(R.id.sticker_keyboard_list);
|
||||
this.adapter = new StickerKeyboardPageAdapter(glideRequests, this);
|
||||
this.layoutManager = new GridLayoutManager(requireContext(), 2);
|
||||
this.listTouchListener = new ListTouchListener(requireContext(), glideRequests);
|
||||
this.packId = getArguments().getString(KEY_PACK_ID);
|
||||
|
||||
list.setLayoutManager(layoutManager);
|
||||
list.setAdapter(adapter);
|
||||
list.addOnItemTouchListener(listTouchListener);
|
||||
|
||||
initViewModel(packId);
|
||||
onScreenWidthChanged(getScreenWidth());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
onScreenWidthChanged(getScreenWidth());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStickerClicked(@NonNull StickerRecord sticker) {
|
||||
if (eventListener != null) {
|
||||
eventListener.onStickerSelected(sticker);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStickerLongClicked(@NonNull View targetView) {
|
||||
if (listTouchListener != null) {
|
||||
listTouchListener.enterHoverMode(list, targetView);
|
||||
}
|
||||
}
|
||||
|
||||
public void setEventListener(@NonNull EventListener eventListener) {
|
||||
this.eventListener = eventListener;
|
||||
}
|
||||
|
||||
public @NonNull String getPackId() {
|
||||
return packId;
|
||||
}
|
||||
|
||||
private void initViewModel(@NonNull String packId) {
|
||||
StickerKeyboardRepository repository = new StickerKeyboardRepository(DatabaseFactory.getStickerDatabase(requireContext()));
|
||||
viewModel = ViewModelProviders.of(this, new StickerKeyboardPageViewModel.Factory(requireActivity().getApplication(), repository)).get(StickerKeyboardPageViewModel.class);
|
||||
|
||||
viewModel.getStickers(packId).observe(getViewLifecycleOwner(), stickerRecords -> {
|
||||
if (stickerRecords == null) return;
|
||||
|
||||
adapter.setStickers(stickerRecords, calculateStickerSize(getScreenWidth()));
|
||||
});
|
||||
}
|
||||
|
||||
private void onScreenWidthChanged(@Px int newWidth) {
|
||||
if (layoutManager != null) {
|
||||
layoutManager.setSpanCount(calculateColumnCount(newWidth));
|
||||
adapter.setStickerSize(calculateStickerSize(newWidth));
|
||||
}
|
||||
}
|
||||
|
||||
private int getScreenWidth() {
|
||||
Point size = new Point();
|
||||
requireActivity().getWindowManager().getDefaultDisplay().getSize(size);
|
||||
return size.x;
|
||||
}
|
||||
|
||||
private int calculateColumnCount(@Px int screenWidth) {
|
||||
float modifier = getResources().getDimensionPixelOffset(R.dimen.sticker_page_item_padding);
|
||||
float divisor = getResources().getDimensionPixelOffset(R.dimen.sticker_page_item_divisor);
|
||||
return (int) ((screenWidth - modifier) / divisor);
|
||||
}
|
||||
|
||||
private int calculateStickerSize(@Px int screenWidth) {
|
||||
float multiplier = getResources().getDimensionPixelOffset(R.dimen.sticker_page_item_multiplier);
|
||||
int columnCount = calculateColumnCount(screenWidth);
|
||||
|
||||
return (int) ((screenWidth - ((columnCount + 1) * multiplier)) / columnCount);
|
||||
}
|
||||
|
||||
private final class ListTouchListener implements RecyclerView.OnItemTouchListener {
|
||||
|
||||
private final StickerPreviewPopup popup;
|
||||
|
||||
private boolean hoverMode;
|
||||
|
||||
ListTouchListener(@NonNull Context context, @NonNull GlideRequests glideRequests) {
|
||||
this.popup = new StickerPreviewPopup(context, glideRequests);
|
||||
popup.setAnimationStyle(R.style.StickerPopupAnimation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {
|
||||
return hoverMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {
|
||||
switch (motionEvent.getAction()) {
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
hoverMode = false;
|
||||
popup.dismiss();
|
||||
eventListener.onStickerPopupEnded();
|
||||
break;
|
||||
default:
|
||||
for (int i = 0, len = recyclerView.getChildCount(); i < len; i++) {
|
||||
View child = recyclerView.getChildAt(i);
|
||||
|
||||
if (ViewUtil.isPointInsideView(recyclerView, motionEvent.getRawX(), motionEvent.getRawY()) &&
|
||||
ViewUtil.isPointInsideView(child, motionEvent.getRawX(), motionEvent.getRawY()))
|
||||
{
|
||||
showStickerForView(recyclerView, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestDisallowInterceptTouchEvent(boolean b) {
|
||||
}
|
||||
|
||||
void enterHoverMode(@NonNull RecyclerView recyclerView, View targetView) {
|
||||
this.hoverMode = true;
|
||||
showStickerForView(recyclerView, targetView);
|
||||
}
|
||||
|
||||
private void showStickerForView(@NonNull RecyclerView recyclerView, @NonNull View view) {
|
||||
StickerKeyboardPageViewHolder holder = (StickerKeyboardPageViewHolder) recyclerView.getChildViewHolder(view);
|
||||
|
||||
if (holder != null && holder.getCurrentSticker() != null) {
|
||||
if (!popup.isShowing()) {
|
||||
popup.showAtLocation(recyclerView, Gravity.NO_GRAVITY, 0, 0);
|
||||
eventListener.onStickerPopupStarted();
|
||||
}
|
||||
popup.presentSticker(holder.getCurrentSticker());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
void onStickerSelected(@NonNull StickerRecord sticker);
|
||||
void onStickerPopupStarted();
|
||||
void onStickerPopupEnded();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.app.Application;
|
||||
import android.arch.lifecycle.LiveData;
|
||||
import android.arch.lifecycle.MutableLiveData;
|
||||
import android.arch.lifecycle.ViewModel;
|
||||
import android.arch.lifecycle.ViewModelProvider;
|
||||
import android.database.ContentObserver;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Throttler;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
final class StickerKeyboardPageViewModel extends ViewModel {
|
||||
|
||||
static final String RECENT_PACK_ID = "RECENT";
|
||||
|
||||
private final Application application;
|
||||
private final StickerKeyboardRepository repository;
|
||||
private final MutableLiveData<List<StickerRecord>> stickers;
|
||||
private final Throttler observerThrottler;
|
||||
private final ContentObserver observer;
|
||||
|
||||
private String packId;
|
||||
|
||||
private StickerKeyboardPageViewModel(@NonNull Application application, @NonNull StickerKeyboardRepository repository) {
|
||||
this.application = application;
|
||||
this.repository = repository;
|
||||
this.stickers = new MutableLiveData<>();
|
||||
this.observerThrottler = new Throttler(500);
|
||||
this.observer = new ContentObserver(new Handler()) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
observerThrottler.publish(() -> getStickers(packId));
|
||||
}
|
||||
};
|
||||
|
||||
application.getContentResolver().registerContentObserver(DatabaseContentProviders.Sticker.CONTENT_URI, true, observer);
|
||||
}
|
||||
|
||||
LiveData<List<StickerRecord>> getStickers(@NonNull String packId) {
|
||||
this.packId = packId;
|
||||
|
||||
if (RECENT_PACK_ID.equals(packId)) {
|
||||
repository.getRecentStickers(stickers::postValue);
|
||||
} else {
|
||||
repository.getStickersForPack(packId, stickers::postValue);
|
||||
}
|
||||
|
||||
return stickers;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
application.getContentResolver().unregisterContentObserver(observer);
|
||||
}
|
||||
|
||||
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
private final Application application;
|
||||
private final StickerKeyboardRepository repository;
|
||||
|
||||
Factory(@NonNull Application application, @NonNull StickerKeyboardRepository repository) {
|
||||
this.application = application;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new StickerKeyboardPageViewModel(application, repository));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.arch.lifecycle.ViewModelProviders;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentStatePagerAdapter;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.model.StickerPackRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.stickers.StickerKeyboardPageFragment.EventListener;
|
||||
import org.thoughtcrime.securesms.stickers.StickerKeyboardRepository.PackListResult;
|
||||
import org.thoughtcrime.securesms.util.ResUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Throttler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A provider to select stickers in the {@link org.thoughtcrime.securesms.components.emoji.MediaKeyboard}.
|
||||
*/
|
||||
public final class StickerKeyboardProvider implements MediaKeyboardProvider,
|
||||
MediaKeyboardProvider.AddObserver,
|
||||
StickerKeyboardPageFragment.EventListener
|
||||
{
|
||||
|
||||
private final Context context;
|
||||
private final StickerEventListener eventListener;
|
||||
private final StickerPagerAdapter pagerAdapter;
|
||||
private final Throttler stickerThrottler;
|
||||
|
||||
private Controller controller;
|
||||
private Presenter presenter;
|
||||
private boolean isSoloProvider;
|
||||
private StickerKeyboardViewModel viewModel;
|
||||
|
||||
public StickerKeyboardProvider(@NonNull AppCompatActivity activity,
|
||||
@NonNull StickerEventListener eventListener)
|
||||
{
|
||||
this.context = activity;
|
||||
this.eventListener = eventListener;
|
||||
this.pagerAdapter = new StickerPagerAdapter(activity.getSupportFragmentManager(), this);
|
||||
this.stickerThrottler = new Throttler(100);
|
||||
|
||||
initViewModel(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProviderIconView(boolean selected) {
|
||||
if (selected) {
|
||||
return ThemeUtil.isDarkTheme(context) ? R.layout.sticker_keyboard_icon_dark_selected : R.layout.sticker_keyboard_icon_light_selected;
|
||||
} else {
|
||||
return ThemeUtil.isDarkTheme(context) ? R.layout.sticker_keyboard_icon_dark : R.layout.sticker_keyboard_icon_light;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestPresentation(@NonNull Presenter presenter, boolean isSoloProvider) {
|
||||
this.presenter = presenter;
|
||||
this.isSoloProvider = isSoloProvider;
|
||||
|
||||
PackListResult result = viewModel.getPacks().getValue();
|
||||
|
||||
if (result != null) {
|
||||
present(presenter, result, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setController(@Nullable Controller controller) {
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddClicked() {
|
||||
eventListener.onStickerManagementClicked();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStickerSelected(@NonNull StickerRecord sticker) {
|
||||
stickerThrottler.publish(() -> eventListener.onStickerSelected(sticker));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStickerPopupStarted() {
|
||||
if (controller != null) {
|
||||
controller.setViewPagerEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStickerPopupEnded() {
|
||||
if (controller != null) {
|
||||
controller.setViewPagerEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void initViewModel(@NonNull AppCompatActivity activity) {
|
||||
StickerKeyboardRepository repository = new StickerKeyboardRepository(DatabaseFactory.getStickerDatabase(activity));
|
||||
viewModel = ViewModelProviders.of(activity, new StickerKeyboardViewModel.Factory(activity.getApplication(), repository)).get(StickerKeyboardViewModel.class);
|
||||
|
||||
viewModel.getPacks().observe(activity, result -> {
|
||||
if (result == null) return;
|
||||
|
||||
int previousCount = pagerAdapter.getCount();
|
||||
|
||||
pagerAdapter.setPacks(result.getPacks());
|
||||
|
||||
if (presenter != null) {
|
||||
present(presenter, result, previousCount != pagerAdapter.getCount());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void present(@NonNull Presenter presenter, @NonNull PackListResult result, boolean calculateStartingIndex) {
|
||||
if (result.getPacks().isEmpty() && presenter.isVisible()) {
|
||||
context.startActivity(StickerManagementActivity.getIntent(context));
|
||||
presenter.requestDismissal();
|
||||
return;
|
||||
}
|
||||
|
||||
int startingIndex = presenter.getCurrentPosition();
|
||||
|
||||
if (calculateStartingIndex) {
|
||||
startingIndex = !result.hasRecents() && result.getPacks().size() > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
presenter.present(this, pagerAdapter, new IconProvider(context, result.getPacks()), null, this, null, startingIndex);
|
||||
|
||||
if (isSoloProvider && result.getPacks().isEmpty()) {
|
||||
context.startActivity(StickerManagementActivity.getIntent(context));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
return obj instanceof StickerKeyboardProvider;
|
||||
}
|
||||
|
||||
private static class StickerPagerAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
private final List<StickerPackRecord> packs;
|
||||
private final Map<String, Integer> itemPositions;
|
||||
private final EventListener eventListener;
|
||||
|
||||
public StickerPagerAdapter(@NonNull FragmentManager fm, @NonNull EventListener eventListener) {
|
||||
super(fm);
|
||||
this.eventListener = eventListener;
|
||||
this.packs = new ArrayList<>();
|
||||
this.itemPositions = new HashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemPosition(@NonNull Object object) {
|
||||
String packId = ((StickerKeyboardPageFragment) object).getPackId();
|
||||
|
||||
if (itemPositions.containsKey(packId)) {
|
||||
//noinspection ConstantConditions
|
||||
return itemPositions.get(packId);
|
||||
}
|
||||
|
||||
return POSITION_NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int i) {
|
||||
StickerKeyboardPageFragment fragment;
|
||||
|
||||
if (i == 0) {
|
||||
fragment = StickerKeyboardPageFragment.newInstance(StickerKeyboardPageFragment.RECENT_PACK_ID);
|
||||
} else {
|
||||
StickerPackRecord pack = packs.get(i - 1);
|
||||
fragment = StickerKeyboardPageFragment.newInstance(pack.getPackId());
|
||||
}
|
||||
|
||||
fragment.setEventListener(eventListener);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return packs.isEmpty() ? 0 : packs.size() + 1;
|
||||
}
|
||||
|
||||
void setPacks(@NonNull List<StickerPackRecord> packs) {
|
||||
itemPositions.clear();
|
||||
|
||||
if (areListsEqual(this.packs, packs)) {
|
||||
itemPositions.put(StickerKeyboardPageFragment.RECENT_PACK_ID, 0);
|
||||
for (int i = 0; i < packs.size(); i++) {
|
||||
itemPositions.put(packs.get(i).getPackId(), i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
this.packs.clear();
|
||||
this.packs.addAll(packs);
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
boolean areListsEqual(@NonNull List<StickerPackRecord> a, @NonNull List<StickerPackRecord> b) {
|
||||
if (a.size() != b.size()) return false;
|
||||
|
||||
for (int i = 0; i < a.size(); i++) {
|
||||
if (!a.get(i).equals(b.get(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static class IconProvider implements TabIconProvider {
|
||||
|
||||
private final Context context;
|
||||
private final List<StickerPackRecord> packs;
|
||||
|
||||
private IconProvider(@NonNull Context context, List<StickerPackRecord> packs) {
|
||||
this.context = context;
|
||||
this.packs = packs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadCategoryTabIcon(@NonNull GlideRequests glideRequests, @NonNull ImageView imageView, int index) {
|
||||
if (index == 0) {
|
||||
Drawable icon = ResUtil.getDrawable(context, R.attr.emoji_category_recent);
|
||||
imageView.setImageDrawable(icon);
|
||||
} else {
|
||||
Uri uri = packs.get(index - 1).getCover().getUri();
|
||||
|
||||
glideRequests.load(new DecryptableStreamUriLoader.DecryptableUri(uri))
|
||||
.into(imageView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface StickerEventListener {
|
||||
void onStickerSelected(@NonNull StickerRecord sticker);
|
||||
void onStickerManagementClicked();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.StickerDatabase;
|
||||
import org.thoughtcrime.securesms.database.StickerDatabase.StickerPackRecordReader;
|
||||
import org.thoughtcrime.securesms.database.StickerDatabase.StickerRecordReader;
|
||||
import org.thoughtcrime.securesms.database.model.StickerPackRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
final class StickerKeyboardRepository {
|
||||
|
||||
private static final int RECENT_LIMIT = 24;
|
||||
|
||||
private final StickerDatabase stickerDatabase;
|
||||
|
||||
StickerKeyboardRepository(@NonNull StickerDatabase stickerDatabase) {
|
||||
this.stickerDatabase = stickerDatabase;
|
||||
}
|
||||
|
||||
void getPackList(@NonNull Callback<PackListResult> callback) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
List<StickerPackRecord> packs = new ArrayList<>();
|
||||
|
||||
try (StickerPackRecordReader reader = new StickerPackRecordReader(stickerDatabase.getInstalledStickerPacks())) {
|
||||
StickerPackRecord pack;
|
||||
while ((pack = reader.getNext()) != null) {
|
||||
packs.add(pack);
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasRecents;
|
||||
|
||||
try (Cursor recentsCursor = stickerDatabase.getRecentlyUsedStickers(1)) {
|
||||
hasRecents = recentsCursor != null && recentsCursor.moveToFirst();
|
||||
}
|
||||
|
||||
callback.onComplete(new PackListResult(packs, hasRecents));
|
||||
});
|
||||
}
|
||||
|
||||
void getStickersForPack(@NonNull String packId, @NonNull Callback<List<StickerRecord>> callback) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
List<StickerRecord> stickers = new ArrayList<>();
|
||||
|
||||
try (StickerRecordReader reader = new StickerRecordReader(stickerDatabase.getStickersForPack(packId))) {
|
||||
StickerRecord sticker;
|
||||
while ((sticker = reader.getNext()) != null) {
|
||||
stickers.add(sticker);
|
||||
}
|
||||
}
|
||||
|
||||
callback.onComplete(stickers);
|
||||
});
|
||||
}
|
||||
|
||||
void getRecentStickers(@NonNull Callback<List<StickerRecord>> callback) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
List<StickerRecord> stickers = new ArrayList<>();
|
||||
|
||||
try (StickerRecordReader reader = new StickerRecordReader(stickerDatabase.getRecentlyUsedStickers(RECENT_LIMIT))) {
|
||||
StickerRecord sticker;
|
||||
while ((sticker = reader.getNext()) != null) {
|
||||
stickers.add(sticker);
|
||||
}
|
||||
}
|
||||
|
||||
callback.onComplete(stickers);
|
||||
});
|
||||
}
|
||||
|
||||
static class PackListResult {
|
||||
|
||||
private final List<StickerPackRecord> packs;
|
||||
private final boolean hasRecents;
|
||||
|
||||
PackListResult(List<StickerPackRecord> packs, boolean hasRecents) {
|
||||
this.packs = packs;
|
||||
this.hasRecents = hasRecents;
|
||||
}
|
||||
|
||||
List<StickerPackRecord> getPacks() {
|
||||
return packs;
|
||||
}
|
||||
|
||||
boolean hasRecents() {
|
||||
return hasRecents;
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback<T> {
|
||||
void onComplete(T result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.app.Application;
|
||||
import android.arch.lifecycle.LiveData;
|
||||
import android.arch.lifecycle.MutableLiveData;
|
||||
import android.arch.lifecycle.ViewModel;
|
||||
import android.arch.lifecycle.ViewModelProvider;
|
||||
import android.database.ContentObserver;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
|
||||
import org.thoughtcrime.securesms.stickers.StickerKeyboardRepository.PackListResult;
|
||||
import org.thoughtcrime.securesms.util.Throttler;
|
||||
|
||||
final class StickerKeyboardViewModel extends ViewModel {
|
||||
|
||||
private final Application application;
|
||||
private final MutableLiveData<PackListResult> packs;
|
||||
private final Throttler observerThrottler;
|
||||
private final ContentObserver observer;
|
||||
|
||||
private StickerKeyboardViewModel(@NonNull Application application, @NonNull StickerKeyboardRepository repository) {
|
||||
this.application = application;
|
||||
this.packs = new MutableLiveData<>();
|
||||
this.observerThrottler = new Throttler(500);
|
||||
this.observer = new ContentObserver(new Handler()) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
observerThrottler.publish(() -> repository.getPackList(packs::postValue));
|
||||
}
|
||||
};
|
||||
|
||||
repository.getPackList(packs::postValue);
|
||||
application.getContentResolver().registerContentObserver(DatabaseContentProviders.StickerPack.CONTENT_URI, true, observer);
|
||||
}
|
||||
|
||||
@NonNull LiveData<PackListResult> getPacks() {
|
||||
return packs;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
application.getContentResolver().unregisterContentObserver(observer);
|
||||
}
|
||||
|
||||
public static final class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
private final Application application;
|
||||
private final StickerKeyboardRepository repository;
|
||||
|
||||
public Factory(@NonNull Application application, @NonNull StickerKeyboardRepository repository) {
|
||||
this.application = application;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new StickerKeyboardViewModel(application, repository));
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/org/thoughtcrime/securesms/stickers/StickerLocator.java
Normal file
60
src/org/thoughtcrime/securesms/stickers/StickerLocator.java
Normal file
@@ -0,0 +1,60 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
public class StickerLocator implements Parcelable {
|
||||
|
||||
private final String packId;
|
||||
private final String packKey;
|
||||
private final int stickerId;
|
||||
|
||||
public StickerLocator(@NonNull String packId, @NonNull String packKey, int stickerId) {
|
||||
this.packId = packId;
|
||||
this.packKey = packKey;
|
||||
this.stickerId = stickerId;
|
||||
}
|
||||
|
||||
private StickerLocator(Parcel in) {
|
||||
packId = in.readString();
|
||||
packKey = in.readString();
|
||||
stickerId = in.readInt();
|
||||
}
|
||||
|
||||
public @NonNull String getPackId() {
|
||||
return packId;
|
||||
}
|
||||
|
||||
public @NonNull String getPackKey() {
|
||||
return packKey;
|
||||
}
|
||||
|
||||
public @NonNull int getStickerId() {
|
||||
return stickerId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(packId);
|
||||
dest.writeString(packKey);
|
||||
dest.writeInt(stickerId);
|
||||
}
|
||||
|
||||
public static final Creator<StickerLocator> CREATOR = new Creator<StickerLocator>() {
|
||||
@Override
|
||||
public StickerLocator createFromParcel(Parcel in) {
|
||||
return new StickerLocator(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StickerLocator[] newArray(int size) {
|
||||
return new StickerLocator[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.arch.lifecycle.ViewModelProviders;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.ShareActivity;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
/**
|
||||
* Allows the user to view and manage (install, uninstall, etc) their stickers.
|
||||
*/
|
||||
public final class StickerManagementActivity extends PassphraseRequiredActionBarActivity implements StickerManagementAdapter.EventListener {
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
|
||||
private RecyclerView list;
|
||||
private StickerManagementAdapter adapter;
|
||||
|
||||
private StickerManagementViewModel viewModel;
|
||||
|
||||
public static Intent getIntent(@NonNull Context context) {
|
||||
return new Intent(context, StickerManagementActivity.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
super.onPreCreate();
|
||||
dynamicTheme.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
setContentView(R.layout.sticker_management_activity);
|
||||
|
||||
initView();
|
||||
initToolbar();
|
||||
initViewModel();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
viewModel.onVisible();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStickerPackClicked(@NonNull String packId, @NonNull String packKey) {
|
||||
startActivity(StickerPackPreviewActivity.getIntent(packId, packKey));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStickerPackUninstallClicked(@NonNull String packId, @NonNull String packKey) {
|
||||
viewModel.onStickerPackUninstallClicked(packId, packKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStickerPackInstallClicked(@NonNull String packId, @NonNull String packKey) {
|
||||
viewModel.onStickerPackInstallClicked(packId, packKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStickerPackShareClicked(@NonNull String packId, @NonNull String packKey) {
|
||||
Intent composeIntent = new Intent(this, ShareActivity.class);
|
||||
composeIntent.putExtra(Intent.EXTRA_TEXT, StickerUrl.createShareLink(packId, packKey));
|
||||
startActivity(composeIntent);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
this.list = findViewById(R.id.sticker_management_list);
|
||||
this.adapter = new StickerManagementAdapter(GlideApp.with(this), this);
|
||||
|
||||
list.setLayoutManager(new LinearLayoutManager(this));
|
||||
list.setAdapter(adapter);
|
||||
}
|
||||
|
||||
private void initToolbar() {
|
||||
getSupportActionBar().setTitle(R.string.StickerManagementActivity_stickers);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
private void initViewModel() {
|
||||
StickerManagementRepository repository = new StickerManagementRepository(this);
|
||||
viewModel = ViewModelProviders.of(this, new StickerManagementViewModel.Factory(getApplication(), repository)).get(StickerManagementViewModel.class);
|
||||
|
||||
viewModel.init();
|
||||
viewModel.getStickerPacks().observe(this, packResult -> {
|
||||
if (packResult == null) return;
|
||||
|
||||
adapter.setPackLists(packResult.getInstalledPacks(), packResult.getAvailablePacks(), packResult.getBlessedPacks());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.model.StickerPackRecord;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.StableIdGenerator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
final class StickerManagementAdapter extends RecyclerView.Adapter {
|
||||
|
||||
private static final int TYPE_HEADER = 1;
|
||||
private static final int TYPE_EMPTY = 2;
|
||||
private static final int TYPE_PACK = 3;
|
||||
|
||||
private static final String TAG_YOUR_STICKERS = "YourStickers";
|
||||
private static final String TAG_MESSAGE_STICKERS = "MessageStickers";
|
||||
private static final String TAG_BLESSED_STICKERS = "BlessedStickers";
|
||||
|
||||
private final GlideRequests glideRequests;
|
||||
private final EventListener eventListener;
|
||||
private final StableIdGenerator<String> stableIdGenerator;
|
||||
|
||||
private final List<Section> sections = new ArrayList<Section>(3) {{
|
||||
Section yourStickers = new Section(TAG_YOUR_STICKERS,
|
||||
R.string.StickerManagementAdapter_installed_stickers,
|
||||
R.string.StickerManagementAdapter_no_stickers_installed,
|
||||
new ArrayList<>(),
|
||||
0);
|
||||
Section messageStickers = new Section(TAG_MESSAGE_STICKERS,
|
||||
R.string.StickerManagementAdapter_stickers_you_received,
|
||||
R.string.StickerManagementAdapter_stickers_from_incoming_messages_will_appear_here,
|
||||
new ArrayList<>(),
|
||||
yourStickers.size());
|
||||
|
||||
add(yourStickers);
|
||||
add(messageStickers);
|
||||
}};
|
||||
|
||||
StickerManagementAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.eventListener = eventListener;
|
||||
this.stableIdGenerator = new StableIdGenerator<>();
|
||||
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
for (Section section : sections) {
|
||||
if (section.handles(position)) {
|
||||
return section.getItemId(stableIdGenerator, position);
|
||||
}
|
||||
}
|
||||
throw new NoSectionException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
for (Section section : sections) {
|
||||
if (section.handles(position)) {
|
||||
return section.getViewType(position);
|
||||
}
|
||||
}
|
||||
throw new NoSectionException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
|
||||
switch (viewType) {
|
||||
case TYPE_HEADER:
|
||||
return new HeaderViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.sticker_management_header_item, viewGroup, false));
|
||||
case TYPE_EMPTY:
|
||||
return new EmptyViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.sticker_management_empty_item, viewGroup, false));
|
||||
case TYPE_PACK:
|
||||
return new StickerViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.sticker_management_sticker_item, viewGroup, false));
|
||||
default:
|
||||
throw new AssertionError("Unexpected viewType! " + viewType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||
for (Section section : sections) {
|
||||
if (section.handles(position)) {
|
||||
section.bindViewHolder(viewHolder, position, glideRequests, eventListener);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new NoSectionException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
|
||||
if (holder instanceof StickerViewHolder) {
|
||||
((StickerViewHolder) holder).recycle();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return Stream.of(sections).reduce(0, (sum, section) -> sum + section.size());
|
||||
}
|
||||
|
||||
void setPackLists(@NonNull List<StickerPackRecord> installedPacks,
|
||||
@NonNull List<StickerPackRecord> availablePacks,
|
||||
@NonNull List<StickerPackRecord> blessedPacks)
|
||||
{
|
||||
Section yourStickers = new Section(TAG_YOUR_STICKERS,
|
||||
R.string.StickerManagementAdapter_installed_stickers,
|
||||
R.string.StickerManagementAdapter_no_stickers_installed,
|
||||
installedPacks,
|
||||
0);
|
||||
Section blessedStickers = new Section(TAG_BLESSED_STICKERS,
|
||||
R.string.StickerManagementAdapter_signal_artist_series,
|
||||
0,
|
||||
blessedPacks,
|
||||
yourStickers.size());
|
||||
Section messageStickers = new Section(TAG_MESSAGE_STICKERS,
|
||||
R.string.StickerManagementAdapter_stickers_you_received,
|
||||
R.string.StickerManagementAdapter_stickers_from_incoming_messages_will_appear_here,
|
||||
availablePacks,
|
||||
yourStickers.size() + (blessedPacks.isEmpty() ? 0 : blessedStickers.size()));
|
||||
|
||||
sections.clear();
|
||||
sections.add(yourStickers);
|
||||
|
||||
if (!blessedPacks.isEmpty()) {
|
||||
sections.add(blessedStickers);
|
||||
}
|
||||
|
||||
sections.add(messageStickers);
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private static class Section {
|
||||
private static final String STABLE_ID_HEADER = "header";
|
||||
private static final String STABLE_ID_TEXT = "text";
|
||||
|
||||
private final String tag;
|
||||
private final int titleResId;
|
||||
private final int emptyResId;
|
||||
private final List<StickerPackRecord> records;
|
||||
private final int offset;
|
||||
|
||||
Section(@NonNull String tag,
|
||||
@StringRes int titleResId,
|
||||
@StringRes int emptyResId,
|
||||
@NonNull List<StickerPackRecord> records,
|
||||
int offset)
|
||||
{
|
||||
this.tag = tag;
|
||||
this.titleResId = titleResId;
|
||||
this.emptyResId = emptyResId;
|
||||
this.records = records;
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
int getViewType(int globalPosition) {
|
||||
int localPosition = globalPosition - offset;
|
||||
|
||||
if (localPosition == 0) {
|
||||
return TYPE_HEADER;
|
||||
} else if (records.isEmpty()) {
|
||||
return TYPE_EMPTY;
|
||||
} else {
|
||||
return TYPE_PACK;
|
||||
}
|
||||
}
|
||||
|
||||
long getItemId(@NonNull StableIdGenerator<String> idGenerator, int globalPosition) {
|
||||
int localPosition = globalPosition - offset;
|
||||
|
||||
if (localPosition == 0) {
|
||||
return idGenerator.getId(tag + "_" + STABLE_ID_HEADER);
|
||||
} else if (records.isEmpty()) {
|
||||
return idGenerator.getId(tag + "_" + STABLE_ID_TEXT);
|
||||
} else {
|
||||
return idGenerator.getId(records.get(localPosition - 1).getPackId());
|
||||
}
|
||||
}
|
||||
|
||||
void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
int globalPosition,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull EventListener eventListener)
|
||||
{
|
||||
int localPosition = globalPosition - offset;
|
||||
|
||||
if (localPosition == 0) {
|
||||
((HeaderViewHolder) viewHolder).bind(titleResId);
|
||||
} else if (records.isEmpty()) {
|
||||
((EmptyViewHolder) viewHolder).bind(emptyResId);
|
||||
} else {
|
||||
((StickerViewHolder) viewHolder).bind(glideRequests, eventListener, records.get(localPosition - 1), localPosition == records.size());
|
||||
}
|
||||
}
|
||||
|
||||
boolean handles(int globalPosition) {
|
||||
int localPosition = globalPosition - offset;
|
||||
return localPosition >= 0 && localPosition < size();
|
||||
}
|
||||
|
||||
int size() {
|
||||
return records.isEmpty() ? 2 : records.size() + 1;
|
||||
}
|
||||
}
|
||||
|
||||
static class StickerViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ImageView cover;
|
||||
private final TextView title;
|
||||
private final TextView author;
|
||||
private final View badge;
|
||||
private final View divider;
|
||||
private final View actionButton;
|
||||
private final ImageView actionButtonImage;
|
||||
private final View shareButton;
|
||||
private final ImageView shareButtonImage;
|
||||
|
||||
StickerViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
this.cover = itemView.findViewById(R.id.sticker_management_cover);
|
||||
this.title = itemView.findViewById(R.id.sticker_management_title);
|
||||
this.author = itemView.findViewById(R.id.sticker_management_author);
|
||||
this.badge = itemView.findViewById(R.id.sticker_management_blessed_badge);
|
||||
this.divider = itemView.findViewById(R.id.sticker_management_divider);
|
||||
this.actionButton = itemView.findViewById(R.id.sticker_management_action_button);
|
||||
this.actionButtonImage = itemView.findViewById(R.id.sticker_management_action_button_image);
|
||||
this.shareButton = itemView.findViewById(R.id.sticker_management_share_button);
|
||||
this.shareButtonImage = itemView.findViewById(R.id.sticker_management_share_button_image);
|
||||
}
|
||||
|
||||
void bind(@NonNull GlideRequests glideRequests,
|
||||
@NonNull EventListener eventListener,
|
||||
@NonNull StickerPackRecord stickerPack,
|
||||
boolean lastInList)
|
||||
{
|
||||
title.setText(stickerPack.getTitle().or(itemView.getResources().getString(R.string.StickerManagementAdapter_untitled)));
|
||||
author.setText(stickerPack.getAuthor().or(itemView.getResources().getString(R.string.StickerManagementAdapter_unknown)));
|
||||
divider.setVisibility(lastInList ? View.GONE : View.VISIBLE);
|
||||
badge.setVisibility(BlessedPacks.contains(stickerPack.getPackId()) ? View.VISIBLE : View.GONE);
|
||||
|
||||
glideRequests.load(new DecryptableUri(stickerPack.getCover().getUri()))
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.into(cover);
|
||||
|
||||
if (stickerPack.isInstalled()) {
|
||||
actionButtonImage.setImageResource(R.drawable.ic_x);
|
||||
actionButton.setOnClickListener(v -> eventListener.onStickerPackUninstallClicked(stickerPack.getPackId(), stickerPack.getPackKey()));
|
||||
|
||||
shareButton.setVisibility(View.VISIBLE);
|
||||
shareButtonImage.setVisibility(View.VISIBLE);
|
||||
shareButton.setOnClickListener(v -> eventListener.onStickerPackShareClicked(stickerPack.getPackId(), stickerPack.getPackKey()));
|
||||
} else {
|
||||
actionButtonImage.setImageResource(R.drawable.ic_arrow_down);
|
||||
actionButton.setOnClickListener(v -> eventListener.onStickerPackInstallClicked(stickerPack.getPackId(), stickerPack.getPackKey()));
|
||||
|
||||
shareButton.setVisibility(View.GONE);
|
||||
shareButtonImage.setVisibility(View.GONE);
|
||||
shareButton.setOnClickListener(null);
|
||||
}
|
||||
|
||||
itemView.setOnClickListener(v -> eventListener.onStickerPackClicked(stickerPack.getPackId(), stickerPack.getPackKey()));
|
||||
}
|
||||
|
||||
void recycle() {
|
||||
actionButton.setOnClickListener(null);
|
||||
shareButton.setOnClickListener(null);
|
||||
itemView.setOnClickListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
static class HeaderViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final TextView titleView;
|
||||
|
||||
HeaderViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
this.titleView = itemView.findViewById(R.id.sticker_management_header);
|
||||
}
|
||||
|
||||
void bind(@StringRes int title) {
|
||||
titleView.setText(title);
|
||||
}
|
||||
}
|
||||
|
||||
static class EmptyViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final TextView text;
|
||||
|
||||
EmptyViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
this.text = itemView.findViewById(R.id.sticker_management_empty_text);
|
||||
}
|
||||
|
||||
void bind(@StringRes int title) {
|
||||
text.setText(title);
|
||||
}
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
void onStickerPackClicked(@NonNull String packId, @NonNull String packKey);
|
||||
void onStickerPackUninstallClicked(@NonNull String packId, @NonNull String packKey);
|
||||
void onStickerPackInstallClicked(@NonNull String packId, @NonNull String packKey);
|
||||
void onStickerPackShareClicked(@NonNull String packId, @NonNull String packKey);
|
||||
}
|
||||
|
||||
private static class NoSectionException extends IllegalStateException {}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.StickerDatabase;
|
||||
import org.thoughtcrime.securesms.database.StickerDatabase.StickerPackRecordReader;
|
||||
import org.thoughtcrime.securesms.database.model.StickerPackRecord;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceStickerPackOperationJob;
|
||||
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
final class StickerManagementRepository {
|
||||
|
||||
private final Context context;
|
||||
private final StickerDatabase stickerDatabase;
|
||||
private final AttachmentDatabase attachmentDatabase;
|
||||
|
||||
StickerManagementRepository(@NonNull Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.stickerDatabase = DatabaseFactory.getStickerDatabase(context);
|
||||
this.attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
}
|
||||
|
||||
void deleteOrphanedStickerPacks() {
|
||||
SignalExecutors.SERIAL.execute(stickerDatabase::deleteOrphanedPacks);
|
||||
}
|
||||
|
||||
void fetchUnretrievedReferencePacks() {
|
||||
SignalExecutors.SERIAL.execute(() -> {
|
||||
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
|
||||
|
||||
try (Cursor cursor = attachmentDatabase.getUnavailableStickerPacks()) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String packId = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.STICKER_PACK_ID));
|
||||
String packKey = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.STICKER_PACK_KEY));
|
||||
|
||||
jobManager.add(new StickerPackDownloadJob(packId, packKey, true));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void getStickerPacks(@NonNull Callback<PackResult> callback) {
|
||||
SignalExecutors.SERIAL.execute(() -> {
|
||||
List<StickerPackRecord> installedPacks = new ArrayList<>();
|
||||
List<StickerPackRecord> availablePacks = new ArrayList<>();
|
||||
List<StickerPackRecord> blessedPacks = new ArrayList<>();
|
||||
|
||||
try (StickerPackRecordReader reader = new StickerPackRecordReader(stickerDatabase.getAllStickerPacks())) {
|
||||
StickerPackRecord record;
|
||||
while ((record = reader.getNext()) != null) {
|
||||
if (record.isInstalled()) {
|
||||
installedPacks.add(record);
|
||||
} else if (BlessedPacks.contains(record.getPackId())) {
|
||||
blessedPacks.add(record);
|
||||
} else {
|
||||
availablePacks.add(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback.onComplete(new PackResult(installedPacks, availablePacks, blessedPacks));
|
||||
});
|
||||
}
|
||||
|
||||
void uninstallStickerPack(@NonNull String packId, @NonNull String packKey) {
|
||||
SignalExecutors.SERIAL.execute(() -> {
|
||||
stickerDatabase.uninstallPack(packId);
|
||||
|
||||
if (TextSecurePreferences.isMultiDevice(context)) {
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new MultiDeviceStickerPackOperationJob(packId, packKey, MultiDeviceStickerPackOperationJob.Type.REMOVE));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void installStickerPack(@NonNull String packId, @NonNull String packKey) {
|
||||
SignalExecutors.SERIAL.execute(() -> {
|
||||
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
|
||||
|
||||
if (stickerDatabase.isPackAvailableAsReference(packId)) {
|
||||
stickerDatabase.markPackAsInstalled(packId);
|
||||
}
|
||||
|
||||
jobManager.add(new StickerPackDownloadJob(packId, packKey, false));
|
||||
|
||||
if (TextSecurePreferences.isMultiDevice(context)) {
|
||||
jobManager.add(new MultiDeviceStickerPackOperationJob(packId, packKey, MultiDeviceStickerPackOperationJob.Type.INSTALL));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static class PackResult {
|
||||
|
||||
private final List<StickerPackRecord> installedPacks;
|
||||
private final List<StickerPackRecord> availablePacks;
|
||||
private final List<StickerPackRecord> blessedPacks;
|
||||
|
||||
PackResult(@NonNull List<StickerPackRecord> installedPacks,
|
||||
@NonNull List<StickerPackRecord> availablePacks,
|
||||
@NonNull List<StickerPackRecord> blessedPacks)
|
||||
{
|
||||
this.installedPacks = installedPacks;
|
||||
this.availablePacks = availablePacks;
|
||||
this.blessedPacks = blessedPacks;
|
||||
}
|
||||
|
||||
@NonNull List<StickerPackRecord> getInstalledPacks() {
|
||||
return installedPacks;
|
||||
}
|
||||
|
||||
@NonNull List<StickerPackRecord> getAvailablePacks() {
|
||||
return availablePacks;
|
||||
}
|
||||
|
||||
@NonNull List<StickerPackRecord> getBlessedPacks() {
|
||||
return blessedPacks;
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback<T> {
|
||||
void onComplete(T result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.app.Application;
|
||||
import android.arch.lifecycle.LiveData;
|
||||
import android.arch.lifecycle.MutableLiveData;
|
||||
import android.arch.lifecycle.ViewModel;
|
||||
import android.arch.lifecycle.ViewModelProvider;
|
||||
import android.database.ContentObserver;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
|
||||
import org.thoughtcrime.securesms.stickers.StickerManagementRepository.PackResult;
|
||||
|
||||
final class StickerManagementViewModel extends ViewModel {
|
||||
|
||||
private final Application application;
|
||||
private final StickerManagementRepository repository;
|
||||
private final MutableLiveData<PackResult> packs;
|
||||
private final ContentObserver observer;
|
||||
|
||||
private StickerManagementViewModel(@NonNull Application application, @NonNull StickerManagementRepository repository) {
|
||||
this.application = application;
|
||||
this.repository = repository;
|
||||
this.packs = new MutableLiveData<>();
|
||||
this.observer = new ContentObserver(new Handler()) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
repository.deleteOrphanedStickerPacks();
|
||||
repository.getStickerPacks(packs::postValue);
|
||||
}
|
||||
};
|
||||
|
||||
application.getContentResolver().registerContentObserver(DatabaseContentProviders.StickerPack.CONTENT_URI, true, observer);
|
||||
}
|
||||
|
||||
void init() {
|
||||
repository.deleteOrphanedStickerPacks();
|
||||
repository.fetchUnretrievedReferencePacks();
|
||||
}
|
||||
|
||||
void onVisible() {
|
||||
repository.deleteOrphanedStickerPacks();
|
||||
}
|
||||
|
||||
@NonNull LiveData<PackResult> getStickerPacks() {
|
||||
repository.getStickerPacks(packs::postValue);
|
||||
return packs;
|
||||
}
|
||||
|
||||
void onStickerPackUninstallClicked(@NonNull String packId, @NonNull String packKey) {
|
||||
repository.uninstallStickerPack(packId, packKey);
|
||||
}
|
||||
|
||||
void onStickerPackInstallClicked(@NonNull String packId, @NonNull String packKey) {
|
||||
repository.installStickerPack(packId, packKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
application.getContentResolver().unregisterContentObserver(observer);
|
||||
}
|
||||
|
||||
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
|
||||
private final Application application;
|
||||
private final StickerManagementRepository repository;
|
||||
|
||||
Factory(@NonNull Application application, @NonNull StickerManagementRepository repository) {
|
||||
this.application = application;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new StickerManagementViewModel(application, repository));
|
||||
}
|
||||
}
|
||||
}
|
||||
103
src/org/thoughtcrime/securesms/stickers/StickerManifest.java
Normal file
103
src/org/thoughtcrime/securesms/stickers/StickerManifest.java
Normal file
@@ -0,0 +1,103 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Local model that represents the data present in the libsignal model
|
||||
* {@link org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest}.
|
||||
*/
|
||||
public final class StickerManifest {
|
||||
|
||||
private final String packId;
|
||||
private final String packKey;
|
||||
private final Optional<String> title;
|
||||
private final Optional<String> author;
|
||||
private final Optional<Sticker> cover;
|
||||
private final List<Sticker> stickers;
|
||||
|
||||
public StickerManifest(@NonNull String packId,
|
||||
@NonNull String packKey,
|
||||
@NonNull Optional<String> title,
|
||||
@NonNull Optional<String> author,
|
||||
@NonNull Optional<Sticker> cover,
|
||||
@NonNull List<Sticker> stickers)
|
||||
{
|
||||
this.packId = packId;
|
||||
this.packKey = packKey;
|
||||
this.title = title;
|
||||
this.author = author;
|
||||
this.cover = cover;
|
||||
this.stickers = new ArrayList<>(stickers);
|
||||
}
|
||||
|
||||
public @NonNull String getPackId() {
|
||||
return packId;
|
||||
}
|
||||
|
||||
public @NonNull String getPackKey() {
|
||||
return packKey;
|
||||
}
|
||||
|
||||
public @NonNull Optional<String> getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public @NonNull Optional<String> getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public @NonNull Optional<Sticker> getCover() {
|
||||
return cover;
|
||||
}
|
||||
|
||||
public @NonNull List<Sticker> getStickers() {
|
||||
return stickers;
|
||||
}
|
||||
|
||||
public static class Sticker {
|
||||
private final String packId;
|
||||
private final String packKey;
|
||||
private final int id;
|
||||
private final String emoji;
|
||||
private final Optional<Uri> uri;
|
||||
|
||||
public Sticker(@NonNull String packId, @NonNull String packKey, int id, @NonNull String emoji) {
|
||||
this(packId, packKey, id, emoji, null);
|
||||
}
|
||||
|
||||
public Sticker(@NonNull String packId, @NonNull String packKey, int id, @NonNull String emoji, @Nullable Uri uri) {
|
||||
this.packId = packId;
|
||||
this.packKey = packKey;
|
||||
this.id = id;
|
||||
this.emoji = emoji;
|
||||
this.uri = Optional.fromNullable(uri);
|
||||
}
|
||||
|
||||
public @NonNull String getPackId() {
|
||||
return packId;
|
||||
}
|
||||
|
||||
public @NonNull String getPackKey() {
|
||||
return packKey;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getEmoji() {
|
||||
return emoji;
|
||||
}
|
||||
|
||||
public Optional<Uri> getUri() {
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
public class StickerPackInstallEvent {
|
||||
private final Object iconGlideModel;
|
||||
|
||||
public StickerPackInstallEvent(@NonNull Object iconGlideModel) {
|
||||
this.iconGlideModel = iconGlideModel;
|
||||
}
|
||||
|
||||
public @NonNull Object getIconGlideModel() {
|
||||
return iconGlideModel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.arch.lifecycle.ViewModelProviders;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Point;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.widget.GridLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.ShareActivity;
|
||||
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.stickers.StickerManifest.Sticker;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
/**
|
||||
* Shows the contents of a pack and allows the user to install it (if not installed) or remove it
|
||||
* (if installed). This is also the handler for sticker pack deep links.
|
||||
*/
|
||||
public final class StickerPackPreviewActivity extends PassphraseRequiredActionBarActivity {
|
||||
|
||||
private static final String TAG = Log.tag(StickerPackPreviewActivity.class);
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
|
||||
private StickerPackPreviewViewModel viewModel;
|
||||
|
||||
private ImageView coverImage;
|
||||
private TextView stickerTitle;
|
||||
private TextView stickerAuthor;
|
||||
private View installButton;
|
||||
private View removeButton;
|
||||
private RecyclerView stickerList;
|
||||
private View shareButton;
|
||||
private View shareButtonImage;
|
||||
|
||||
private StickerPackPreviewAdapter adapter;
|
||||
private GridLayoutManager layoutManager;
|
||||
|
||||
public static Intent getIntent(@NonNull String packId, @NonNull String packKey) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, StickerUrl.createActionUri(packId, packKey));
|
||||
intent.addCategory(Intent.CATEGORY_DEFAULT);
|
||||
intent.addCategory(Intent.CATEGORY_BROWSABLE);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
super.onPreCreate();
|
||||
dynamicTheme.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
setContentView(R.layout.sticker_preview_activity);
|
||||
|
||||
Optional<Pair<String, String>> stickerParams = StickerUrl.parseActionUri(getIntent().getData());
|
||||
|
||||
if (!stickerParams.isPresent()) {
|
||||
Log.w(TAG, "Invalid URI!");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
String packId = stickerParams.get().first();
|
||||
String packKey = stickerParams.get().second();
|
||||
|
||||
initToolbar();
|
||||
initView();
|
||||
initViewModel(packId, packKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
onScreenWidthChanged(getScreenWidth());
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
this.coverImage = findViewById(R.id.sticker_install_cover);
|
||||
this.stickerTitle = findViewById(R.id.sticker_install_title);
|
||||
this.stickerAuthor = findViewById(R.id.sticker_install_author);
|
||||
this.installButton = findViewById(R.id.sticker_install_button);
|
||||
this.removeButton = findViewById(R.id.sticker_install_remove_button);
|
||||
this.stickerList = findViewById(R.id.sticker_install_list);
|
||||
this.shareButton = findViewById(R.id.sticker_install_share_button);
|
||||
this.shareButtonImage = findViewById(R.id.sticker_install_share_button_image);
|
||||
|
||||
this.adapter = new StickerPackPreviewAdapter(GlideApp.with(this));
|
||||
this.layoutManager = new GridLayoutManager(this, 2);
|
||||
onScreenWidthChanged(getScreenWidth());
|
||||
|
||||
stickerList.setLayoutManager(layoutManager);
|
||||
stickerList.setAdapter(adapter);
|
||||
}
|
||||
|
||||
private void initToolbar() {
|
||||
Toolbar toolbar = findViewById(R.id.sticker_install_toolbar);
|
||||
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setTitle(R.string.StickerPackPreviewActivity_stickers);
|
||||
|
||||
toolbar.setNavigationOnClickListener(v -> onBackPressed());
|
||||
|
||||
if (!ThemeUtil.isDarkTheme(this) && Build.VERSION.SDK_INT >= 23) {
|
||||
setStatusBarColor(ThemeUtil.getThemedColor(this, R.attr.sticker_preview_status_bar_color));
|
||||
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
|
||||
}
|
||||
}
|
||||
|
||||
private void initViewModel(@NonNull String packId, @NonNull String packKey) {
|
||||
viewModel = ViewModelProviders.of(this, new StickerPackPreviewViewModel.Factory(getApplication(),
|
||||
new StickerPackPreviewRepository(this),
|
||||
new StickerManagementRepository(this)))
|
||||
.get(StickerPackPreviewViewModel.class);
|
||||
|
||||
viewModel.getStickerManifest(packId, packKey).observe(this, manifest -> {
|
||||
if (manifest == null) return;
|
||||
|
||||
if (manifest.isPresent()) {
|
||||
presentManifest(manifest.get().getManifest());
|
||||
presentButton(manifest.get().isInstalled());
|
||||
presentShareButton(manifest.get().isInstalled(), manifest.get().getManifest().getPackId(), manifest.get().getManifest().getPackKey());
|
||||
} else {
|
||||
presentError();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void presentManifest(@NonNull StickerManifest manifest) {
|
||||
stickerTitle.setText(manifest.getTitle().or(getString(R.string.StickerPackPreviewActivity_untitled)));
|
||||
stickerAuthor.setText(manifest.getAuthor().or(getString(R.string.StickerPackPreviewActivity_unknown)));
|
||||
adapter.setStickers(manifest.getStickers());
|
||||
|
||||
installButton.setOnClickListener(v -> {
|
||||
SimpleTask.run(() -> {
|
||||
ApplicationContext.getInstance(this)
|
||||
.getJobManager()
|
||||
.add(new StickerPackDownloadJob(manifest.getPackId(), manifest.getPackKey(), false));
|
||||
|
||||
return null;
|
||||
}, (nothing) -> finish());
|
||||
});
|
||||
|
||||
Sticker first = manifest.getStickers().isEmpty() ? null : manifest.getStickers().get(0);
|
||||
Sticker cover = manifest.getCover().or(Optional.fromNullable(first)).orNull();
|
||||
|
||||
if (cover != null) {
|
||||
Object model = cover.getUri().isPresent() ? new DecryptableStreamUriLoader.DecryptableUri(cover.getUri().get())
|
||||
: new StickerRemoteUri(cover.getPackId(), cover.getPackKey(), cover.getId());
|
||||
GlideApp.with(this).load(model)
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.into(coverImage);
|
||||
} else {
|
||||
coverImage.setImageDrawable(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void presentButton(boolean installed) {
|
||||
if (installed) {
|
||||
removeButton.setVisibility(View.VISIBLE);
|
||||
removeButton.setOnClickListener(v -> {
|
||||
viewModel.onRemoveClicked();
|
||||
finish();
|
||||
});
|
||||
installButton.setVisibility(View.GONE);
|
||||
installButton.setOnClickListener(null);
|
||||
} else {
|
||||
installButton.setVisibility(View.VISIBLE);
|
||||
installButton.setOnClickListener(v -> {
|
||||
viewModel.onInstallClicked();
|
||||
finish();
|
||||
});
|
||||
removeButton.setVisibility(View.GONE);
|
||||
removeButton.setOnClickListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void presentShareButton(boolean installed, @NonNull String packId, @NonNull String packKey) {
|
||||
if (installed) {
|
||||
shareButton.setVisibility(View.VISIBLE);
|
||||
shareButtonImage.setVisibility(View.VISIBLE);
|
||||
shareButton.setOnClickListener(v -> {
|
||||
Intent composeIntent = new Intent(this, ShareActivity.class);
|
||||
composeIntent.putExtra(Intent.EXTRA_TEXT, StickerUrl.createShareLink(packId, packKey));
|
||||
startActivity(composeIntent);
|
||||
finish();
|
||||
});
|
||||
} else {
|
||||
shareButton.setVisibility(View.GONE);
|
||||
shareButtonImage.setVisibility(View.GONE);
|
||||
shareButton.setOnClickListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void presentError() {
|
||||
Toast.makeText(this, R.string.StickerPackPreviewActivity_failed_to_load_sticker_pack, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
private void onScreenWidthChanged(int newWidth) {
|
||||
if (layoutManager != null) {
|
||||
layoutManager.setSpanCount(newWidth / getResources().getDimensionPixelOffset(R.dimen.sticker_preview_sticker_size));
|
||||
}
|
||||
}
|
||||
|
||||
private int getScreenWidth() {
|
||||
Point size = new Point();
|
||||
getWindowManager().getDefaultDisplay().getSize(size);
|
||||
return size.x;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class StickerPackPreviewAdapter extends RecyclerView.Adapter<StickerPackPreviewAdapter.StickerViewHolder> {
|
||||
|
||||
private final GlideRequests glideRequests;
|
||||
private final List<StickerManifest.Sticker> list;
|
||||
|
||||
public StickerPackPreviewAdapter(@NonNull GlideRequests glideRequests) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.list = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull StickerViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
|
||||
return new StickerViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.sticker_preview_list_item, viewGroup, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull StickerViewHolder stickerViewHolder, int i) {
|
||||
stickerViewHolder.bind(glideRequests, list.get(i));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return list.size();
|
||||
}
|
||||
|
||||
void setStickers(List<StickerManifest.Sticker> stickers) {
|
||||
list.clear();
|
||||
list.addAll(stickers);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
static class StickerViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ImageView image;
|
||||
|
||||
private StickerViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
this.image = itemView.findViewById(R.id.sticker_install_item_image);
|
||||
}
|
||||
|
||||
void bind(@NonNull GlideRequests glideRequests, @NonNull StickerManifest.Sticker sticker) {
|
||||
Object model = sticker.getUri().isPresent() ? new DecryptableStreamUriLoader.DecryptableUri(sticker.getUri().get())
|
||||
: new StickerRemoteUri(sticker.getPackId(), sticker.getPackKey(), sticker.getId());
|
||||
glideRequests.load(model)
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.into(image);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.gms.common.util.Hex;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.StickerDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.StickerPackRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public final class StickerPackPreviewRepository implements InjectableType {
|
||||
|
||||
private static final String TAG = Log.tag(StickerPackPreviewRepository.class);
|
||||
|
||||
private final StickerDatabase stickerDatabase;
|
||||
|
||||
@Inject SignalServiceMessageReceiver receiver;
|
||||
|
||||
public StickerPackPreviewRepository(@NonNull Context context) {
|
||||
ApplicationContext.getInstance(context).injectDependencies(this);
|
||||
this.stickerDatabase = DatabaseFactory.getStickerDatabase(context);
|
||||
}
|
||||
|
||||
public void getStickerManifest(@NonNull String packId,
|
||||
@NonNull String packKey,
|
||||
@NonNull Callback<Optional<StickerManifestResult>> callback)
|
||||
{
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
Optional<StickerManifestResult> localManifest = getManifestFromDatabase(packId);
|
||||
|
||||
if (localManifest.isPresent()) {
|
||||
Log.d(TAG, "Found manifest locally.");
|
||||
callback.onComplete(localManifest);
|
||||
} else {
|
||||
Log.d(TAG, "Looking for manifest remotely.");
|
||||
callback.onComplete(getManifestRemote(packId, packKey));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private Optional<StickerManifestResult> getManifestFromDatabase(@NonNull String packId) {
|
||||
StickerPackRecord record = stickerDatabase.getStickerPack(packId);
|
||||
|
||||
if (record != null && record.isInstalled()) {
|
||||
StickerManifest.Sticker cover = toSticker(record.getCover());
|
||||
List<StickerManifest.Sticker> stickers = getStickersFromDatabase(packId);
|
||||
|
||||
StickerManifest manifest = new StickerManifest(record.getPackId(),
|
||||
record.getPackKey(),
|
||||
record.getTitle(),
|
||||
record.getAuthor(),
|
||||
Optional.of(cover),
|
||||
stickers);
|
||||
|
||||
return Optional.of(new StickerManifestResult(manifest, record.isInstalled()));
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private Optional<StickerManifestResult> getManifestRemote(@NonNull String packId, @NonNull String packKey) {
|
||||
try {
|
||||
byte[] packIdBytes = Hex.stringToBytes(packId);
|
||||
byte[] packKeyBytes = Hex.stringToBytes(packKey);
|
||||
SignalServiceStickerManifest remoteManifest = receiver.retrieveStickerManifest(packIdBytes, packKeyBytes);
|
||||
StickerManifest localManifest = new StickerManifest(packId,
|
||||
packKey,
|
||||
remoteManifest.getTitle(),
|
||||
remoteManifest.getAuthor(),
|
||||
toOptionalSticker(packId, packKey, remoteManifest.getCover()),
|
||||
Stream.of(remoteManifest.getStickers())
|
||||
.map(s -> toSticker(packId, packKey, s))
|
||||
.toList());
|
||||
|
||||
return Optional.of(new StickerManifestResult(localManifest, false));
|
||||
} catch (IOException | InvalidMessageException e) {
|
||||
Log.w(TAG, "Failed to retrieve pack manifest.", e);
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private List<StickerManifest.Sticker> getStickersFromDatabase(@NonNull String packId) {
|
||||
List<StickerManifest.Sticker> stickers = new ArrayList<>();
|
||||
|
||||
try (Cursor cursor = stickerDatabase.getStickersForPack(packId)) {
|
||||
StickerDatabase.StickerRecordReader reader = new StickerDatabase.StickerRecordReader(cursor);
|
||||
|
||||
StickerRecord record;
|
||||
while ((record = reader.getNext()) != null) {
|
||||
stickers.add(toSticker(record));
|
||||
}
|
||||
}
|
||||
|
||||
return stickers;
|
||||
}
|
||||
|
||||
|
||||
private Optional<StickerManifest.Sticker> toOptionalSticker(@NonNull String packId,
|
||||
@NonNull String packKey,
|
||||
@NonNull Optional<SignalServiceStickerManifest.StickerInfo> remoteSticker)
|
||||
{
|
||||
return remoteSticker.isPresent() ? Optional.of(toSticker(packId, packKey, remoteSticker.get()))
|
||||
: Optional.absent();
|
||||
}
|
||||
|
||||
private StickerManifest.Sticker toSticker(@NonNull String packId,
|
||||
@NonNull String packKey,
|
||||
@NonNull SignalServiceStickerManifest.StickerInfo remoteSticker)
|
||||
{
|
||||
return new StickerManifest.Sticker(packId, packKey, remoteSticker.getId(), remoteSticker.getEmoji());
|
||||
}
|
||||
|
||||
private StickerManifest.Sticker toSticker(@NonNull StickerRecord record) {
|
||||
return new StickerManifest.Sticker(record.getPackId(), record.getPackKey(), record.getStickerId(), record.getEmoji(), record.getUri());
|
||||
}
|
||||
|
||||
static class StickerManifestResult {
|
||||
private final StickerManifest manifest;
|
||||
private final boolean isInstalled;
|
||||
|
||||
StickerManifestResult(StickerManifest manifest, boolean isInstalled) {
|
||||
this.manifest = manifest;
|
||||
this.isInstalled = isInstalled;
|
||||
}
|
||||
|
||||
public StickerManifest getManifest() {
|
||||
return manifest;
|
||||
}
|
||||
|
||||
public boolean isInstalled() {
|
||||
return isInstalled;
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback<T> {
|
||||
void onComplete(T result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.app.Application;
|
||||
import android.arch.lifecycle.LiveData;
|
||||
import android.arch.lifecycle.MutableLiveData;
|
||||
import android.arch.lifecycle.ViewModel;
|
||||
import android.arch.lifecycle.ViewModelProvider;
|
||||
import android.database.ContentObserver;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
|
||||
import org.thoughtcrime.securesms.stickers.StickerPackPreviewRepository.StickerManifestResult;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
final class StickerPackPreviewViewModel extends ViewModel {
|
||||
|
||||
private final Application application;
|
||||
private final StickerPackPreviewRepository previewRepository;
|
||||
private final StickerManagementRepository managementRepository;
|
||||
private final MutableLiveData<Optional<StickerManifestResult>> stickerManifest;
|
||||
private final ContentObserver packObserver;
|
||||
|
||||
private String packId;
|
||||
private String packKey;
|
||||
|
||||
private StickerPackPreviewViewModel(@NonNull Application application,
|
||||
@NonNull StickerPackPreviewRepository previewRepository,
|
||||
@NonNull StickerManagementRepository managementRepository)
|
||||
{
|
||||
this.application = application;
|
||||
this.previewRepository = previewRepository;
|
||||
this.managementRepository = managementRepository;
|
||||
this.stickerManifest = new MutableLiveData<>();
|
||||
this.packObserver = new ContentObserver(new Handler()) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
if (!TextUtils.isEmpty(packId) && !TextUtils.isEmpty(packKey)) {
|
||||
previewRepository.getStickerManifest(packId, packKey, stickerManifest::postValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
application.getContentResolver().registerContentObserver(DatabaseContentProviders.StickerPack.CONTENT_URI, true, packObserver);
|
||||
}
|
||||
|
||||
LiveData<Optional<StickerManifestResult>> getStickerManifest(@NonNull String packId, @NonNull String packKey) {
|
||||
this.packId = packId;
|
||||
this.packKey = packKey;
|
||||
|
||||
previewRepository.getStickerManifest(packId, packKey, stickerManifest::postValue);
|
||||
|
||||
return stickerManifest;
|
||||
}
|
||||
|
||||
void onInstallClicked() {
|
||||
managementRepository.installStickerPack(packId, packKey);
|
||||
}
|
||||
|
||||
void onRemoveClicked() {
|
||||
managementRepository.uninstallStickerPack(packId, packKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
application.getContentResolver().unregisterContentObserver(packObserver);
|
||||
}
|
||||
|
||||
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
private final Application application;
|
||||
private final StickerPackPreviewRepository previewRepository;
|
||||
private final StickerManagementRepository managementRepository;
|
||||
|
||||
Factory(@NonNull Application application,
|
||||
@NonNull StickerPackPreviewRepository previewRepository,
|
||||
@NonNull StickerManagementRepository managementRepository)
|
||||
{
|
||||
this.application = application;
|
||||
this.previewRepository = previewRepository;
|
||||
this.managementRepository = managementRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new StickerPackPreviewViewModel(application, previewRepository, managementRepository));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
|
||||
/**
|
||||
* A popup that shows a given sticker fullscreen.
|
||||
*/
|
||||
final class StickerPreviewPopup extends PopupWindow {
|
||||
|
||||
private final GlideRequests glideRequests;
|
||||
private final ImageView image;
|
||||
|
||||
StickerPreviewPopup(@NonNull Context context, @NonNull GlideRequests glideRequests) {
|
||||
super(LayoutInflater.from(context).inflate(R.layout.sticker_preview_popup, null),
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
this.glideRequests = glideRequests;
|
||||
this.image = getContentView().findViewById(R.id.sticker_popup_image);
|
||||
|
||||
setTouchable(false);
|
||||
}
|
||||
|
||||
void presentSticker(@NonNull StickerRecord stickerRecord) {
|
||||
glideRequests.load(new DecryptableUri(stickerRecord.getUri()))
|
||||
.into(image);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.load.Key;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Used as a model to be given to Glide for a sticker that isn't present locally.
|
||||
*/
|
||||
public final class StickerRemoteUri implements Key {
|
||||
|
||||
private final String packId;
|
||||
private final String packKey;
|
||||
private final int stickerId;
|
||||
|
||||
public StickerRemoteUri(@NonNull String packId, @NonNull String packKey, int stickerId) {
|
||||
this.packId = packId;
|
||||
this.packKey = packKey;
|
||||
this.stickerId = stickerId;
|
||||
}
|
||||
|
||||
public @NonNull String getPackId() {
|
||||
return packId;
|
||||
}
|
||||
|
||||
public @NonNull String getPackKey() {
|
||||
return packKey;
|
||||
}
|
||||
|
||||
public int getStickerId() {
|
||||
return stickerId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
|
||||
messageDigest.update(packId.getBytes());
|
||||
messageDigest.update(packKey.getBytes());
|
||||
messageDigest.update(ByteBuffer.allocate(4).putInt(stickerId).array());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
StickerRemoteUri that = (StickerRemoteUri) o;
|
||||
return stickerId == that.stickerId &&
|
||||
Objects.equals(packId, that.packId) &&
|
||||
Objects.equals(packKey, that.packKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(packId, packKey, stickerId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.Priority;
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.data.DataFetcher;
|
||||
import com.google.android.gms.common.util.Hex;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Downloads a sticker remotely. Used with Glide.
|
||||
*/
|
||||
public final class StickerRemoteUriFetcher implements DataFetcher<InputStream> {
|
||||
|
||||
private static final String TAG = Log.tag(StickerRemoteUriFetcher.class);
|
||||
|
||||
private final SignalServiceMessageReceiver receiver;
|
||||
private final StickerRemoteUri stickerUri;
|
||||
|
||||
public StickerRemoteUriFetcher(@NonNull SignalServiceMessageReceiver receiver, @NonNull StickerRemoteUri stickerUri) {
|
||||
this.receiver = receiver;
|
||||
this.stickerUri = stickerUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
|
||||
try {
|
||||
byte[] packIdBytes = Hex.stringToBytes(stickerUri.getPackId());
|
||||
byte[] packKeyBytes = Hex.stringToBytes(stickerUri.getPackKey());
|
||||
InputStream stream = receiver.retrieveSticker(packIdBytes, packKeyBytes, stickerUri.getStickerId());
|
||||
|
||||
callback.onDataReady(stream);
|
||||
} catch (IOException | InvalidMessageException e) {
|
||||
callback.onLoadFailed(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
Log.d(TAG, "Canceled.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Class<InputStream> getDataClass() {
|
||||
return InputStream.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull DataSource getDataSource() {
|
||||
return DataSource.REMOTE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.model.ModelLoader;
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
* Glide loader to fetch a sticker remotely.
|
||||
*/
|
||||
public final class StickerRemoteUriLoader implements ModelLoader<StickerRemoteUri, InputStream> {
|
||||
|
||||
private final SignalServiceMessageReceiver receiver;
|
||||
|
||||
public StickerRemoteUriLoader(@NonNull SignalServiceMessageReceiver receiver) {
|
||||
this.receiver = receiver;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public @Nullable LoadData<InputStream> buildLoadData(@NonNull StickerRemoteUri sticker, int width, int height, @NonNull Options options) {
|
||||
return new LoadData<>(sticker, new StickerRemoteUriFetcher(receiver, sticker));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull StickerRemoteUri sticker) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static class Factory implements ModelLoaderFactory<StickerRemoteUri, InputStream>, InjectableType {
|
||||
|
||||
@Inject SignalServiceMessageReceiver receiver;
|
||||
|
||||
public Factory(@NonNull Context context) {
|
||||
ApplicationContext.getInstance(context).injectDependencies(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ModelLoader<StickerRemoteUri, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
|
||||
return new StickerRemoteUriLoader(receiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teardown() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.CursorList;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.StickerDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.StickerPackRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
public final class StickerSearchRepository {
|
||||
|
||||
private final StickerDatabase stickerDatabase;
|
||||
private final AttachmentDatabase attachmentDatabase;
|
||||
|
||||
public StickerSearchRepository(@NonNull Context context) {
|
||||
this.stickerDatabase = DatabaseFactory.getStickerDatabase(context);
|
||||
this.attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
}
|
||||
|
||||
public void searchByEmoji(@NonNull String emoji, @NonNull Callback<CursorList<StickerRecord>> callback) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
Cursor cursor = stickerDatabase.getStickersByEmoji(emoji);
|
||||
|
||||
if (cursor != null) {
|
||||
callback.onResult(new CursorList<>(cursor, new StickerModelBuilder()));
|
||||
} else {
|
||||
callback.onResult(CursorList.emptyList());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void getStickerFeatureAvailability(@NonNull Callback<Boolean> callback) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try (Cursor cursor = stickerDatabase.getAllStickerPacks("1")) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
callback.onResult(true);
|
||||
} else {
|
||||
callback.onResult(attachmentDatabase.hasStickerAttachments());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class StickerModelBuilder implements CursorList.ModelBuilder<StickerRecord> {
|
||||
@Override
|
||||
public StickerRecord build(@NonNull Cursor cursor) {
|
||||
return new StickerDatabase.StickerRecordReader(cursor).getCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
private static class StickerPackModelBuilder implements CursorList.ModelBuilder<StickerPackRecord> {
|
||||
@Override
|
||||
public StickerPackRecord build(@NonNull Cursor cursor) {
|
||||
return new StickerDatabase.StickerPackRecordReader(cursor).getCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback<T> {
|
||||
void onResult(T result);
|
||||
}
|
||||
}
|
||||
57
src/org/thoughtcrime/securesms/stickers/StickerUrl.java
Normal file
57
src/org/thoughtcrime/securesms/stickers/StickerUrl.java
Normal file
@@ -0,0 +1,57 @@
|
||||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Manages creating and parsing the various sticker pack URLs.
|
||||
*/
|
||||
public class StickerUrl {
|
||||
|
||||
private static final Pattern STICKER_URL_PATTERN = Pattern.compile("^https://signal\\.org/addstickers/#pack_id=(.*)&pack_key=(.*)$");
|
||||
|
||||
public static Optional<Pair<String, String>> parseActionUri(@Nullable Uri uri) {
|
||||
if (uri == null) return Optional.absent();
|
||||
|
||||
String packId = uri.getQueryParameter("pack_id");
|
||||
String packKey = uri.getQueryParameter("pack_key");
|
||||
|
||||
if (TextUtils.isEmpty(packId) || TextUtils.isEmpty(packKey)) {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
return Optional.of(new Pair<>(packId, packKey));
|
||||
}
|
||||
|
||||
public static @NonNull Uri createActionUri(@NonNull String packId, @NonNull String packKey) {
|
||||
return Uri.parse(String.format("sgnl://addstickers?pack_id=%s&pack_key=%s", packId, packKey));
|
||||
}
|
||||
|
||||
public static boolean isValidShareLink(@Nullable String url) {
|
||||
return parseShareLink(url).isPresent();
|
||||
}
|
||||
|
||||
public static @NonNull Optional<Pair<String, String>> parseShareLink(@Nullable String url) {
|
||||
if (url == null) return Optional.absent();
|
||||
|
||||
Matcher matcher = STICKER_URL_PATTERN.matcher(url);
|
||||
|
||||
if (matcher.matches() && matcher.groupCount() == 2) {
|
||||
return Optional.of(new Pair<>(matcher.group(1), matcher.group(2)));
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
public static String createShareLink(@NonNull String packId, @NonNull String packKey) {
|
||||
return "https://signal.org/addstickers/#pack_id=" + packId + "&pack_key=" + packKey;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user