|
|
|
@ -8,18 +8,20 @@ import android.graphics.Paint;
|
|
|
|
|
import android.graphics.PixelFormat;
|
|
|
|
|
import android.graphics.Rect;
|
|
|
|
|
import android.graphics.drawable.Drawable;
|
|
|
|
|
import android.graphics.drawable.Drawable.Callback;
|
|
|
|
|
import android.os.AsyncTask;
|
|
|
|
|
import android.os.Handler;
|
|
|
|
|
import android.os.Looper;
|
|
|
|
|
import android.text.Spannable;
|
|
|
|
|
import android.text.SpannableStringBuilder;
|
|
|
|
|
import android.text.style.ImageSpan;
|
|
|
|
|
import android.util.Log;
|
|
|
|
|
import android.util.SparseArray;
|
|
|
|
|
import android.view.View;
|
|
|
|
|
|
|
|
|
|
import org.thoughtcrime.securesms.R;
|
|
|
|
|
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
|
|
|
|
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.Util;
|
|
|
|
|
|
|
|
|
@ -27,17 +29,14 @@ import java.io.IOException;
|
|
|
|
|
import java.io.InputStream;
|
|
|
|
|
import java.lang.ref.SoftReference;
|
|
|
|
|
import java.util.Arrays;
|
|
|
|
|
import java.util.concurrent.ExecutorService;
|
|
|
|
|
import java.util.concurrent.Callable;
|
|
|
|
|
import java.util.regex.Matcher;
|
|
|
|
|
import java.util.regex.Pattern;
|
|
|
|
|
|
|
|
|
|
public class EmojiProvider {
|
|
|
|
|
private static final String TAG = EmojiProvider.class.getSimpleName();
|
|
|
|
|
private static final ExecutorService executor = Util.newSingleThreadedLifoExecutor();
|
|
|
|
|
private static volatile EmojiProvider instance = null;
|
|
|
|
|
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());
|
|
|
|
|
private static final String TAG = EmojiProvider.class.getSimpleName();
|
|
|
|
|
private static volatile EmojiProvider instance = null;
|
|
|
|
|
private static final Paint paint = new Paint();
|
|
|
|
|
static { paint.setFilterBitmap(true); }
|
|
|
|
|
|
|
|
|
|
private final SparseArray<DrawInfo> offsets = new SparseArray<>();
|
|
|
|
@ -55,7 +54,7 @@ public class EmojiProvider {
|
|
|
|
|
|
|
|
|
|
private final Context context;
|
|
|
|
|
private final int bigDrawSize;
|
|
|
|
|
private final int[] pages;
|
|
|
|
|
private final Handler handler = new Handler(Looper.getMainLooper());
|
|
|
|
|
|
|
|
|
|
public static EmojiProvider getInstance(Context context) {
|
|
|
|
|
if (instance == null) {
|
|
|
|
@ -69,72 +68,28 @@ public class EmojiProvider {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.pages = ResUtil.getResourceIds(context, R.array.emoji_categories);
|
|
|
|
|
for (int i = 0; i < pages.length; i++) {
|
|
|
|
|
final int[] page = context.getResources().getIntArray(pages[i]);
|
|
|
|
|
for (int j = 0; j < page.length; j++) {
|
|
|
|
|
offsets.put(page[j], new DrawInfo(i, j));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void preloadPage(final int page, final PageLoadedListener pageLoadListener) {
|
|
|
|
|
executor.submit(new Runnable() {
|
|
|
|
|
@Override
|
|
|
|
|
public void run() {
|
|
|
|
|
try {
|
|
|
|
|
loadPage(page);
|
|
|
|
|
if (pageLoadListener != null) {
|
|
|
|
|
pageLoadListener.onPageLoaded();
|
|
|
|
|
}
|
|
|
|
|
} catch (IOException ioe) {
|
|
|
|
|
Log.w(TAG, ioe);
|
|
|
|
|
}
|
|
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
public CharSequence emojify(CharSequence text, double size, Callback callback) {
|
|
|
|
|
Matcher matches = EMOJI_RANGE.matcher(text);
|
|
|
|
|
SpannableStringBuilder builder = new SpannableStringBuilder(text);
|
|
|
|
|
|
|
|
|
|
while (matches.find()) {
|
|
|
|
|
int codePoint = matches.group().codePointAt(0);
|
|
|
|
|
Drawable drawable = getEmojiDrawable(codePoint, size, pageLoadedListener);
|
|
|
|
|
Drawable drawable = getEmojiDrawable(codePoint, size);
|
|
|
|
|
if (drawable != null) {
|
|
|
|
|
ImageSpan imageSpan = new ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM);
|
|
|
|
|
char[] chars = new char[matches.end() - matches.start()];
|
|
|
|
|
Arrays.fill(chars, ' ');
|
|
|
|
|
builder.setSpan(imageSpan, matches.start(), matches.end(),
|
|
|
|
|
builder.setSpan(new InvalidatingDrawableSpan(drawable, callback), matches.start(), matches.end(),
|
|
|
|
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -142,51 +97,52 @@ public class EmojiProvider {
|
|
|
|
|
return builder;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Drawable getEmojiDrawable(int emojiCode, double size, PageLoadedListener pageLoadedListener) {
|
|
|
|
|
return getEmojiDrawable(offsets.get(emojiCode), size, pageLoadedListener);
|
|
|
|
|
public Drawable getEmojiDrawable(int emojiCode, double size) {
|
|
|
|
|
return getEmojiDrawable(offsets.get(emojiCode), size);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Drawable getEmojiDrawable(DrawInfo drawInfo, double size, PageLoadedListener pageLoadedListener) {
|
|
|
|
|
if (drawInfo == null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
final Drawable drawable = new EmojiDrawable(drawInfo, bigDrawSize);
|
|
|
|
|
private Drawable getEmojiDrawable(DrawInfo drawInfo, double size) {
|
|
|
|
|
if (drawInfo == null) return null;
|
|
|
|
|
|
|
|
|
|
final EmojiDrawable drawable = new EmojiDrawable(drawInfo, bigDrawSize);
|
|
|
|
|
drawable.setBounds(0, 0, (int)((double)bigDrawSize * size), (int)((double)bigDrawSize * size));
|
|
|
|
|
if (bitmaps.get(drawInfo.page) == null || bitmaps.get(drawInfo.page).get() == null) {
|
|
|
|
|
preloadPage(drawInfo.page, pageLoadedListener);
|
|
|
|
|
}
|
|
|
|
|
drawInfo.page.get().addListener(new FutureTaskListener<Bitmap>() {
|
|
|
|
|
@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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class EmojiDrawable extends Drawable {
|
|
|
|
|
private final int index;
|
|
|
|
|
private final int page;
|
|
|
|
|
private final int emojiSize;
|
|
|
|
|
private Bitmap bmp;
|
|
|
|
|
|
|
|
|
|
@Override public int getIntrinsicWidth() {
|
|
|
|
|
return emojiSize;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override public int getIntrinsicHeight() {
|
|
|
|
|
return emojiSize;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public EmojiDrawable(DrawInfo info, int emojiSize) {
|
|
|
|
|
this.index = info.index;
|
|
|
|
|
this.page = info.page;
|
|
|
|
|
this.emojiSize = emojiSize;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void draw(Canvas canvas) {
|
|
|
|
|
if (bitmaps.get(page) == null || bitmaps.get(page).get() == null) {
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
if (bmp == null) return;
|
|
|
|
|
|
|
|
|
|
Rect b = copyBounds();
|
|
|
|
|
|
|
|
|
@ -202,6 +158,12 @@ public class EmojiProvider {
|
|
|
|
|
paint);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setBitmap(Bitmap bitmap) {
|
|
|
|
|
Util.assertMainThread();
|
|
|
|
|
bmp = bitmap;
|
|
|
|
|
invalidateSelf();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public int getOpacity() {
|
|
|
|
|
return PixelFormat.TRANSLUCENT;
|
|
|
|
@ -212,39 +174,13 @@ public class EmojiProvider {
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
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 {
|
|
|
|
|
int page;
|
|
|
|
|
int index;
|
|
|
|
|
EmojiPageBitmap page;
|
|
|
|
|
int index;
|
|
|
|
|
|
|
|
|
|
public DrawInfo(final int page, final int index) {
|
|
|
|
|
public DrawInfo(final EmojiPageBitmap page, final int index) {
|
|
|
|
|
this.page = page;
|
|
|
|
|
this.index = index;
|
|
|
|
|
}
|
|
|
|
@ -258,7 +194,67 @@ public class EmojiProvider {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface PageLoadedListener {
|
|
|
|
|
void onPageLoaded();
|
|
|
|
|
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 {
|
|
|
|
|
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");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|