Merge Signal 4.41.0

This commit is contained in:
Niels Andriesse
2019-08-07 16:48:54 +10:00
584 changed files with 9504 additions and 1558 deletions

View 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);
}
}

View File

@@ -0,0 +1,134 @@
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.database.model.StickerRecord;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequests;
import java.util.ArrayList;
import java.util.List;
import network.loki.messenger.R;
/**
* 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);
}
}

View File

@@ -0,0 +1,215 @@
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.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;
import network.loki.messenger.R;
/**
* 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();
}
}

View File

@@ -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));
}
}
}

View File

@@ -0,0 +1,258 @@
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.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;
import network.loki.messenger.R;
/**
* 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();
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}
}

View 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];
}
};
}

View File

@@ -0,0 +1,118 @@
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.ShareActivity;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.util.DynamicTheme;
import network.loki.messenger.R;
/**
* 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());
});
}
}

View File

@@ -0,0 +1,329 @@
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.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;
import network.loki.messenger.R;
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 {}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}
}

View 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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,240 @@
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.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;
import network.loki.messenger.R;
/**
* 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;
}
}

View File

@@ -0,0 +1,68 @@
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.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GlideRequests;
import java.util.ArrayList;
import java.util.List;
import network.loki.messenger.R;
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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}
}

View File

@@ -0,0 +1,39 @@
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.database.model.StickerRecord;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequests;
import network.loki.messenger.R;
/**
* 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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() {
}
}
}

View File

@@ -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);
}
}

View 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;
}
}