diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiUtil.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiUtil.java new file mode 100644 index 0000000000..7a2e06d906 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiUtil.java @@ -0,0 +1,70 @@ +package org.thoughtcrime.securesms.components.emoji; + +import androidx.annotation.NonNull; + +import org.whispersystems.libsignal.util.Pair; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public final class EmojiUtil { + + private static final Map VARIATION_MAP = new HashMap<>(); + + static { + for (EmojiPageModel page : EmojiPages.DATA_PAGES) { + for (Emoji emoji : page.getDisplayEmoji()) { + for (String variation : emoji.getVariations()) { + VARIATION_MAP.put(variation, emoji.getValue()); + } + } + } + } + + public static final int MAX_EMOJI_LENGTH; + static { + int max = 0; + for (EmojiPageModel page : EmojiPages.DATA_PAGES) { + for (String emoji : page.getEmoji()) { + max = Math.max(max, emoji.length()); + } + } + MAX_EMOJI_LENGTH = max; + } + + private EmojiUtil() {} + + /** + * This will return all ways we know of expressing a singular emoji. This is to aid in search, + * where some platforms may send an emoji we've locally marked as 'obsolete'. + */ + public static @NonNull Set getAllRepresentations(@NonNull String emoji) { + Set out = new HashSet<>(); + + out.add(emoji); + + for (Pair pair : EmojiPages.OBSOLETE) { + if (pair.first().equals(emoji)) { + out.add(pair.second()); + } else if (pair.second().equals(emoji)) { + out.add(pair.first()); + } + } + + return out; + } + + /** + * When provided an emoji that is a skin variation of another, this will return the default yellow + * version. This is to aid in search, so using a variation will still find all emojis tagged with + * the default version. + * + * If the emoji has no skin variations, this function will return the original emoji. + */ + public static @NonNull String getCanonicalRepresentation(@NonNull String emoji) { + String canonical = VARIATION_MAP.get(emoji); + return canonical != null ? canonical : emoji; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationStickerViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationStickerViewModel.java index 8f8ff2a693..2403e47884 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationStickerViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationStickerViewModel.java @@ -10,6 +10,7 @@ import android.os.Handler; import androidx.annotation.NonNull; import android.text.TextUtils; +import org.thoughtcrime.securesms.components.emoji.EmojiUtil; import org.thoughtcrime.securesms.database.CursorList; import org.thoughtcrime.securesms.database.DatabaseContentProviders; import org.thoughtcrime.securesms.database.model.StickerRecord; @@ -17,21 +18,21 @@ import org.thoughtcrime.securesms.stickers.StickerSearchRepository; import org.thoughtcrime.securesms.util.CloseableLiveData; import org.thoughtcrime.securesms.util.Throttler; +import java.util.List; + class ConversationStickerViewModel extends ViewModel { - private static final int SEARCH_LIMIT = 10; - - private final Application application; - private final StickerSearchRepository repository; - private final CloseableLiveData> stickers; - private final MutableLiveData stickersAvailable; - private final Throttler availabilityThrottler; - private final ContentObserver packObserver; + private final Application application; + private final StickerSearchRepository repository; + private final MutableLiveData> stickers; + private final MutableLiveData stickersAvailable; + private final Throttler availabilityThrottler; + private final ContentObserver packObserver; private ConversationStickerViewModel(@NonNull Application application, @NonNull StickerSearchRepository repository) { this.application = application; this.repository = repository; - this.stickers = new CloseableLiveData<>(); + this.stickers = new MutableLiveData<>(); this.stickersAvailable = new MutableLiveData<>(); this.availabilityThrottler = new Throttler(500); this.packObserver = new ContentObserver(new Handler()) { @@ -44,7 +45,7 @@ class ConversationStickerViewModel extends ViewModel { application.getContentResolver().registerContentObserver(DatabaseContentProviders.StickerPack.CONTENT_URI, true, packObserver); } - @NonNull LiveData> getStickerResults() { + @NonNull LiveData> getStickerResults() { return stickers; } @@ -54,7 +55,7 @@ class ConversationStickerViewModel extends ViewModel { } void onInputTextUpdated(@NonNull String text) { - if (TextUtils.isEmpty(text) || text.length() > SEARCH_LIMIT) { + if (TextUtils.isEmpty(text) || text.length() > EmojiUtil.MAX_EMOJI_LENGTH) { stickers.setValue(CursorList.emptyList()); } else { repository.searchByEmoji(text, stickers::postValue); @@ -63,7 +64,6 @@ class ConversationStickerViewModel extends ViewModel { @Override protected void onCleared() { - stickers.close(); application.getContentResolver().unregisterContentObserver(packObserver); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/StickerDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/StickerDatabase.java index c77d90f27b..871f25b9e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/StickerDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/StickerDatabase.java @@ -30,6 +30,8 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; import java.util.List; public class StickerDatabase extends Database { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerSearchRepository.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerSearchRepository.java index d9bde9b461..2650c1f79e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerSearchRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerSearchRepository.java @@ -4,14 +4,20 @@ import android.content.Context; import android.database.Cursor; import androidx.annotation.NonNull; +import org.thoughtcrime.securesms.components.emoji.EmojiUtil; 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.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; +import java.util.Set; + public final class StickerSearchRepository { private final StickerDatabase stickerDatabase; @@ -22,15 +28,22 @@ public final class StickerSearchRepository { this.attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context); } - public void searchByEmoji(@NonNull String emoji, @NonNull Callback> callback) { + public void searchByEmoji(@NonNull String emoji, @NonNull Callback> callback) { SignalExecutors.BOUNDED.execute(() -> { - Cursor cursor = stickerDatabase.getStickersByEmoji(emoji); + String searchEmoji = EmojiUtil.getCanonicalRepresentation(emoji); + List out = new ArrayList<>(); + Set possible = EmojiUtil.getAllRepresentations(searchEmoji); - if (cursor != null) { - callback.onResult(new CursorList<>(cursor, new StickerModelBuilder())); - } else { - callback.onResult(CursorList.emptyList()); + for (String candidate : possible) { + try (StickerRecordReader reader = new StickerRecordReader(stickerDatabase.getStickersByEmoji(candidate))) { + StickerRecord record = null; + while ((record = reader.getNext()) != null) { + out.add(record); + } + } } + + callback.onResult(out); }); } @@ -49,14 +62,7 @@ public final class StickerSearchRepository { private static class StickerModelBuilder implements CursorList.ModelBuilder { @Override public StickerRecord build(@NonNull Cursor cursor) { - return new StickerDatabase.StickerRecordReader(cursor).getCurrent(); - } - } - - private static class StickerPackModelBuilder implements CursorList.ModelBuilder { - @Override - public StickerPackRecord build(@NonNull Cursor cursor) { - return new StickerDatabase.StickerPackRecordReader(cursor).getCurrent(); + return new StickerRecordReader(cursor).getCurrent(); } }