EmojiSearch.java

  1package com.cheogram.android;
  2import android.util.Log;
  3
  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;
 11import android.widget.ArrayAdapter;
 12
 13import androidx.databinding.DataBindingUtil;
 14
 15import com.google.common.collect.Lists;
 16import com.google.common.io.CharStreams;
 17
 18import java.io.IOException;
 19import java.io.InputStreamReader;
 20import java.lang.Comparable;
 21import java.util.Arrays;
 22import java.util.ArrayList;
 23import java.util.Collections;
 24import java.util.List;
 25import java.util.Set;
 26import java.util.TreeSet;
 27
 28import me.xdrop.fuzzywuzzy.FuzzySearch;
 29import me.xdrop.fuzzywuzzy.algorithms.WeightedRatio;
 30import me.xdrop.fuzzywuzzy.model.BoundExtractedResult;
 31
 32import org.json.JSONArray;
 33import org.json.JSONException;
 34import org.json.JSONObject;
 35
 36import eu.siacs.conversations.R;
 37import eu.siacs.conversations.databinding.EmojiSearchRowBinding;
 38
 39public class EmojiSearch {
 40	protected final Set<Emoji> emoji = new TreeSet<>();
 41
 42	public EmojiSearch(Context context) {
 43		try {
 44			final JSONArray data = new JSONArray(CharStreams.toString(new InputStreamReader(context.getResources().openRawResource(R.raw.emoji), "UTF-8")));
 45			for (int i = 0; i < data.length(); i++) {
 46				emoji.add(new Emoji(data.getJSONObject(i)));
 47			}
 48		} catch (final JSONException | IOException e) {
 49			throw new IllegalStateException("emoji.json invalid: " + e);
 50		}
 51	}
 52
 53	public synchronized void addEmoji(final Emoji one) {
 54		emoji.add(one);
 55	}
 56
 57	public synchronized List<Emoji> find(final String q) {
 58		final Set<Emoji> emoticon = new TreeSet<>();
 59		for (Emoji e : emoji) {
 60			if (e.emoticonMatch(q)) {
 61				emoticon.add(e);
 62			}
 63		}
 64
 65		WeightedRatio wr = new WeightedRatio();
 66		List<BoundExtractedResult<Emoji>> result = FuzzySearch.extractTop(
 67			q,
 68			emoji,
 69			(e) -> e.fuzzyFind,
 70			(query, s) -> {
 71				int score = 0;
 72				String[] kinds = s.split(">");
 73				for (int i = 0; i < kinds.length; i++) {
 74					int nscore = Collections.max(Lists.transform(Arrays.asList(kinds[i].split("~")), (x) -> wr.apply(query, x))) - (i * 2);
 75					if (nscore > score) score = nscore;
 76				}
 77				return score;
 78			},
 79			10
 80		);
 81
 82		List<Emoji> lst = new ArrayList<>(emoticon);
 83		lst.addAll(Lists.transform(result, (r) -> r.getReferent()));
 84
 85		List<Emoji> scanList = new ArrayList<>(lst);
 86		int inserted = 0;
 87		for (int i = 0; i < scanList.size(); i++) {
 88			for (Emoji e : emoji) {
 89				if (e.shortcodeMatch(scanList.get(i).uniquePart())) {
 90					inserted ++;
 91					lst.add(i + inserted, e);
 92				}
 93			}
 94		}
 95		return lst;
 96	}
 97
 98	public EmojiSearchAdapter makeAdapter(Context context) {
 99		return new EmojiSearchAdapter(context);
100	}
101
102	public static class Emoji implements Comparable<Emoji> {
103		protected final String unicode;
104		protected final int order;
105		protected final List<String> tags = new ArrayList<>();
106		protected final List<String> emoticon = new ArrayList<>();
107		protected final List<String> shortcodes = new ArrayList<>();
108		protected final String fuzzyFind;
109
110		public Emoji(final String unicode, final int order, final String fuzzyFind) {
111			this.unicode = unicode;
112			this.order = order;
113			this.fuzzyFind = fuzzyFind;
114		}
115
116		public Emoji(JSONObject o) throws JSONException {
117			unicode = o.getString("unicode");
118			order = o.getInt("order");
119			final JSONArray rawTags = o.getJSONArray("tags");
120			for (int i = 0; i < rawTags.length(); i++) {
121				tags.add(rawTags.getString(i));
122			}
123			final JSONArray rawEmoticon = o.getJSONArray("emoticon");
124			for (int i = 0; i < rawEmoticon.length(); i++) {
125				emoticon.add(rawEmoticon.getString(i));
126			}
127			final JSONArray rawShortcodes = o.getJSONArray("shortcodes");
128			for (int i = 0; i < rawShortcodes.length(); i++) {
129				shortcodes.add(rawShortcodes.getString(i));
130			}
131			fuzzyFind = String.join("~", shortcodes) + ">" + String.join("~", tags);
132		}
133
134		public boolean emoticonMatch(final String q) {
135			for (final String emote : emoticon) {
136				if (emote.equals(q) || emote.equals(":" + q)) return true;
137			}
138
139			return false;
140		}
141
142		public boolean shortcodeMatch(final String q) {
143			for (final String shortcode : shortcodes) {
144				if (shortcode.equals(q)) return true;
145			}
146
147			return false;
148		}
149
150		public SpannableStringBuilder toInsert() {
151			return new SpannableStringBuilder(unicode);
152		}
153
154		public String uniquePart() {
155			return unicode;
156		}
157
158		@Override
159		public int compareTo(Emoji o) {
160			if (equals(o)) return 0;
161			if (order == o.order) return uniquePart().compareTo(o.uniquePart());
162			return order - o.order;
163		}
164
165		@Override
166		public boolean equals(Object o) {
167			if (!(o instanceof Emoji)) return false;
168
169			return uniquePart().equals(((Emoji) o).uniquePart());
170		}
171	}
172
173	public static class CustomEmoji extends Emoji {
174		protected final String source;
175		protected final Drawable icon;
176
177		public CustomEmoji(final String shortcode, final String source, final Drawable icon) {
178			super(null, 10, shortcode);
179			shortcodes.add(shortcode);
180			this.source = source;
181			this.icon = icon;
182			if (icon == null) {
183				throw new IllegalArgumentException("icon must not be null");
184			}
185		}
186
187		public SpannableStringBuilder toInsert() {
188			SpannableStringBuilder builder = new SpannableStringBuilder(":" + shortcodes.get(0) + ":");
189			builder.setSpan(new InlineImageSpan(icon, source), 0, builder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
190			return builder;
191		}
192
193		@Override
194		public String uniquePart() {
195			return source;
196		}
197	}
198
199	public class EmojiSearchAdapter extends ArrayAdapter<Emoji> {
200		public EmojiSearchAdapter(Context context) {
201			super(context, 0);
202		}
203
204		@Override
205		public View getView(int position, View view, ViewGroup parent) {
206			EmojiSearchRowBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.emoji_search_row, parent, false);
207			if (getItem(position) instanceof CustomEmoji) {
208				binding.nonunicode.setText(getItem(position).toInsert());
209				binding.nonunicode.setVisibility(View.VISIBLE);
210				binding.unicode.setVisibility(View.GONE);
211			} else {
212				binding.unicode.setText(getItem(position).toInsert());
213				binding.unicode.setVisibility(View.VISIBLE);
214				binding.nonunicode.setVisibility(View.GONE);
215			}
216			binding.shortcode.setText(getItem(position).shortcodes.get(0));
217			return binding.getRoot();
218		}
219
220		public void search(final String q) {
221			clear();
222			addAll(find(q));
223			notifyDataSetChanged();
224		}
225	}
226}