fix recent emoji pane

1) Make recent list properly update and invalidate.
2) Show most-recently-used first.
3) Refactoring

Closes #3171
// FREEBIE
This commit is contained in:
Jake McGinty 2015-05-14 21:08:37 -07:00 committed by Moxie Marlinspike
parent 5ec9197912
commit cf420de65f
11 changed files with 210 additions and 172 deletions

View File

@ -16,7 +16,6 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import com.astuetz.PagerSlidingTabStrip; import com.astuetz.PagerSlidingTabStrip;
@ -38,6 +37,7 @@ public class EmojiDrawer extends Fragment {
private KeyboardAwareLinearLayout container; private KeyboardAwareLinearLayout container;
private ViewPager pager; private ViewPager pager;
private PagerSlidingTabStrip strip; private PagerSlidingTabStrip strip;
private RecentEmojiPageModel recentModel;
public static EmojiDrawer newInstance(@ArrayRes int categories, @ArrayRes int icons) { public static EmojiDrawer newInstance(@ArrayRes int categories, @ArrayRes int icons) {
final EmojiDrawer fragment = new EmojiDrawer(); final EmojiDrawer fragment = new EmojiDrawer();
@ -104,6 +104,7 @@ public class EmojiDrawer extends Fragment {
getArguments().getInt("icons")), getArguments().getInt("icons")),
new EmojiSelectionListener() { new EmojiSelectionListener() {
@Override public void onEmojiSelected(int emojiCode) { @Override public void onEmojiSelected(int emojiCode) {
recentModel.onCodePointSelected(emojiCode);
composeText.insertEmoji(emojiCode); composeText.insertEmoji(emojiCode);
} }
})); }));
@ -114,7 +115,8 @@ public class EmojiDrawer extends Fragment {
final int[] icons = ResUtil.getResourceIds(getActivity(), iconsRes); final int[] icons = ResUtil.getResourceIds(getActivity(), iconsRes);
final int[] pages = ResUtil.getResourceIds(getActivity(), pagesRes); final int[] pages = ResUtil.getResourceIds(getActivity(), pagesRes);
final List<EmojiPageModel> models = new LinkedList<>(); final List<EmojiPageModel> models = new LinkedList<>();
models.add(new RecentEmojiPageModel(getActivity())); recentModel = new RecentEmojiPageModel(getActivity());
models.add(recentModel);
for (int i = 0; i < icons.length; i++) { for (int i = 0; i < icons.length; i++) {
models.add(new StaticEmojiPageModel(icons[i], getResources().getIntArray(pages[i]))); models.add(new StaticEmojiPageModel(icons[i], getResources().getIntArray(pages[i])));
} }

View File

@ -3,29 +3,26 @@ package org.thoughtcrime.securesms.components.emoji;
import android.content.Context; import android.content.Context;
import android.support.v7.widget.AppCompatEditText; import android.support.v7.widget.AppCompatEditText;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log;
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.InvalidatingPageLoadedListener;
public class EmojiEditText extends AppCompatEditText { public class EmojiEditText extends AppCompatEditText {
public EmojiEditText(Context context) { public EmojiEditText(Context context) {
super(context); super(context);
init();
} }
public EmojiEditText(Context context, AttributeSet attrs) { public EmojiEditText(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
init();
} }
public EmojiEditText(Context context, AttributeSet attrs, public EmojiEditText(Context context, AttributeSet attrs,
int defStyleAttr) int defStyleAttr)
{ {
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
init();
} }
private void init() { @Override public void setText(CharSequence text, BufferType type) {
super.setText(EmojiProvider.getInstance(getContext()).emojify(text, EmojiProvider.EMOJI_SMALL, new PostInvalidateCallback(this)),
BufferType.SPANNABLE);
} }
public void insertEmoji(int codePoint) { public void insertEmoji(int codePoint) {
@ -34,7 +31,7 @@ public class EmojiEditText extends AppCompatEditText {
final char[] chars = Character.toChars(codePoint); final char[] chars = Character.toChars(codePoint);
final CharSequence text = EmojiProvider.getInstance(getContext()).emojify(new String(chars), final CharSequence text = EmojiProvider.getInstance(getContext()).emojify(new String(chars),
EmojiProvider.EMOJI_SMALL, EmojiProvider.EMOJI_SMALL,
new InvalidatingPageLoadedListener(this)); new PostInvalidateCallback(this));
getText().replace(Math.min(start, end), Math.max(start, end), text); getText().replace(Math.min(start, end), Math.max(start, end), text);
setSelection(end + chars.length); setSelection(end + chars.length);

View File

@ -17,7 +17,7 @@ import android.widget.GridView;
import android.widget.ImageView; import android.widget.ImageView;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.InvalidatingPageLoadedListener; import org.thoughtcrime.securesms.components.emoji.EmojiPageModel.OnModelChangedListener;
public class EmojiPageFragment extends Fragment { public class EmojiPageFragment extends Fragment {
private static final String TAG = EmojiPageFragment.class.getSimpleName(); private static final String TAG = EmojiPageFragment.class.getSimpleName();
@ -42,11 +42,16 @@ public class EmojiPageFragment extends Fragment {
grid.setColumnWidth(getResources().getDimensionPixelSize(R.dimen.emoji_drawer_size) + 2 * getResources().getDimensionPixelSize(R.dimen.emoji_drawer_item_padding)); grid.setColumnWidth(getResources().getDimensionPixelSize(R.dimen.emoji_drawer_size) + 2 * getResources().getDimensionPixelSize(R.dimen.emoji_drawer_item_padding));
grid.setOnItemClickListener(new OnItemClickListener() { grid.setOnItemClickListener(new OnItemClickListener() {
@Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
model.onCodePointSelected((Integer)view.getTag());
if (listener != null) listener.onEmojiSelected((Integer)view.getTag()); if (listener != null) listener.onEmojiSelected((Integer)view.getTag());
} }
}); });
grid.setAdapter(new EmojiGridAdapter(getActivity(), model)); grid.setAdapter(new EmojiGridAdapter(getActivity(), model));
model.setOnModelChangedListener(new OnModelChangedListener() {
@Override public void onModelChanged() {
((EmojiGridAdapter)grid.getAdapter()).notifyDataSetChanged();
}
});
return view; return view;
} }
@ -99,9 +104,7 @@ public class EmojiPageFragment extends Fragment {
final Integer unicodeTag = model.getCodePoints()[position]; final Integer unicodeTag = model.getCodePoints()[position];
final EmojiProvider provider = EmojiProvider.getInstance(context); final EmojiProvider provider = EmojiProvider.getInstance(context);
final Drawable drawable = provider.getEmojiDrawable(unicodeTag, final Drawable drawable = provider.getEmojiDrawable(unicodeTag, EmojiProvider.EMOJI_HUGE);
EmojiProvider.EMOJI_HUGE,
new InvalidatingPageLoadedListener(view));
view.setImageDrawable(drawable); view.setImageDrawable(drawable);
view.setPadding(pad, pad, pad, pad); view.setPadding(pad, pad, pad, pad);

View File

@ -1,7 +1,16 @@
package org.thoughtcrime.securesms.components.emoji; package org.thoughtcrime.securesms.components.emoji;
public interface EmojiPageModel { public abstract class EmojiPageModel {
int getIconRes(); protected OnModelChangedListener listener;
int[] getCodePoints();
void onCodePointSelected(int codePoint); public abstract int getIconRes();
public abstract int[] getCodePoints();
public void setOnModelChangedListener(OnModelChangedListener listener) {
this.listener = listener;
}
interface OnModelChangedListener {
void onModelChanged();
}
} }

View File

@ -8,18 +8,20 @@ import android.graphics.Paint;
import android.graphics.PixelFormat; import android.graphics.PixelFormat;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.graphics.drawable.Drawable.Callback;
import android.os.AsyncTask;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.style.ImageSpan;
import android.util.Log; import android.util.Log;
import android.util.SparseArray; import android.util.SparseArray;
import android.view.View;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.FutureTaskListener;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.ResUtil; import org.thoughtcrime.securesms.util.ResUtil;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
@ -27,17 +29,14 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.ref.SoftReference; import java.lang.ref.SoftReference;
import java.util.Arrays; import java.util.Arrays;
import java.util.concurrent.ExecutorService; import java.util.concurrent.Callable;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class EmojiProvider { public class EmojiProvider {
private static final String TAG = EmojiProvider.class.getSimpleName(); private static final String TAG = EmojiProvider.class.getSimpleName();
private static final ExecutorService executor = Util.newSingleThreadedLifoExecutor(); private static volatile EmojiProvider instance = null;
private static volatile EmojiProvider instance = null; private static final Paint paint = new Paint();
private static final SparseArray<SoftReference<Bitmap>> bitmaps = new SparseArray<>();
private static final Paint paint = new Paint();
private static final Handler handler = new Handler(Looper.getMainLooper());
static { paint.setFilterBitmap(true); } static { paint.setFilterBitmap(true); }
private final SparseArray<DrawInfo> offsets = new SparseArray<>(); private final SparseArray<DrawInfo> offsets = new SparseArray<>();
@ -55,7 +54,7 @@ public class EmojiProvider {
private final Context context; private final Context context;
private final int bigDrawSize; private final int bigDrawSize;
private final int[] pages; private final Handler handler = new Handler(Looper.getMainLooper());
public static EmojiProvider getInstance(Context context) { public static EmojiProvider getInstance(Context context) {
if (instance == null) { if (instance == null) {
@ -69,72 +68,28 @@ public class EmojiProvider {
} }
private EmojiProvider(Context context) { private EmojiProvider(Context context) {
this.context = context.getApplicationContext(); int[] pages = ResUtil.getResourceIds(context, R.array.emoji_categories);
this.context = context.getApplicationContext();
this.bigDrawSize = context.getResources().getDimensionPixelSize(R.dimen.emoji_drawer_size); this.bigDrawSize = context.getResources().getDimensionPixelSize(R.dimen.emoji_drawer_size);
this.pages = ResUtil.getResourceIds(context, R.array.emoji_categories);
for (int i = 0; i < pages.length; i++) { for (int i = 0; i < pages.length; i++) {
final int[] page = context.getResources().getIntArray(pages[i]); final EmojiPageBitmap page = new EmojiPageBitmap(i);
for (int j = 0; j < page.length; j++) { final int[] codePoints = context.getResources().getIntArray(pages[i]);
offsets.put(page[j], new DrawInfo(i, j)); for (int j = 0; j < codePoints.length; j++) {
offsets.put(codePoints[j], new DrawInfo(page, j));
} }
} }
} }
private void preloadPage(final int page, final PageLoadedListener pageLoadListener) { public CharSequence emojify(CharSequence text, double size, Callback callback) {
executor.submit(new Runnable() {
@Override
public void run() {
try {
loadPage(page);
if (pageLoadListener != null) {
pageLoadListener.onPageLoaded();
}
} catch (IOException ioe) {
Log.w(TAG, ioe);
}
}
});
}
private void loadPage(int page) throws IOException {
if (page < 0 || page >= pages.length) {
throw new IndexOutOfBoundsException("can't load page that doesn't exist");
}
if (bitmaps.get(page) != null && bitmaps.get(page).get() != null) return;
try {
final String file = "emoji_" + page + "_wrapped.png";
final InputStream measureStream = context.getAssets().open(file);
final InputStream bitmapStream = context.getAssets().open(file);
final Bitmap bitmap = BitmapUtil.createScaledBitmap(measureStream, bitmapStream, (float) bigDrawSize / (float) EMOJI_RAW_SIZE);
bitmaps.put(page, new SoftReference<>(bitmap));
Log.w(TAG, "onPageLoaded(" + page + ")");
} catch (IOException ioe) {
Log.w(TAG, ioe);
throw ioe;
} catch (BitmapDecodingException bde) {
Log.w(TAG, bde);
throw new AssertionError("emoji sprite asset is corrupted or android decoding is broken");
}
}
public CharSequence emojify(CharSequence text, PageLoadedListener pageLoadedListener) {
return emojify(text, EMOJI_LARGE, pageLoadedListener);
}
public CharSequence emojify(CharSequence text, double size, PageLoadedListener pageLoadedListener) {
Matcher matches = EMOJI_RANGE.matcher(text); Matcher matches = EMOJI_RANGE.matcher(text);
SpannableStringBuilder builder = new SpannableStringBuilder(text); SpannableStringBuilder builder = new SpannableStringBuilder(text);
while (matches.find()) { while (matches.find()) {
int codePoint = matches.group().codePointAt(0); int codePoint = matches.group().codePointAt(0);
Drawable drawable = getEmojiDrawable(codePoint, size, pageLoadedListener); Drawable drawable = getEmojiDrawable(codePoint, size);
if (drawable != null) { if (drawable != null) {
ImageSpan imageSpan = new ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM); builder.setSpan(new InvalidatingDrawableSpan(drawable, callback), matches.start(), matches.end(),
char[] chars = new char[matches.end() - matches.start()];
Arrays.fill(chars, ' ');
builder.setSpan(imageSpan, matches.start(), matches.end(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
} }
@ -142,51 +97,52 @@ public class EmojiProvider {
return builder; return builder;
} }
public Drawable getEmojiDrawable(int emojiCode, double size, PageLoadedListener pageLoadedListener) { public Drawable getEmojiDrawable(int emojiCode, double size) {
return getEmojiDrawable(offsets.get(emojiCode), size, pageLoadedListener); return getEmojiDrawable(offsets.get(emojiCode), size);
} }
private Drawable getEmojiDrawable(DrawInfo drawInfo, double size, PageLoadedListener pageLoadedListener) { private Drawable getEmojiDrawable(DrawInfo drawInfo, double size) {
if (drawInfo == null) { if (drawInfo == null) return null;
return null;
} final EmojiDrawable drawable = new EmojiDrawable(drawInfo, bigDrawSize);
final Drawable drawable = new EmojiDrawable(drawInfo, bigDrawSize);
drawable.setBounds(0, 0, (int)((double)bigDrawSize * size), (int)((double)bigDrawSize * size)); drawable.setBounds(0, 0, (int)((double)bigDrawSize * size), (int)((double)bigDrawSize * size));
if (bitmaps.get(drawInfo.page) == null || bitmaps.get(drawInfo.page).get() == null) { drawInfo.page.get().addListener(new FutureTaskListener<Bitmap>() {
preloadPage(drawInfo.page, pageLoadedListener); @Override public void onSuccess(final Bitmap result) {
} handler.post(new Runnable() {
@Override public void run() {
drawable.setBitmap(result);
}
});
}
@Override public void onFailure(Throwable error) {
Log.w(TAG, error);
}
});
return drawable; return drawable;
} }
public class EmojiDrawable extends Drawable { public class EmojiDrawable extends Drawable {
private final int index; private final int index;
private final int page;
private final int emojiSize; private final int emojiSize;
private Bitmap bmp; private Bitmap bmp;
@Override public int getIntrinsicWidth() {
return emojiSize;
}
@Override public int getIntrinsicHeight() {
return emojiSize;
}
public EmojiDrawable(DrawInfo info, int emojiSize) { public EmojiDrawable(DrawInfo info, int emojiSize) {
this.index = info.index; this.index = info.index;
this.page = info.page;
this.emojiSize = emojiSize; this.emojiSize = emojiSize;
} }
@Override @Override
public void draw(Canvas canvas) { public void draw(Canvas canvas) {
if (bitmaps.get(page) == null || bitmaps.get(page).get() == null) { if (bmp == null) return;
preloadPage(page, new PageLoadedListener() {
@Override public void onPageLoaded() {
handler.post(new Runnable() {
@Override public void run() {
invalidateSelf();
}
});
}
});
return;
}
if (bmp == null) {
bmp = bitmaps.get(page).get();
}
Rect b = copyBounds(); Rect b = copyBounds();
@ -202,6 +158,12 @@ public class EmojiProvider {
paint); paint);
} }
public void setBitmap(Bitmap bitmap) {
Util.assertMainThread();
bmp = bitmap;
invalidateSelf();
}
@Override @Override
public int getOpacity() { public int getOpacity() {
return PixelFormat.TRANSLUCENT; return PixelFormat.TRANSLUCENT;
@ -212,39 +174,13 @@ public class EmojiProvider {
@Override @Override
public void setColorFilter(ColorFilter cf) { } public void setColorFilter(ColorFilter cf) { }
@Override
public String toString() {
return "EmojiDrawable{" +
"page=" + page +
", index=" + index +
'}';
}
}
public static class InvalidatingPageLoadedListener implements PageLoadedListener {
private final View view;
public InvalidatingPageLoadedListener(final View view) {
this.view = view;
}
@Override
public void onPageLoaded() {
view.postInvalidate();
}
@Override
public String toString() {
return "InvalidatingPageLoadedListener{}";
}
} }
class DrawInfo { class DrawInfo {
int page; EmojiPageBitmap page;
int index; int index;
public DrawInfo(final int page, final int index) { public DrawInfo(final EmojiPageBitmap page, final int index) {
this.page = page; this.page = page;
this.index = index; this.index = index;
} }
@ -258,7 +194,67 @@ public class EmojiProvider {
} }
} }
interface PageLoadedListener { private class EmojiPageBitmap {
void onPageLoaded(); private int page;
private SoftReference<Bitmap> bitmapReference;
private ListenableFutureTask<Bitmap> task;
public EmojiPageBitmap(int page) {
this.page = page;
}
private ListenableFutureTask<Bitmap> get() {
Util.assertMainThread();
if (bitmapReference != null && bitmapReference.get() != null) {
return new ListenableFutureTask<>(bitmapReference.get());
} else if (task != null) {
return task;
} else {
Callable<Bitmap> callable = new Callable<Bitmap>() {
@Override public Bitmap call() throws Exception {
try {
Log.w(TAG, "loading page " + page);
return loadPage();
} catch (IOException ioe) {
Log.w(TAG, ioe);
}
return null;
}
};
task = new ListenableFutureTask<>(callable);
new AsyncTask<Void, Void, Void>() {
@Override protected Void doInBackground(Void... params) {
task.run();
return null;
}
@Override protected void onPostExecute(Void aVoid) {
task = null;
}
}.execute();
}
return task;
}
private Bitmap loadPage() throws IOException {
if (bitmapReference != null && bitmapReference.get() != null) return bitmapReference.get();
try {
final String file = "emoji_" + page + "_wrapped.png";
final InputStream measureStream = context.getAssets().open(file);
final InputStream bitmapStream = context.getAssets().open(file);
final Bitmap bitmap = BitmapUtil.createScaledBitmap(measureStream, bitmapStream, (float)bigDrawSize / (float)EMOJI_RAW_SIZE);
bitmapReference = new SoftReference<>(bitmap);
Log.w(TAG, "onPageLoaded(" + page + ")");
return bitmap;
} catch (IOException ioe) {
Log.w(TAG, ioe);
throw ioe;
} catch (BitmapDecodingException bde) {
Log.w(TAG, bde);
throw new AssertionError("emoji sprite asset is corrupted or android decoding is broken");
}
}
} }
} }

View File

@ -1,45 +1,26 @@
package org.thoughtcrime.securesms.components.emoji; package org.thoughtcrime.securesms.components.emoji;
import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.graphics.Rect;
import android.os.Build.VERSION_CODES;
import android.support.v7.widget.AppCompatTextView; import android.support.v7.widget.AppCompatTextView;
import android.text.method.TransformationMethod;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View;
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.InvalidatingPageLoadedListener;
public class EmojiTextView extends AppCompatTextView { public class EmojiTextView extends AppCompatTextView {
public EmojiTextView(Context context) { public EmojiTextView(Context context) {
super(context); super(context);
init();
} }
public EmojiTextView(Context context, AttributeSet attrs) { public EmojiTextView(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
init();
} }
public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr) { public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
init();
} }
private void init() { @Override public void setText(CharSequence text, BufferType type) {
setTransformationMethod(new EmojiTransformationMethod()); super.setText(EmojiProvider.getInstance(getContext()).emojify(text,
}
private static class EmojiTransformationMethod implements TransformationMethod {
@Override public CharSequence getTransformation(CharSequence source, View view) {
return EmojiProvider.getInstance(view.getContext()).emojify(source,
EmojiProvider.EMOJI_SMALL, EmojiProvider.EMOJI_SMALL,
new InvalidatingPageLoadedListener(view)); new PostInvalidateCallback(this)),
} BufferType.SPANNABLE);
@Override public void onFocusChanged(View view, CharSequence sourceText, boolean focused,
int direction, Rect previouslyFocusedRect) { }
} }
} }

View File

@ -0,0 +1,12 @@
package org.thoughtcrime.securesms.components.emoji;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Drawable.Callback;
import android.text.style.ImageSpan;
public class InvalidatingDrawableSpan extends ImageSpan {
public InvalidatingDrawableSpan(Drawable drawable, Callback callback) {
super(drawable, ALIGN_BOTTOM);
drawable.setCallback(callback);
}
}

View File

@ -0,0 +1,25 @@
package org.thoughtcrime.securesms.components.emoji;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Drawable.Callback;
import android.view.View;
public class PostInvalidateCallback implements Callback {
private final View view;
public PostInvalidateCallback(View view) {
this.view = view;
}
@Override public void invalidateDrawable(Drawable who) {
view.postInvalidate();
}
@Override public void scheduleDrawable(Drawable who, Runnable what, long when) {
}
@Override public void unscheduleDrawable(Drawable who, Runnable what) {
}
}

View File

@ -18,13 +18,14 @@ import java.io.IOException;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
public class RecentEmojiPageModel implements EmojiPageModel { public class RecentEmojiPageModel extends EmojiPageModel {
private static final String TAG = RecentEmojiPageModel.class.getSimpleName(); private static final String TAG = RecentEmojiPageModel.class.getSimpleName();
private static final String EMOJI_LRU_PREFERENCE = "pref_recent_emoji"; private static final String EMOJI_LRU_PREFERENCE = "pref_recent_emoji";
private static final int EMOJI_LRU_SIZE = 50; private static final int EMOJI_LRU_SIZE = 50;
private final SharedPreferences prefs; private final SharedPreferences prefs;
private final LinkedHashSet<Integer> recentlyUsed; private final LinkedHashSet<Integer> recentlyUsed;
private OnModelChangedListener listener;
public RecentEmojiPageModel(Context context) { public RecentEmojiPageModel(Context context) {
this.prefs = PreferenceManager.getDefaultSharedPreferences(context); this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
@ -50,10 +51,11 @@ public class RecentEmojiPageModel implements EmojiPageModel {
} }
@Override public int[] getCodePoints() { @Override public int[] getCodePoints() {
return toPrimitiveArray(recentlyUsed); return toReversePrimitiveArray(recentlyUsed);
} }
@Override public void onCodePointSelected(int codePoint) { public void onCodePointSelected(int codePoint) {
Log.w(TAG, "onCodePointSelected(" + codePoint + ")");
recentlyUsed.remove(codePoint); recentlyUsed.remove(codePoint);
recentlyUsed.add(codePoint); recentlyUsed.add(codePoint);
@ -80,6 +82,12 @@ public class RecentEmojiPageModel implements EmojiPageModel {
return null; return null;
} }
}.execute(); }.execute();
if (listener != null) listener.onModelChanged();
}
@Override public void setOnModelChangedListener(OnModelChangedListener listener) {
this.listener = listener;
} }
private LinkedHashSet<Integer> fromHexString(@Nullable LinkedHashSet<String> stringSet) { private LinkedHashSet<Integer> fromHexString(@Nullable LinkedHashSet<String> stringSet) {
@ -100,11 +108,11 @@ public class RecentEmojiPageModel implements EmojiPageModel {
return stringSet; return stringSet;
} }
private int[] toPrimitiveArray(@NonNull LinkedHashSet<Integer> integerSet) { private int[] toReversePrimitiveArray(@NonNull LinkedHashSet<Integer> integerSet) {
int[] ints = new int[integerSet.size()]; int[] ints = new int[integerSet.size()];
int i = 0; int i = integerSet.size() - 1;
for (Integer integer : integerSet) { for (Integer integer : integerSet) {
ints[i++] = integer; ints[i--] = integer;
} }
return ints; return ints;
} }

View File

@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.components.emoji;
import android.support.annotation.DrawableRes; import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
public class StaticEmojiPageModel implements EmojiPageModel { public class StaticEmojiPageModel extends EmojiPageModel {
@DrawableRes private final int icon; @DrawableRes private final int icon;
@NonNull private final int[] codePoints; @NonNull private final int[] codePoints;
@ -19,6 +19,4 @@ public class StaticEmojiPageModel implements EmojiPageModel {
@NonNull public int[] getCodePoints() { @NonNull public int[] getCodePoints() {
return codePoints; return codePoints;
} }
@Override public void onCodePointSelected(int codePoint) { }
} }

View File

@ -24,6 +24,7 @@ import android.graphics.Typeface;
import android.os.Build; import android.os.Build;
import android.os.Build.VERSION; import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES; import android.os.Build.VERSION_CODES;
import android.os.Looper;
import android.provider.Telephony; import android.provider.Telephony;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import android.text.Spannable; import android.text.Spannable;
@ -296,4 +297,10 @@ public class Util {
public static boolean isMmsCapable(Context context) { public static boolean isMmsCapable(Context context) {
return (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) || OutgoingLegacyMmsConnection.isConnectionPossible(context); return (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) || OutgoingLegacyMmsConnection.isConnectionPossible(context);
} }
public static void assertMainThread() {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new AssertionError("Main-thread assertion failed.");
}
}
} }