EmojiSearch.java

  1package com.cheogram.android;
  2
  3import android.app.Activity;
  4import android.content.Context;
  5import android.graphics.drawable.Drawable;
  6import android.text.Spannable;
  7import android.text.SpannableStringBuilder;
  8import android.view.LayoutInflater;
  9import android.view.View;
 10import android.view.ViewGroup;
 11
 12import androidx.databinding.DataBindingUtil;
 13import androidx.recyclerview.widget.DiffUtil;
 14import androidx.recyclerview.widget.ListAdapter;
 15import androidx.recyclerview.widget.RecyclerView;
 16
 17import com.google.common.collect.Lists;
 18import com.google.common.io.CharStreams;
 19
 20import java.io.IOException;
 21import java.io.InputStreamReader;
 22import java.lang.Comparable;
 23import java.util.ArrayList;
 24import java.util.Arrays;
 25import java.util.Collections;
 26import java.util.List;
 27import java.util.PriorityQueue;
 28import java.util.Set;
 29import java.util.TreeSet;
 30import java.util.function.Consumer;
 31
 32import me.xdrop.fuzzywuzzy.FuzzySearch;
 33import me.xdrop.fuzzywuzzy.model.BoundExtractedResult;
 34
 35import org.json.JSONArray;
 36import org.json.JSONException;
 37import org.json.JSONObject;
 38
 39import eu.siacs.conversations.R;
 40import eu.siacs.conversations.databinding.EmojiSearchRowBinding;
 41import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
 42
 43public class EmojiSearch {
 44	protected final Set<Emoji> emoji = new TreeSet<>();
 45
 46	public EmojiSearch(Context context) {
 47		try {
 48			final JSONArray data = new JSONArray(CharStreams.toString(new InputStreamReader(context.getResources().openRawResource(R.raw.emoji), "UTF-8")));
 49			for (int i = 0; i < data.length(); i++) {
 50				emoji.add(new Emoji(data.getJSONObject(i)));
 51			}
 52		} catch (final JSONException | IOException e) {
 53			throw new IllegalStateException("emoji.json invalid: " + e);
 54		}
 55	}
 56
 57	public synchronized void addEmoji(final Emoji one) {
 58		emoji.add(one);
 59	}
 60
 61	public synchronized List<Emoji> find(final String q) {
 62		final ResultPQ pq = new ResultPQ();
 63		for (Emoji e : emoji) {
 64			if (e.emoticonMatch(q)) {
 65				pq.addTopK(e, 999999, 10);
 66			}
 67			int shortcodeScore = e.shortcodes.isEmpty() ? 0 : Collections.max(Lists.transform(e.shortcodes, (shortcode) -> FuzzySearch.ratio(q, shortcode)));
 68			int tagScore = e.tags.isEmpty() ? 0 : Collections.max(Lists.transform(e.tags, (tag) -> FuzzySearch.ratio(q, tag))) - 2;
 69			pq.addTopK(e, Math.max(shortcodeScore, tagScore), 10);
 70		}
 71
 72		for (BoundExtractedResult<Emoji> r : new ArrayList<>(pq)) {
 73			for (Emoji e : emoji) {
 74				if (e.shortcodeMatch(r.getReferent().uniquePart())) {
 75					// hack see https://stackoverflow.com/questions/76880072/imagespan-with-emojicompat
 76					e.shortcodes.clear();
 77					e.shortcodes.addAll(r.getReferent().shortcodes);
 78
 79					pq.addTopK(e, r.getScore() - 1, 10);
 80				}
 81			}
 82		}
 83
 84		List<Emoji> result = new ArrayList<>();
 85		for (int i = 0; i < 10; i++) {
 86			BoundExtractedResult<Emoji> e = pq.poll();
 87			if (e != null) result.add(e.getReferent());
 88		}
 89		Collections.reverse(result);
 90		return result;
 91	}
 92
 93	public EmojiSearchAdapter makeAdapter(Consumer<Emoji> callback) {
 94		return new EmojiSearchAdapter(callback);
 95	}
 96
 97	public static class ResultPQ extends PriorityQueue<BoundExtractedResult<Emoji>> {
 98		public void addTopK(Emoji e, int score, int k) {
 99			BoundExtractedResult r = new BoundExtractedResult(e, null, score, 0);
100			if (size() < k) {
101				add(r);
102			} else if (r.compareTo(peek()) > 0) {
103				poll();
104				add(r);
105			}
106		}
107	}
108
109	public static class Emoji implements Comparable<Emoji> {
110		protected final String unicode;
111		protected final int order;
112		protected final List<String> tags = new ArrayList<>();
113		protected final List<String> emoticon = new ArrayList<>();
114		protected final List<String> shortcodes = new ArrayList<>();
115
116		public Emoji(final String unicode, final int order) {
117			this.unicode = unicode;
118			this.order = order;
119		}
120
121		public Emoji(JSONObject o) throws JSONException {
122			unicode = o.getString("unicode");
123			order = o.getInt("order");
124			final JSONArray rawTags = o.getJSONArray("tags");
125			for (int i = 0; i < rawTags.length(); i++) {
126				tags.add(rawTags.getString(i));
127			}
128			final JSONArray rawEmoticon = o.getJSONArray("emoticon");
129			for (int i = 0; i < rawEmoticon.length(); i++) {
130				emoticon.add(rawEmoticon.getString(i));
131			}
132			final JSONArray rawShortcodes = o.getJSONArray("shortcodes");
133			for (int i = 0; i < rawShortcodes.length(); i++) {
134				shortcodes.add(rawShortcodes.getString(i));
135			}
136		}
137
138		public boolean emoticonMatch(final String q) {
139			for (final String emote : emoticon) {
140				if (emote.equals(q) || emote.equals(":" + q)) return true;
141			}
142
143			return false;
144		}
145
146		public boolean shortcodeMatch(final String q) {
147			for (final String shortcode : shortcodes) {
148				if (shortcode.equals(q)) return true;
149			}
150
151			return false;
152		}
153
154		public SpannableStringBuilder toInsert() {
155			return new SpannableStringBuilder(unicode);
156		}
157
158		public String uniquePart() {
159			return unicode;
160		}
161
162		@Override
163		public int compareTo(Emoji o) {
164			if (equals(o)) return 0;
165			if (order == o.order) return uniquePart().compareTo(o.uniquePart());
166			return order - o.order;
167		}
168
169		@Override
170		public boolean equals(Object o) {
171			if (!(o instanceof Emoji)) return false;
172
173			return uniquePart().equals(((Emoji) o).uniquePart());
174		}
175	}
176
177	public static class CustomEmoji extends Emoji {
178		protected final String source;
179		protected final Drawable icon;
180
181		public CustomEmoji(final String shortcode, final String source, final Drawable icon, final String tag) {
182			super(null, 10);
183			shortcodes.add(shortcode);
184			if (tag != null) tags.add(tag);
185			this.source = source;
186			this.icon = icon;
187			if (icon == null) {
188				throw new IllegalArgumentException("icon must not be null");
189			}
190		}
191
192		public SpannableStringBuilder toInsert() {
193			SpannableStringBuilder builder = new SpannableStringBuilder(":" + shortcodes.get(0) + ":");
194			builder.setSpan(new InlineImageSpan(icon, source), 0, builder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
195			return builder;
196		}
197
198		@Override
199		public String uniquePart() {
200			return source;
201		}
202	}
203
204	public class EmojiSearchAdapter extends ListAdapter<Emoji, EmojiSearchAdapter.ViewHolder> {
205		ReplacingSerialSingleThreadExecutor executor = new ReplacingSerialSingleThreadExecutor("EmojiSearchAdapter");
206
207		static final DiffUtil.ItemCallback<Emoji> DIFF = new DiffUtil.ItemCallback<Emoji>() {
208			@Override
209			public boolean areItemsTheSame(Emoji a, Emoji b) {
210				return a.equals(b);
211			}
212
213			@Override
214			public boolean areContentsTheSame(Emoji a, Emoji b) {
215				return a.equals(b);
216			}
217		};
218		final Consumer<Emoji> callback;
219
220		public EmojiSearchAdapter(final Consumer<Emoji> callback) {
221			super(DIFF);
222			this.callback = callback;
223		}
224
225		@Override
226		public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int position) {
227			return new ViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.emoji_search_row, viewGroup, false));
228		}
229
230		@Override
231		public void onBindViewHolder(ViewHolder viewHolder, int position) {
232			final var binding = viewHolder.binding;
233			final var item = getItem(position);
234			if (item instanceof CustomEmoji) {
235				binding.nonunicode.setText(item.toInsert());
236				binding.nonunicode.setVisibility(View.VISIBLE);
237				binding.unicode.setVisibility(View.GONE);
238			} else {
239				binding.unicode.setText(item.toInsert());
240				binding.unicode.setVisibility(View.VISIBLE);
241				binding.nonunicode.setVisibility(View.GONE);
242			}
243			binding.shortcode.setText(item.shortcodes.get(0));
244			binding.getRoot().setOnClickListener(v -> {
245				callback.accept(item);
246			});
247		}
248
249		public void search(final Activity activity, final String q) {
250			executor.execute(() -> {
251				final List<Emoji> results = find(q);
252				activity.runOnUiThread(() -> {
253					submitList(results);
254				});
255			});
256		}
257
258		public static class ViewHolder extends RecyclerView.ViewHolder {
259			public final EmojiSearchRowBinding binding;
260
261			private ViewHolder(EmojiSearchRowBinding binding) {
262				super(binding.getRoot());
263				this.binding = binding;
264			}
265		}
266	}
267}