Use vanniktech emoji library.

This commit is contained in:
akwizgran
2018-07-16 18:19:01 +01:00
parent d8b04edcd0
commit 428501cf5f
61 changed files with 256 additions and 1708 deletions

View File

@@ -1,94 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.support.annotation.UiThread;
import android.support.v7.widget.AppCompatImageButton;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import static android.view.HapticFeedbackConstants.KEYBOARD_TAP;
import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_UP;
@UiThread
public class RepeatableImageKey extends AppCompatImageButton {
private KeyEventListener listener;
public RepeatableImageKey(Context context) {
super(context);
init();
}
public RepeatableImageKey(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public RepeatableImageKey(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setOnClickListener(new RepeaterClickListener());
setOnTouchListener(new RepeaterTouchListener());
}
public void setOnKeyEventListener(KeyEventListener listener) {
this.listener = listener;
}
private void notifyListener() {
if (listener != null) listener.onKeyEvent();
}
private class RepeaterClickListener implements OnClickListener {
@Override
public void onClick(View v) {
notifyListener();
}
}
private class Repeater implements Runnable {
@Override
public void run() {
notifyListener();
postDelayed(this, ViewConfiguration.getKeyRepeatDelay());
}
}
private class RepeaterTouchListener implements OnTouchListener {
private final Repeater repeater;
private RepeaterTouchListener() {
repeater = new Repeater();
}
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case ACTION_DOWN:
view.postDelayed(repeater,
ViewConfiguration.getKeyRepeatTimeout());
performHapticFeedback(KEYBOARD_TAP);
return false;
case ACTION_CANCEL:
case ACTION_UP:
view.removeCallbacks(repeater);
return false;
default:
return false;
}
}
}
public interface KeyEventListener {
void onKeyEvent();
}
}

View File

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

View File

@@ -1,202 +0,0 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.content.res.ColorStateList;
import android.support.annotation.NonNull;
import android.support.annotation.UiThread;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.AppCompatImageView;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import com.astuetz.PagerSlidingTabStrip;
import com.astuetz.PagerSlidingTabStrip.CustomTabProvider;
import org.briarproject.briar.R;
import org.thoughtcrime.securesms.components.RepeatableImageKey;
import org.thoughtcrime.securesms.components.emoji.EmojiPageView.EmojiSelectionListener;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import static android.support.v4.widget.ImageViewCompat.setImageTintList;
import static android.view.KeyEvent.ACTION_DOWN;
import static android.view.KeyEvent.KEYCODE_DEL;
import static android.widget.ImageView.ScaleType.CENTER_INSIDE;
import static java.util.logging.Level.INFO;
@UiThread
public class EmojiDrawer extends LinearLayout {
private static final Logger LOG =
Logger.getLogger(EmojiDrawer.class.getName());
private static final KeyEvent DELETE_KEY_EVENT =
new KeyEvent(ACTION_DOWN, KEYCODE_DEL);
private ViewPager pager;
private List<EmojiPageModel> models;
private PagerSlidingTabStrip strip;
private RecentEmojiPageModel recentModel;
private EmojiEventListener listener;
private EmojiDrawerListener drawerListener;
public EmojiDrawer(Context context) {
this(context, null);
}
public EmojiDrawer(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
setOrientation(VERTICAL);
}
private void initView() {
View v = LayoutInflater.from(getContext())
.inflate(R.layout.emoji_drawer, this, true);
initializeResources(v);
initializePageModels();
initializeEmojiGrid();
}
public void setEmojiEventListener(EmojiEventListener listener) {
this.listener = listener;
}
public void setDrawerListener(EmojiDrawerListener listener) {
this.drawerListener = listener;
}
private void initializeResources(View v) {
this.pager = v.findViewById(R.id.emoji_pager);
this.strip = v.findViewById(R.id.tabs);
RepeatableImageKey backspace = v.findViewById(R.id.backspace);
backspace.setOnKeyEventListener(() -> {
if (listener != null) listener.onKeyEvent(DELETE_KEY_EVENT);
});
}
public boolean isShowing() {
return getVisibility() == VISIBLE;
}
public void show(int height) {
if (this.pager == null) initView();
ViewGroup.LayoutParams params = getLayoutParams();
params.height = height;
if (LOG.isLoggable(INFO))
LOG.info("Showing emoji drawer with height " + params.height);
setLayoutParams(params);
setVisibility(VISIBLE);
if (drawerListener != null) drawerListener.onShown();
}
public void hide() {
setVisibility(GONE);
if (drawerListener != null) drawerListener.onHidden();
}
private void initializeEmojiGrid() {
pager.setAdapter(new EmojiPagerAdapter(getContext(), models,
emoji -> {
recentModel.onCodePointSelected(emoji);
if (listener != null) listener.onEmojiSelected(emoji);
}));
if (recentModel.getEmoji().length == 0) {
pager.setCurrentItem(1);
}
strip.setViewPager(pager);
}
private void initializePageModels() {
this.models = new LinkedList<>();
this.recentModel = new RecentEmojiPageModel(getContext());
this.models.add(recentModel);
this.models.addAll(EmojiProvider.getInstance(getContext())
.getStaticPages());
}
public static class EmojiPagerAdapter extends PagerAdapter
implements CustomTabProvider {
private Context context;
private List<EmojiPageModel> pages;
private EmojiSelectionListener listener;
private EmojiPagerAdapter(@NonNull Context context,
@NonNull List<EmojiPageModel> pages,
@Nullable EmojiSelectionListener listener) {
super();
this.context = context;
this.pages = pages;
this.listener = listener;
}
@Override
public int getCount() {
return pages.size();
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container,
int position) {
EmojiPageView page = new EmojiPageView(context);
page.setModel(pages.get(position));
page.setEmojiSelectedListener(listener);
container.addView(page);
return page;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position,
@NonNull Object object) {
container.removeView((View) object);
}
@Override
public boolean isViewFromObject(@NonNull View view,
@NonNull Object object) {
return view == object;
}
@Override
public View getCustomTabView(ViewGroup viewGroup, int i) {
ImageView image = new AppCompatImageView(context);
image.setScaleType(CENTER_INSIDE);
image.setImageResource(pages.get(i).getIcon());
setImageTintList(image, ColorStateList.valueOf(
ContextCompat.getColor(context, R.color.color_primary)));
return image;
}
@Override
public void tabSelected(View view) {
view.animate().setDuration(300).alpha(1);
}
@Override
public void tabUnselected(View view) {
view.animate().setDuration(400).alpha(0.4f);
}
}
public interface EmojiEventListener extends EmojiSelectionListener {
void onKeyEvent(KeyEvent keyEvent);
}
public interface EmojiDrawerListener {
void onShown();
void onHidden();
}
}

View File

@@ -1,48 +0,0 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.UiThread;
import android.support.v7.widget.AppCompatEditText;
import android.text.InputFilter;
import android.util.AttributeSet;
import org.briarproject.briar.R;
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
import javax.annotation.Nullable;
@UiThread
public class EmojiEditText extends AppCompatEditText {
public EmojiEditText(Context context) {
this(context, null);
}
public EmojiEditText(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, R.attr.editTextStyle);
}
public EmojiEditText(Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
// this ensures the view is redrawn when invalidated
setLayerType(LAYER_TYPE_SOFTWARE, null);
setFilters(new InputFilter[] {new EmojiFilter(this)});
}
public void insertEmoji(String emoji) {
int start = getSelectionStart();
int end = getSelectionEnd();
getText().replace(Math.min(start, end), Math.max(start, end), emoji);
setSelection(start + emoji.length());
}
@Override
public void invalidateDrawable(@NonNull Drawable drawable) {
if (drawable instanceof EmojiDrawable) invalidate();
else super.invalidateDrawable(drawable);
}
}

View File

@@ -1,36 +0,0 @@
package org.thoughtcrime.securesms.components.emoji;
import android.support.annotation.UiThread;
import android.text.InputFilter;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextUtils;
import android.widget.TextView;
import javax.annotation.Nullable;
@UiThread
class EmojiFilter implements InputFilter {
private final TextView view;
EmojiFilter(TextView view) {
this.view = view;
}
@Nullable
@Override
public CharSequence filter(CharSequence source, int start, int end,
Spanned dest, int dstart, int dend) {
char[] v = new char[end - start];
TextUtils.getChars(source, start, end, v, 0);
Spannable emojified = EmojiProvider.getInstance(view.getContext())
.emojify(new String(v), view);
if (source instanceof Spanned && emojified != null) {
TextUtils.copySpansFrom((Spanned) source, start, end, null,
emojified, 0);
}
return emojified;
}
}

View File

@@ -1,20 +0,0 @@
package org.thoughtcrime.securesms.components.emoji;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import javax.annotation.Nullable;
interface EmojiPageModel {
@DrawableRes
int getIcon();
@NonNull
String[] getEmoji();
boolean hasSpriteMap();
@Nullable
String getSprite();
}

View File

@@ -1,111 +0,0 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.support.annotation.UiThread;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.FrameLayout;
import android.widget.GridView;
import org.briarproject.briar.R;
import javax.annotation.Nullable;
@UiThread
public class EmojiPageView extends FrameLayout {
private final GridView grid;
private EmojiSelectionListener listener;
public EmojiPageView(Context context) {
this(context, null);
}
public EmojiPageView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public EmojiPageView(Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
View view = LayoutInflater.from(getContext())
.inflate(R.layout.emoji_grid_layout, this, true);
grid = view.findViewById(R.id.emoji);
grid.setColumnWidth(getResources()
.getDimensionPixelSize(R.dimen.emoji_drawer_size) + 2 *
getResources().getDimensionPixelSize(
R.dimen.emoji_drawer_item_padding));
grid.setOnItemClickListener((parent, view1, position, id) -> {
if (listener != null)
listener.onEmojiSelected(((EmojiView) view1).getEmoji());
});
}
public void setModel(EmojiPageModel model) {
grid.setAdapter(new EmojiGridAdapter(getContext(), model));
}
public void setEmojiSelectedListener(EmojiSelectionListener listener) {
this.listener = listener;
}
private static class EmojiGridAdapter extends BaseAdapter {
private final Context context;
private final EmojiPageModel model;
private final int emojiSize;
private EmojiGridAdapter(Context context, EmojiPageModel model) {
this.context = context;
this.model = model;
emojiSize = (int) context.getResources()
.getDimension(R.dimen.emoji_drawer_size);
}
@Override
public int getCount() {
return model.getEmoji().length;
}
@Nullable
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
EmojiView view;
int pad = context.getResources()
.getDimensionPixelSize(R.dimen.emoji_drawer_item_padding);
if (convertView != null && convertView instanceof EmojiView) {
view = (EmojiView) convertView;
} else {
EmojiView emojiView = new EmojiView(context);
emojiView.setPadding(pad, pad, pad, pad);
emojiView.setLayoutParams(
new AbsListView.LayoutParams(emojiSize + 2 * pad,
emojiSize + 2 * pad));
view = emojiView;
}
String emoji = model.getEmoji()[position];
view.setEmoji(emoji);
return view;
}
}
interface EmojiSelectionListener {
void onEmojiSelected(String emoji);
}
}

View File

@@ -1,65 +0,0 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import org.briarproject.briar.R;
import java.util.Arrays;
import java.util.List;
class EmojiPages {
static List<EmojiPageModel> getPages(Context ctx) {
return Arrays.asList(
new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_smiley_people,
R.array.emoji_smiley_people,
"emoji_smiley_people.png"),
new StaticEmojiPageModel(ctx,
R.drawable.ic_emoji_animals_nature,
R.array.emoji_animals_nature,
"emoji_animals_nature.png"),
new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_food_drink,
R.array.emoji_food_drink,
"emoji_food_drink.png"),
new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_travel_places,
R.array.emoji_travel_places,
"emoji_travel_places.png"),
new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_activity,
R.array.emoji_activity,
"emoji_activity.png"),
new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_objects,
R.array.emoji_objects,
"emoji_objects.png"),
new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_symbols,
R.array.emoji_symbols,
"emoji_symbols.png"),
new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_flags,
R.array.emoji_flags,
"emoji_flags.png"),
new StaticEmojiPageModel(R.drawable.ic_emoji_emoticons,
new String[] {
":-)", ";-)", "(-:", ":->", ":-D", "\\o/",
":-P", "B-)", ":-$", ":-*", "O:-)", "=-O",
"O_O", "O_o", "o_O", ":O", ":-!", ":-x",
":-|", ":-\\", ":-(", ":'(", ":-[", ">:-(",
"^.^", "^_^", "\\(\u02c6\u02da\u02c6)/",
"\u30fd(\u00b0\u25c7\u00b0 )\u30ce",
"\u00af\\(\u00b0_o)/\u00af",
"\u00af\\_(\u30c4)_/\u00af", "(\u00ac_\u00ac)",
"(>_<)", "(\u2565\ufe4f\u2565)",
"(\u261e\uff9f\u30ee\uff9f)\u261e",
"\u261c(\uff9f\u30ee\uff9f\u261c)",
"\u261c(\u2312\u25bd\u2312)\u261e",
"(\u256f\u00b0\u25a1\u00b0)\u256f\ufe35",
"\u253b\u2501\u253b",
"\u252c\u2500\u252c",
"\u30ce(\u00b0\u2013\u00b0\u30ce)",
"(^._.^)\uff89",
"\u0e05^\u2022\ufecc\u2022^\u0e05",
"(\u2022_\u2022)",
" \u25a0-\u25a0\u00ac <(\u2022_\u2022) ",
"(\u25a0_\u25a0\u00ac)"
}, null));
}
}

View File

@@ -1,306 +0,0 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.support.annotation.UiThread;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.util.SparseArray;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.R;
import org.briarproject.briar.android.BriarApplication;
import org.thoughtcrime.securesms.components.util.FutureTaskListener;
import org.thoughtcrime.securesms.components.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import javax.inject.Inject;
import static android.graphics.Paint.ANTI_ALIAS_FLAG;
import static android.graphics.Paint.FILTER_BITMAP_FLAG;
import static android.graphics.PixelFormat.TRANSLUCENT;
import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class EmojiProvider {
private static volatile EmojiProvider INSTANCE = null;
private static final Paint PAINT =
new Paint(FILTER_BITMAP_FLAG | ANTI_ALIAS_FLAG);
@Inject
AndroidExecutor androidExecutor;
private static final Logger LOG =
Logger.getLogger(EmojiProvider.class.getName());
private final SparseArray<DrawInfo> offsets = new SparseArray<>();
private static final Pattern EMOJI_RANGE = Pattern.compile(
// 0x203c,0x2049 0x20a0-0x32ff 0x1f00-0x1fff 0xfe4e5-0xfe4ee
// |=== !!, ?! ===||==== misc ===||========= emoticons =======||========== flags ==========|
"[\\u203c\\u2049\\u20a0-\\u32ff\\ud83c\\udc00-\\ud83f\\udfff\\udbb9\\udce5-\\udbb9\\udcee]");
private static final int EMOJI_RAW_HEIGHT = 64;
private static final int EMOJI_RAW_WIDTH = 64;
private static final int EMOJI_VERT_PAD = 0;
private static final int EMOJI_PER_ROW = 32;
private final Context context;
private final float decodeScale;
private final List<EmojiPageModel> staticPages;
static EmojiProvider getInstance(Context context) {
if (INSTANCE == null) {
synchronized (EmojiProvider.class) {
if (INSTANCE == null) {
LOG.info("Creating new instance of EmojiProvider");
INSTANCE = new EmojiProvider(context);
BriarApplication app =
(BriarApplication) context.getApplicationContext();
app.getApplicationComponent().inject(INSTANCE);
}
}
}
return INSTANCE;
}
private EmojiProvider(Context context) {
this.context = context.getApplicationContext();
float drawerSize =
context.getResources().getDimension(R.dimen.emoji_drawer_size);
decodeScale = Math.min(1f, drawerSize / EMOJI_RAW_HEIGHT);
staticPages = EmojiPages.getPages(context);
for (EmojiPageModel page : staticPages) {
if (page.hasSpriteMap()) {
EmojiPageBitmap pageBitmap = new EmojiPageBitmap(page);
for (int i = 0; i < page.getEmoji().length; i++) {
offsets.put(Character.codePointAt(page.getEmoji()[i], 0),
new DrawInfo(pageBitmap, i));
}
}
}
}
@Nullable
@UiThread
Spannable emojify(@Nullable CharSequence text, TextView tv) {
if (text == null) return null;
Matcher matches = EMOJI_RANGE.matcher(text);
SpannableStringBuilder builder = new SpannableStringBuilder(text);
while (matches.find()) {
int codePoint = matches.group().codePointAt(0);
Drawable drawable = getEmojiDrawable(codePoint);
if (drawable != null) {
builder.setSpan(new EmojiSpan(drawable, tv), matches.start(),
matches.end(), SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
return builder;
}
@Nullable
@UiThread
Drawable getEmojiDrawable(int emojiCode) {
return getEmojiDrawable(offsets.get(emojiCode));
}
@Nullable
private Drawable getEmojiDrawable(@Nullable DrawInfo drawInfo) {
if (drawInfo == null) {
return null;
}
EmojiDrawable drawable = new EmojiDrawable(drawInfo, decodeScale);
drawInfo.page.get().addListener(new FutureTaskListener<Bitmap>() {
@Override
public void onSuccess(Bitmap result) {
androidExecutor.runOnUiThread(() -> drawable.setBitmap(result));
}
@Override
public void onFailure(Throwable error) {
logException(LOG, WARNING, error);
}
});
return drawable;
}
List<EmojiPageModel> getStaticPages() {
return staticPages;
}
static class EmojiDrawable extends Drawable {
private final DrawInfo info;
private final float intrinsicWidth, intrinsicHeight, verticalPad;
private Bitmap bmp;
private EmojiDrawable(DrawInfo info, float decodeScale) {
this.info = info;
intrinsicWidth = EMOJI_RAW_WIDTH * decodeScale;
intrinsicHeight = EMOJI_RAW_HEIGHT * decodeScale;
verticalPad = EMOJI_VERT_PAD * decodeScale;
}
@Override
public int getIntrinsicWidth() {
return (int) intrinsicWidth;
}
@Override
public int getIntrinsicHeight() {
return (int) intrinsicHeight;
}
@Override
public void draw(Canvas canvas) {
if (bmp == null) {
return;
}
int row = info.index / EMOJI_PER_ROW;
int rowIndex = info.index % EMOJI_PER_ROW;
int left = (int) (rowIndex * intrinsicWidth);
int top = (int) (row * intrinsicHeight + row * verticalPad);
int right = (int) ((rowIndex + 1) * intrinsicWidth);
int bottom =
(int) ((row + 1) * intrinsicHeight + row * verticalPad);
canvas.drawBitmap(bmp, new Rect(left, top, right, bottom),
getBounds(), PAINT);
}
void setBitmap(Bitmap bitmap) {
if (bmp == null || !bmp.sameAs(bitmap)) {
bmp = bitmap;
invalidateSelf();
}
}
@Override
public int getOpacity() {
return TRANSLUCENT;
}
@Override
public void setAlpha(int alpha) {
}
@Override
public void setColorFilter(@Nullable ColorFilter cf) {
}
}
private static class DrawInfo {
private final EmojiPageBitmap page;
private final int index;
private DrawInfo(EmojiPageBitmap page, int index) {
this.page = page;
this.index = index;
}
@Override
public String toString() {
return "DrawInfo{ " + "page = " + page + ", index = " + index + '}';
}
}
private class EmojiPageBitmap {
private final EmojiPageModel model;
private ListenableFutureTask<Bitmap> task;
private volatile SoftReference<Bitmap> bitmapReference;
private EmojiPageBitmap(EmojiPageModel model) {
this.model = model;
}
@UiThread
private ListenableFutureTask<Bitmap> get() {
if (bitmapReference != null) {
Bitmap bitmap = bitmapReference.get();
if (bitmap != null) return new ListenableFutureTask<>(bitmap);
}
if (task != null) return task;
Callable<Bitmap> callable = () -> {
if (LOG.isLoggable(INFO))
LOG.info("Loading page " + model.getSprite());
return loadPage();
};
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) {
Bitmap bitmap = bitmapReference.get();
if (bitmap != null) return bitmap;
}
try {
Bitmap bitmap = BitmapUtil.createScaledBitmap(context,
"file:///android_asset/" + model.getSprite(),
decodeScale);
bitmapReference = new SoftReference<>(bitmap);
if (LOG.isLoggable(INFO))
LOG.info("Loaded page " + model.getSprite());
return bitmap;
} catch (BitmapDecodingException e) {
logException(LOG, WARNING, e);
throw new IOException(e);
}
}
@Nullable
@Override
public String toString() {
return model.getSprite();
}
}
}

View File

@@ -1,40 +0,0 @@
package org.thoughtcrime.securesms.components.emoji;
import android.graphics.Paint;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.UiThread;
import android.widget.TextView;
import org.briarproject.briar.R;
@UiThread
class EmojiSpan extends AnimatingImageSpan {
private final int size;
private final FontMetricsInt fm;
EmojiSpan(@NonNull Drawable drawable, @NonNull TextView tv) {
super(drawable, tv);
fm = tv.getPaint().getFontMetricsInt();
size = fm != null ? Math.abs(fm.descent) + Math.abs(fm.ascent)
: tv.getResources().getDimensionPixelSize(
R.dimen.conversation_item_body_text_size);
getDrawable().setBounds(0, 0, size, size);
}
@Override
public int getSize(Paint paint, CharSequence text, int start, int end,
FontMetricsInt fm) {
if (fm != null && this.fm != null) {
fm.ascent = this.fm.ascent;
fm.descent = this.fm.descent;
fm.top = this.fm.top;
fm.bottom = this.fm.bottom;
return size;
} else {
return super.getSize(paint, text, start, end, fm);
}
}
}

View File

@@ -1,62 +0,0 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.UiThread;
import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet;
import android.view.ViewConfiguration;
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
import javax.annotation.Nullable;
import static android.widget.TextView.BufferType.SPANNABLE;
@UiThread
public class EmojiTextView extends AppCompatTextView {
public EmojiTextView(Context context) {
this(context, null);
}
public EmojiTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public EmojiTextView(Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
// this ensures the view is redrawn when invalidated
setLayerType(LAYER_TYPE_SOFTWARE, null);
}
@Override
public void setText(@Nullable CharSequence text, BufferType type) {
CharSequence source =
EmojiProvider.getInstance(getContext()).emojify(text, this);
super.setText(source, SPANNABLE);
}
@Override
public void invalidateDrawable(@NonNull Drawable drawable) {
if (drawable instanceof EmojiDrawable) invalidate();
else super.invalidateDrawable(drawable);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
// disable software layer if cache size is too small for it
int drawingCacheSize = ViewConfiguration.get(getContext())
.getScaledMaximumDrawingCacheSize();
int width = right - left;
int height = bottom - top;
int size = width * height * 4;
if (size > drawingCacheSize) {
setLayerType(LAYER_TYPE_NONE, null);
}
super.onLayout(changed, left, top, right, bottom);
}
}

View File

@@ -1,62 +0,0 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.annotation.UiThread;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.AppCompatImageButton;
import android.util.AttributeSet;
import org.briarproject.briar.R;
import org.thoughtcrime.securesms.components.emoji.EmojiDrawer.EmojiDrawerListener;
import javax.annotation.Nullable;
@UiThread
public class EmojiToggle extends AppCompatImageButton
implements EmojiDrawerListener {
private final Drawable emojiToggle;
private final Drawable imeToggle;
public EmojiToggle(Context context) {
this(context, null);
}
public EmojiToggle(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public EmojiToggle(Context context, @Nullable AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
emojiToggle = ContextCompat
.getDrawable(getContext(), R.drawable.ic_emoji_toggle);
imeToggle = ContextCompat
.getDrawable(getContext(), R.drawable.ic_keyboard);
setToEmoji();
}
public void setToEmoji() {
setImageDrawable(emojiToggle);
}
public void setToIme() {
setImageDrawable(imeToggle);
}
public void attach(EmojiDrawer drawer) {
drawer.setDrawerListener(this);
}
@Override
public void onShown() {
setToIme();
}
@Override
public void onHidden() {
setToEmoji();
}
}

View File

@@ -1,88 +0,0 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.UiThread;
import android.util.AttributeSet;
import android.view.View;
import javax.annotation.Nullable;
import static android.graphics.Paint.ANTI_ALIAS_FLAG;
import static android.graphics.Paint.Align.CENTER;
import static android.graphics.Paint.FILTER_BITMAP_FLAG;
import static org.briarproject.briar.android.util.UiUtils.resolveColorAttribute;
@UiThread
public class EmojiView extends View implements Drawable.Callback {
private final Paint paint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG);
private String emoji;
private Drawable drawable;
public EmojiView(Context context) {
this(context, null);
}
public EmojiView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public EmojiView(Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setEmoji(String emoji) {
this.emoji = emoji;
this.drawable = EmojiProvider.getInstance(getContext())
.getEmojiDrawable(Character.codePointAt(emoji, 0));
postInvalidate();
}
public String getEmoji() {
return emoji;
}
@Override
protected void onDraw(Canvas canvas) {
if (drawable != null) {
drawable.setBounds(getPaddingLeft(),
getPaddingTop(),
getWidth() - getPaddingRight(),
getHeight() - getPaddingBottom());
drawable.setCallback(this);
drawable.draw(canvas);
} else {
float targetFontSize =
0.75f * getHeight() - getPaddingTop() - getPaddingBottom();
paint.setTextSize(targetFontSize);
int color = resolveColorAttribute(getContext(),
android.R.attr.textColorPrimary);
paint.setColor(color);
paint.setTextAlign(CENTER);
int xPos = (canvas.getWidth() / 2);
int yPos = (int) ((canvas.getHeight() / 2) -
((paint.descent() + paint.ascent()) / 2));
float overflow = paint.measureText(emoji) /
(getWidth() - getPaddingLeft() - getPaddingRight());
if (overflow > 1f) {
paint.setTextSize(targetFontSize / overflow);
yPos = (int) ((canvas.getHeight() / 2) -
((paint.descent() + paint.ascent()) / 2));
}
canvas.drawText(emoji, xPos, yPos, paint);
}
}
@Override
public void invalidateDrawable(@NonNull Drawable drawable) {
super.invalidateDrawable(drawable);
postInvalidate();
}
}

View File

@@ -1,133 +0,0 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.support.annotation.DrawableRes;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R;
import org.briarproject.briar.android.BriarApplication;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class RecentEmojiPageModel implements EmojiPageModel {
private static final Logger LOG =
Logger.getLogger(RecentEmojiPageModel.class.getName());
private static final String EMOJI_LRU_PREFERENCE = "pref_emoji_recent2";
private static final int EMOJI_LRU_SIZE = 50;
private final LinkedHashSet<String> recentlyUsed; // UI thread
@Inject
SettingsManager settingsManager;
@Inject
@DatabaseExecutor
Executor dbExecutor;
RecentEmojiPageModel(Context context) {
BriarApplication app =
(BriarApplication) context.getApplicationContext();
app.getApplicationComponent().inject(this);
recentlyUsed = getPersistedCache();
}
private LinkedHashSet<String> getPersistedCache() {
String serialized;
try {
// FIXME: Don't make DB calls on the UI thread
Settings settings = settingsManager.getSettings(SETTINGS_NAMESPACE);
serialized = settings.get(EMOJI_LRU_PREFERENCE);
} catch (DbException e) {
logException(LOG, WARNING, e);
serialized = null;
}
return deserialize(serialized);
}
@DrawableRes
@Override
public int getIcon() {
return R.drawable.ic_emoji_recent;
}
@Override
public String[] getEmoji() {
return toReversePrimitiveArray(recentlyUsed);
}
@Override
public boolean hasSpriteMap() {
return false;
}
@Override
public String getSprite() {
return null;
}
void onCodePointSelected(String emoji) {
recentlyUsed.remove(emoji);
recentlyUsed.add(emoji);
if (recentlyUsed.size() > EMOJI_LRU_SIZE) {
Iterator<String> iterator = recentlyUsed.iterator();
iterator.next();
iterator.remove();
}
save(serialize(recentlyUsed));
}
private String serialize(LinkedHashSet<String> emojis) {
return StringUtils.join(emojis, "\t");
}
private LinkedHashSet<String> deserialize(@Nullable String serialized) {
if (serialized == null) return new LinkedHashSet<>();
String[] list = serialized.split("\t");
LinkedHashSet<String> result = new LinkedHashSet<>(list.length);
Collections.addAll(result, list);
return result;
}
private void save(String serialized) {
dbExecutor.execute(() -> {
Settings settings = new Settings();
settings.put(EMOJI_LRU_PREFERENCE, serialized);
try {
settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
private String[] toReversePrimitiveArray(LinkedHashSet<String> emojiSet) {
String[] emojis = new String[emojiSet.size()];
int i = emojiSet.size() - 1;
for (String emoji : emojiSet) {
emojis[i--] = emoji;
}
return emojis;
}
}

View File

@@ -1,74 +0,0 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.support.annotation.ArrayRes;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.UiThread;
import javax.annotation.Nullable;
@UiThread
class StaticEmojiPageModel implements EmojiPageModel {
@DrawableRes
private final int icon;
@NonNull
private final String[] emoji;
@Nullable
private final String sprite;
StaticEmojiPageModel(@DrawableRes int icon, @NonNull String[] emoji,
@Nullable String sprite) {
this.icon = icon;
this.emoji = emoji;
this.sprite = sprite;
}
StaticEmojiPageModel(Context ctx, @DrawableRes int icon,
@ArrayRes int res, @Nullable String sprite) {
this(icon, getEmoji(ctx, res), sprite);
}
@DrawableRes
@Override
public int getIcon() {
return icon;
}
@Override
@NonNull
public String[] getEmoji() {
return emoji;
}
@Override
public boolean hasSpriteMap() {
return sprite != null;
}
@Nullable
@Override
public String getSprite() {
return sprite;
}
@NonNull
private static String[] getEmoji(Context ctx, @ArrayRes int res) {
String[] rawStrings = ctx.getResources().getStringArray(res);
String[] emoji = new String[rawStrings.length];
int i = 0;
for (String codePoint : rawStrings) {
String[] bytes = codePoint.split(",");
int[] codePoints = new int[bytes.length];
int j = 0;
for (String b : bytes) {
codePoints[j] = Integer.valueOf(b, 16);
}
emoji[i] = new String(codePoints, 0, codePoints.length);
i++;
}
return emoji;
}
}

View File

@@ -1,12 +0,0 @@
package org.thoughtcrime.securesms.util;
public class BitmapDecodingException extends Exception {
BitmapDecodingException(String s) {
super(s);
}
BitmapDecodingException(Exception nested) {
super(nested);
}
}

View File

@@ -1,96 +0,0 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Pair;
import com.bumptech.glide.Glide;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DecodeFormat;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.bitmap.BitmapResource;
import com.bumptech.glide.load.resource.bitmap.Downsampler;
import com.bumptech.glide.load.resource.bitmap.FitCenter;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
public class BitmapUtil {
private static final Logger LOG =
Logger.getLogger(BitmapUtil.class.getName());
private static <T> InputStream getInputStreamForModel(Context context,
T model)
throws BitmapDecodingException {
try {
return Glide.buildStreamModelLoader(model, context)
.getResourceFetcher(model, -1, -1)
.loadData(Priority.NORMAL);
} catch (Exception e) {
throw new BitmapDecodingException(e);
}
}
private static <T> Bitmap createScaledBitmapInto(Context context, T model,
int width, int height)
throws BitmapDecodingException {
Bitmap rough = Downsampler.AT_LEAST
.decode(getInputStreamForModel(context, model),
Glide.get(context).getBitmapPool(),
width, height, DecodeFormat.PREFER_RGB_565);
Resource<Bitmap> resource = BitmapResource
.obtain(rough, Glide.get(context).getBitmapPool());
Resource<Bitmap> result =
new FitCenter(context).transform(resource, width, height);
if (result == null) {
throw new BitmapDecodingException("unable to transform Bitmap");
}
return result.get();
}
public static <T> Bitmap createScaledBitmap(Context context, T model,
float scale) throws BitmapDecodingException {
Pair<Integer, Integer> dimens =
getDimensions(getInputStreamForModel(context, model));
return createScaledBitmapInto(context, model,
(int) (dimens.first * scale), (int) (dimens.second * scale));
}
private static BitmapFactory.Options getImageDimensions(
InputStream inputStream)
throws BitmapDecodingException {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BufferedInputStream fis = new BufferedInputStream(inputStream);
BitmapFactory.decodeStream(fis, null, options);
try {
fis.close();
} catch (IOException e) {
logException(LOG, WARNING, e);
}
if (options.outWidth == -1 || options.outHeight == -1) {
throw new BitmapDecodingException(
"Failed to decode image dimensions: " + options.outWidth +
", " + options.outHeight);
}
return options;
}
private static Pair<Integer, Integer> getDimensions(InputStream inputStream)
throws BitmapDecodingException {
BitmapFactory.Options options = getImageDimensions(inputStream);
return new Pair<>(options.outWidth, options.outHeight);
}
}