2015-05-06 13:53:55 -07:00
|
|
|
package org.thoughtcrime.securesms.components.emoji;
|
|
|
|
|
2015-05-21 18:27:31 -07:00
|
|
|
import android.annotation.TargetApi;
|
2015-05-06 13:53:55 -07:00
|
|
|
import android.content.Context;
|
|
|
|
import android.graphics.Bitmap;
|
|
|
|
import android.graphics.Canvas;
|
|
|
|
import android.graphics.ColorFilter;
|
|
|
|
import android.graphics.Paint;
|
|
|
|
import android.graphics.PixelFormat;
|
|
|
|
import android.graphics.Rect;
|
|
|
|
import android.graphics.drawable.Drawable;
|
2015-05-14 21:08:37 -07:00
|
|
|
import android.os.AsyncTask;
|
2015-05-21 18:27:31 -07:00
|
|
|
import android.os.Build.VERSION;
|
|
|
|
import android.os.Build.VERSION_CODES;
|
2015-05-06 13:53:55 -07:00
|
|
|
import android.text.Spannable;
|
|
|
|
import android.text.SpannableStringBuilder;
|
|
|
|
import android.util.Log;
|
|
|
|
import android.util.SparseArray;
|
2015-07-10 14:58:05 -07:00
|
|
|
import android.widget.TextView;
|
2015-05-06 13:53:55 -07:00
|
|
|
|
|
|
|
import org.thoughtcrime.securesms.R;
|
|
|
|
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
|
|
|
import org.thoughtcrime.securesms.util.BitmapUtil;
|
2015-05-14 21:08:37 -07:00
|
|
|
import org.thoughtcrime.securesms.util.FutureTaskListener;
|
|
|
|
import org.thoughtcrime.securesms.util.ListenableFutureTask;
|
2015-05-06 13:53:55 -07:00
|
|
|
import org.thoughtcrime.securesms.util.Util;
|
|
|
|
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.io.InputStream;
|
|
|
|
import java.lang.ref.SoftReference;
|
2015-05-14 21:08:37 -07:00
|
|
|
import java.util.concurrent.Callable;
|
2015-05-06 13:53:55 -07:00
|
|
|
import java.util.regex.Matcher;
|
|
|
|
import java.util.regex.Pattern;
|
|
|
|
|
|
|
|
public class EmojiProvider {
|
2015-05-14 21:08:37 -07:00
|
|
|
private static final String TAG = EmojiProvider.class.getSimpleName();
|
|
|
|
private static volatile EmojiProvider instance = null;
|
2015-05-21 18:27:31 -07:00
|
|
|
private static final Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
|
2015-05-06 13:53:55 -07:00
|
|
|
|
|
|
|
private final SparseArray<DrawInfo> offsets = new SparseArray<>();
|
|
|
|
|
|
|
|
@SuppressWarnings("MalformedRegex")
|
|
|
|
// 0x20a0-0x32ff 0x1f00-0x1fff 0xfe4e5-0xfe4ee
|
|
|
|
// |==== misc ====||======== emoticons ========||========= flags ==========|
|
|
|
|
private static final Pattern EMOJI_RANGE = Pattern.compile("[\\u20a0-\\u32ff\\ud83c\\udc00-\\ud83d\\udeff\\udbb9\\udce5-\\udbb9\\udcee]");
|
|
|
|
|
2015-07-06 14:05:18 -07:00
|
|
|
public static final int EMOJI_RAW_HEIGHT = 64;
|
|
|
|
public static final int EMOJI_RAW_WIDTH = 64;
|
|
|
|
public static final int EMOJI_VERT_PAD = 0;
|
|
|
|
public static final int EMOJI_PER_ROW = 32;
|
2015-05-06 13:53:55 -07:00
|
|
|
|
|
|
|
private final Context context;
|
2015-07-10 14:58:05 -07:00
|
|
|
private final float decodeScale;
|
|
|
|
private final float verticalPad;
|
2015-05-06 13:53:55 -07:00
|
|
|
|
|
|
|
public static EmojiProvider getInstance(Context context) {
|
|
|
|
if (instance == null) {
|
|
|
|
synchronized (EmojiProvider.class) {
|
|
|
|
if (instance == null) {
|
|
|
|
instance = new EmojiProvider(context);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return instance;
|
|
|
|
}
|
|
|
|
|
|
|
|
private EmojiProvider(Context context) {
|
2015-05-20 16:16:27 -07:00
|
|
|
this.context = context.getApplicationContext();
|
2015-07-10 14:58:05 -07:00
|
|
|
this.decodeScale = Math.min(1f, context.getResources().getDimension(R.dimen.emoji_drawer_size) / EMOJI_RAW_HEIGHT);
|
|
|
|
this.verticalPad = EMOJI_VERT_PAD * this.decodeScale;
|
2015-05-21 17:29:23 -07:00
|
|
|
for (EmojiPageModel page : EmojiPages.PAGES) {
|
|
|
|
if (page.hasSpriteMap()) {
|
|
|
|
final 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));
|
|
|
|
}
|
2015-05-06 13:53:55 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-07-10 14:58:05 -07:00
|
|
|
public Spannable emojify(CharSequence text, TextView tv) {
|
2015-05-06 13:53:55 -07:00
|
|
|
Matcher matches = EMOJI_RANGE.matcher(text);
|
|
|
|
SpannableStringBuilder builder = new SpannableStringBuilder(text);
|
|
|
|
|
|
|
|
while (matches.find()) {
|
|
|
|
int codePoint = matches.group().codePointAt(0);
|
2015-07-10 14:58:05 -07:00
|
|
|
Drawable drawable = getEmojiDrawable(codePoint);
|
2015-05-06 13:53:55 -07:00
|
|
|
if (drawable != null) {
|
2015-07-10 14:58:05 -07:00
|
|
|
builder.setSpan(new EmojiSpan(drawable, tv), matches.start(), matches.end(),
|
2015-05-06 13:53:55 -07:00
|
|
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return builder;
|
|
|
|
}
|
|
|
|
|
2015-07-10 14:58:05 -07:00
|
|
|
public Drawable getEmojiDrawable(int emojiCode) {
|
|
|
|
return getEmojiDrawable(offsets.get(emojiCode));
|
2015-05-06 13:53:55 -07:00
|
|
|
}
|
|
|
|
|
2015-07-10 14:58:05 -07:00
|
|
|
private Drawable getEmojiDrawable(DrawInfo drawInfo) {
|
2015-05-21 17:29:23 -07:00
|
|
|
if (drawInfo == null) {
|
|
|
|
return null;
|
|
|
|
}
|
2015-05-14 21:08:37 -07:00
|
|
|
|
2015-07-10 14:58:05 -07:00
|
|
|
final EmojiDrawable drawable = new EmojiDrawable(drawInfo, decodeScale);
|
2015-05-14 21:08:37 -07:00
|
|
|
drawInfo.page.get().addListener(new FutureTaskListener<Bitmap>() {
|
|
|
|
@Override public void onSuccess(final Bitmap result) {
|
2015-05-21 18:27:31 -07:00
|
|
|
Util.runOnMain(new Runnable() {
|
2015-05-14 21:08:37 -07:00
|
|
|
@Override public void run() {
|
|
|
|
drawable.setBitmap(result);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override public void onFailure(Throwable error) {
|
|
|
|
Log.w(TAG, error);
|
|
|
|
}
|
|
|
|
});
|
2015-05-06 13:53:55 -07:00
|
|
|
return drawable;
|
|
|
|
}
|
|
|
|
|
2015-05-14 16:36:25 -07:00
|
|
|
public class EmojiDrawable extends Drawable {
|
2015-05-21 18:27:31 -07:00
|
|
|
private final DrawInfo info;
|
|
|
|
private Bitmap bmp;
|
2015-07-10 14:58:05 -07:00
|
|
|
private float intrinsicWidth;
|
|
|
|
private float intrinsicHeight;
|
2015-05-06 13:53:55 -07:00
|
|
|
|
2015-05-14 21:08:37 -07:00
|
|
|
@Override public int getIntrinsicWidth() {
|
2015-07-10 14:58:05 -07:00
|
|
|
return (int)intrinsicWidth;
|
2015-05-14 21:08:37 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override public int getIntrinsicHeight() {
|
2015-07-10 14:58:05 -07:00
|
|
|
return (int)intrinsicHeight;
|
2015-05-14 21:08:37 -07:00
|
|
|
}
|
|
|
|
|
2015-07-10 14:58:05 -07:00
|
|
|
public EmojiDrawable(DrawInfo info, float decodeScale) {
|
|
|
|
this.info = info;
|
|
|
|
this.intrinsicWidth = EMOJI_RAW_WIDTH * decodeScale;
|
|
|
|
this.intrinsicHeight = EMOJI_RAW_HEIGHT * decodeScale;
|
2015-05-06 13:53:55 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void draw(Canvas canvas) {
|
2015-05-21 18:27:31 -07:00
|
|
|
if (bmp == null) {
|
|
|
|
return;
|
|
|
|
}
|
2015-05-06 13:53:55 -07:00
|
|
|
|
2015-05-21 18:27:31 -07:00
|
|
|
final int row = info.index / EMOJI_PER_ROW;
|
|
|
|
final int row_index = info.index % EMOJI_PER_ROW;
|
2015-05-06 13:53:55 -07:00
|
|
|
|
|
|
|
canvas.drawBitmap(bmp,
|
2015-07-10 14:58:05 -07:00
|
|
|
new Rect((int)(row_index * intrinsicWidth),
|
|
|
|
(int)(row * intrinsicHeight + row * verticalPad),
|
|
|
|
(int)((row_index + 1) * intrinsicWidth),
|
|
|
|
(int)((row + 1) * intrinsicHeight + row * verticalPad)),
|
2015-05-21 18:27:31 -07:00
|
|
|
getBounds(),
|
2015-05-06 13:53:55 -07:00
|
|
|
paint);
|
|
|
|
}
|
|
|
|
|
2015-05-21 18:27:31 -07:00
|
|
|
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
|
2015-05-14 21:08:37 -07:00
|
|
|
public void setBitmap(Bitmap bitmap) {
|
|
|
|
Util.assertMainThread();
|
2015-05-21 18:27:31 -07:00
|
|
|
if (VERSION.SDK_INT < VERSION_CODES.HONEYCOMB_MR1 || bmp == null || !bmp.sameAs(bitmap)) {
|
|
|
|
bmp = bitmap;
|
|
|
|
invalidateSelf();
|
|
|
|
}
|
2015-05-14 21:08:37 -07:00
|
|
|
}
|
|
|
|
|
2015-05-06 13:53:55 -07:00
|
|
|
@Override
|
|
|
|
public int getOpacity() {
|
|
|
|
return PixelFormat.TRANSLUCENT;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void setAlpha(int alpha) { }
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void setColorFilter(ColorFilter cf) { }
|
|
|
|
}
|
|
|
|
|
|
|
|
class DrawInfo {
|
2015-05-14 21:08:37 -07:00
|
|
|
EmojiPageBitmap page;
|
|
|
|
int index;
|
2015-05-06 13:53:55 -07:00
|
|
|
|
2015-05-14 21:08:37 -07:00
|
|
|
public DrawInfo(final EmojiPageBitmap page, final int index) {
|
2015-05-06 13:53:55 -07:00
|
|
|
this.page = page;
|
|
|
|
this.index = index;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public String toString() {
|
|
|
|
return "DrawInfo{" +
|
|
|
|
"page=" + page +
|
|
|
|
", index=" + index +
|
|
|
|
'}';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-14 21:08:37 -07:00
|
|
|
private class EmojiPageBitmap {
|
2015-05-21 17:29:23 -07:00
|
|
|
private EmojiPageModel model;
|
2015-05-14 21:08:37 -07:00
|
|
|
private SoftReference<Bitmap> bitmapReference;
|
|
|
|
private ListenableFutureTask<Bitmap> task;
|
|
|
|
|
2015-05-21 17:29:23 -07:00
|
|
|
public EmojiPageBitmap(EmojiPageModel model) {
|
|
|
|
this.model = model;
|
2015-05-14 21:08:37 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2015-05-21 17:29:23 -07:00
|
|
|
Log.w(TAG, "loading page " + model.getSprite());
|
2015-05-14 21:08:37 -07:00
|
|
|
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 {
|
2015-05-21 17:29:23 -07:00
|
|
|
final InputStream measureStream = context.getAssets().open(model.getSprite());
|
|
|
|
final InputStream bitmapStream = context.getAssets().open(model.getSprite());
|
2015-07-10 14:58:05 -07:00
|
|
|
final Bitmap bitmap = BitmapUtil.createScaledBitmap(measureStream, bitmapStream, decodeScale);
|
2015-05-14 21:08:37 -07:00
|
|
|
bitmapReference = new SoftReference<>(bitmap);
|
2015-05-21 17:29:23 -07:00
|
|
|
Log.w(TAG, "onPageLoaded(" + model.getSprite() + ")");
|
2015-05-14 21:08:37 -07:00
|
|
|
return bitmap;
|
|
|
|
} catch (IOException ioe) {
|
|
|
|
Log.w(TAG, ioe);
|
|
|
|
throw ioe;
|
|
|
|
} catch (BitmapDecodingException bde) {
|
2015-05-21 17:29:23 -07:00
|
|
|
Log.w(TAG, "page " + model + " failed.");
|
2015-05-14 21:08:37 -07:00
|
|
|
Log.w(TAG, bde);
|
|
|
|
throw new AssertionError("emoji sprite asset is corrupted or android decoding is broken");
|
|
|
|
}
|
|
|
|
}
|
2015-05-21 18:27:31 -07:00
|
|
|
|
|
|
|
@Override public String toString() {
|
2015-05-21 17:29:23 -07:00
|
|
|
return model.getSprite();
|
2015-05-21 18:27:31 -07:00
|
|
|
}
|
2015-05-06 13:53:55 -07:00
|
|
|
}
|
|
|
|
}
|