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.graphics.drawable.Drawable.Callback;
|
|
|
|
import android.os.AsyncTask;
|
2015-05-21 18:27:31 -07:00
|
|
|
import android.os.Build.VERSION;
|
|
|
|
import android.os.Build.VERSION_CODES;
|
|
|
|
import android.support.annotation.Nullable;
|
2015-05-06 13:53:55 -07:00
|
|
|
import android.text.Spannable;
|
|
|
|
import android.text.SpannableStringBuilder;
|
|
|
|
import android.util.Log;
|
|
|
|
import android.util.SparseArray;
|
|
|
|
|
|
|
|
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.ResUtil;
|
|
|
|
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-05-18 14:33:11 -07:00
|
|
|
public static final double EMOJI_FULL = 1.00;
|
|
|
|
public static final double EMOJI_SMALL = 0.50;
|
2015-05-29 15:56:00 -07:00
|
|
|
public static final int EMOJI_RAW_HEIGHT = 96;
|
|
|
|
public static final int EMOJI_RAW_WIDTH = 102;
|
|
|
|
public static final int EMOJI_VERT_PAD = 6;
|
2015-05-18 14:33:11 -07:00
|
|
|
public static final int EMOJI_PER_ROW = 15;
|
2015-05-06 13:53:55 -07:00
|
|
|
|
|
|
|
private final Context context;
|
2015-05-18 14:33:11 -07:00
|
|
|
private final double drawWidth;
|
|
|
|
private final double drawHeight;
|
2015-05-20 16:16:27 -07:00
|
|
|
private final double 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-14 21:08:37 -07:00
|
|
|
int[] pages = ResUtil.getResourceIds(context, R.array.emoji_categories);
|
|
|
|
|
2015-05-20 16:16:27 -07:00
|
|
|
this.context = context.getApplicationContext();
|
2015-06-01 14:50:06 -07:00
|
|
|
this.drawHeight = Math.min(context.getResources().getDimension(R.dimen.emoji_drawer_size), EMOJI_RAW_HEIGHT);
|
|
|
|
double drawScale = drawHeight / EMOJI_RAW_HEIGHT;
|
|
|
|
this.drawWidth = EMOJI_RAW_WIDTH * drawScale;
|
|
|
|
this.verticalPad = EMOJI_VERT_PAD * drawScale;
|
2015-05-18 14:33:11 -07:00
|
|
|
Log.w(TAG, "draw size: " + drawWidth + "x" + drawHeight);
|
2015-05-06 13:53:55 -07:00
|
|
|
for (int i = 0; i < pages.length; i++) {
|
2015-05-14 21:08:37 -07:00
|
|
|
final EmojiPageBitmap page = new EmojiPageBitmap(i);
|
|
|
|
final int[] codePoints = context.getResources().getIntArray(pages[i]);
|
|
|
|
for (int j = 0; j < codePoints.length; j++) {
|
|
|
|
offsets.put(codePoints[j], new DrawInfo(page, j));
|
2015-05-06 13:53:55 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-14 21:08:37 -07:00
|
|
|
public CharSequence emojify(CharSequence text, double size, Callback callback) {
|
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-05-14 21:08:37 -07:00
|
|
|
Drawable drawable = getEmojiDrawable(codePoint, size);
|
2015-05-06 13:53:55 -07:00
|
|
|
if (drawable != null) {
|
2015-05-14 21:08:37 -07:00
|
|
|
builder.setSpan(new InvalidatingDrawableSpan(drawable, callback), matches.start(), matches.end(),
|
2015-05-06 13:53:55 -07:00
|
|
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return builder;
|
|
|
|
}
|
|
|
|
|
2015-05-14 21:08:37 -07:00
|
|
|
public Drawable getEmojiDrawable(int emojiCode, double size) {
|
|
|
|
return getEmojiDrawable(offsets.get(emojiCode), size);
|
2015-05-06 13:53:55 -07:00
|
|
|
}
|
|
|
|
|
2015-05-14 21:08:37 -07:00
|
|
|
private Drawable getEmojiDrawable(DrawInfo drawInfo, double size) {
|
|
|
|
if (drawInfo == null) return null;
|
|
|
|
|
2015-05-18 14:33:11 -07:00
|
|
|
final EmojiDrawable drawable = new EmojiDrawable(drawInfo, drawWidth, drawHeight);
|
|
|
|
drawable.setBounds(0, 0, (int)(drawWidth * size), (int)(drawHeight * size));
|
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 final double width;
|
|
|
|
private final double height;
|
|
|
|
private Bitmap bmp;
|
2015-05-06 13:53:55 -07:00
|
|
|
|
2015-05-14 21:08:37 -07:00
|
|
|
@Override public int getIntrinsicWidth() {
|
2015-05-18 14:33:11 -07:00
|
|
|
return (int)width;
|
2015-05-14 21:08:37 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override public int getIntrinsicHeight() {
|
2015-05-18 14:33:11 -07:00
|
|
|
return (int)height;
|
2015-05-14 21:08:37 -07:00
|
|
|
}
|
|
|
|
|
2015-05-18 14:33:11 -07:00
|
|
|
public EmojiDrawable(DrawInfo info, double width, double height) {
|
2015-05-21 18:27:31 -07:00
|
|
|
this.info = info;
|
2015-05-18 14:33:11 -07:00
|
|
|
this.width = width;
|
|
|
|
this.height = height;
|
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) {
|
|
|
|
Log.w(TAG, "no-op draw(" + info.page + ", " + info.index + ")");
|
|
|
|
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-05-18 14:33:11 -07:00
|
|
|
new Rect((int)(row_index * width),
|
2015-05-20 16:16:27 -07:00
|
|
|
(int)(row * height + row * verticalPad),
|
2015-05-18 14:33:11 -07:00
|
|
|
(int)((row_index + 1) * width),
|
2015-05-20 16:16:27 -07:00
|
|
|
(int)((row + 1) * height + 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
|
|
|
Log.w(TAG, "setBitmap(" + info.page + ", " + info.index + ")");
|
|
|
|
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 {
|
|
|
|
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 {
|
2015-05-18 14:33:11 -07:00
|
|
|
final String file = "emoji-" + page + ".png";
|
2015-05-14 21:08:37 -07:00
|
|
|
final InputStream measureStream = context.getAssets().open(file);
|
2015-05-18 14:33:11 -07:00
|
|
|
final InputStream bitmapStream = context.getAssets().open(file);
|
|
|
|
final Bitmap bitmap = BitmapUtil.createScaledBitmap(measureStream, bitmapStream, (float) drawHeight / (float) EMOJI_RAW_HEIGHT);
|
2015-05-14 21:08:37 -07:00
|
|
|
bitmapReference = new SoftReference<>(bitmap);
|
|
|
|
Log.w(TAG, "onPageLoaded(" + page + ")");
|
|
|
|
return bitmap;
|
|
|
|
} catch (IOException ioe) {
|
|
|
|
Log.w(TAG, ioe);
|
|
|
|
throw ioe;
|
|
|
|
} catch (BitmapDecodingException bde) {
|
2015-05-29 15:56:00 -07:00
|
|
|
Log.w(TAG, "page " + page + " 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() {
|
|
|
|
return Integer.toString(page);
|
|
|
|
}
|
2015-05-06 13:53:55 -07:00
|
|
|
}
|
|
|
|
}
|