/* * Copyright 2012 The Stanford MobiSocial Laboratory * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package mobisocial.musubi.ui.util; import java.io.IOException; import java.io.InputStream; import java.lang.ref.SoftReference; import java.util.HashMap; import java.util.Map; import org.apache.commons.io.IOUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Paint.FontMetricsInt; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.support.v4.util.LruCache; import android.text.Spannable; import android.text.SpannableString; import android.text.style.DynamicDrawableSpan; import android.util.Log; public class EmojiSpannableFactory extends Spannable.Factory { private static final String TAG = "StickerFactory"; //final int MAX_RECYCLED_SPANS = 50; final int MAX_CACHED_BITMAPS = 75; final Context mContext; static SoftReference<EmojiSpannableFactory> sSpannableFactory; Bitmap mEmojiBitmap; Map<Long, Integer> mEmojiMap; final StickerCache mStickerCache = new StickerCache(75); private boolean mEmojiPrepared = false; public static EmojiSpannableFactory getInstance(Context context) { if (sSpannableFactory != null) { EmojiSpannableFactory f = sSpannableFactory.get(); if (f != null) { return f; } } EmojiSpannableFactory f = new EmojiSpannableFactory(context); sSpannableFactory = new SoftReference<EmojiSpannableFactory>(f); return f; } private EmojiSpannableFactory(Context context) { mContext = context.getApplicationContext(); new PrepareEmojiAsyncTask().execute(); } @Override public Spannable newSpannable(CharSequence source) { if (source == null) { return null; } SpannableString span = new SpannableString(source); updateSpannable(span); return span; } public void updateSpannable(Spannable span) { Spannable source = span; for (int i = 0; i < source.length(); i++) { char high = source.charAt(i); if (high <= 127) { // fast exit ascii continue; } // Block until we're initialized waitForEmoji(); long codePoint = high; if (Character.isHighSurrogate(high)) { char low = source.charAt(++i); codePoint = Character.toCodePoint(high, low); if (Character.isSurrogatePair(high, low)) { // from BMP if (!mEmojiMap.containsKey(codePoint)) { if (i >= source.length() - 2) { continue; } high = source.charAt(++i); if (!Character.isHighSurrogate(high)) { Log.w(TAG, "bad unicode character? " + high); continue; } low = source.charAt(++i); if (!Character.isSurrogatePair(high, low)) { Log.d(TAG, "Bogus unicode surrogate " + high + ", " + low); continue; } int codePoint2 = Character.toCodePoint(high, low); //String label = String.format("U+%X U+%X", codePoint, codePoint2); codePoint = ((long)codePoint << 16) | codePoint2; } } else { Log.d(TAG, "Bogus unicode"); } } if (mEmojiMap.containsKey(codePoint)) { Bitmap b = mStickerCache.get(codePoint); if (b != null) { DynamicDrawableSpan im = createStickerSpan(b); span.setSpan(im, i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } else { Log.d(TAG, "failed to decode bitmap for codepoints: " + codePoint); } } } } private StickerSpan createStickerSpan(Bitmap b) { return new StickerSpan(mContext, b); } private Long getEmojiCode(String label) { String[] parts = label.split(" "); if (parts.length == 0 || parts.length > 2) { return null; } long code = 0; if (parts.length == 2) { code = (Long.parseLong(parts[0].replace("U+", ""), 16) << 16); code |= Long.parseLong(parts[1].replace("U+", ""), 16); } else { code = (Long.parseLong(parts[0].replace("U+", ""), 16)); } return code; } /** * Blocks until the factory has been populated with emoji. */ private void waitForEmoji() { if (!mEmojiPrepared) { synchronized (this) { while (!mEmojiPrepared) { try { EmojiSpannableFactory.this.wait(); } catch (InterruptedException e) {} } } } } class PrepareEmojiAsyncTask extends AsyncTask<Void, Void, Void> { @Override protected Void doInBackground(Void... params) { try { InputStream inStream = mContext.getAssets().open("emoji/stickers.json"); String jsonSrc = IOUtils.toString(inStream); JSONObject json; int unicodeIndex = 0; int sbIndex = 0; try { mEmojiMap = new HashMap<Long, Integer>(); json = new JSONObject(jsonSrc); JSONArray list = json.getJSONArray("sheets"); int len = list.length(); for (int i = 0; i < len; i++) { JSONArray stickers = list.getJSONObject(i).getJSONArray("unicode"); int stickerCount = stickers.length(); for (int j = 0; j < stickerCount; j++) { Long label = getEmojiCode(stickers.getString(j)); if (label == null) { Log.e(TAG, "Bad emoji " + stickers.getString(j)); unicodeIndex++; continue; } mEmojiMap.put(label, unicodeIndex++); } JSONArray sbStickers = list.getJSONObject(i).getJSONArray("sb"); if (sbStickers != null && sbStickers.length() == stickers.length()) { for (int j = 0; j < stickerCount; j++) { try { Integer label = Integer.parseInt(sbStickers.getString(j), 16); if (!mEmojiMap.containsKey(label)) { mEmojiMap.put(label.longValue(), sbIndex++); } } catch (NumberFormatException e) { Log.e(TAG, "Bad sb code", e); sbIndex++; } } } } } catch (JSONException e) { throw new IOException(e); } } catch (IOException e) { Log.e(TAG, "Error loading emoji", e); mEmojiMap = null; } synchronized (EmojiSpannableFactory.this) { EmojiSpannableFactory.this.mEmojiPrepared = true; EmojiSpannableFactory.this.notify(); } return null; } } class StickerCache extends LruCache<Long, Bitmap> { public StickerCache(int capacity) { super(capacity); } @Override protected Bitmap create(Long label) { try { Map<Long, Integer> emojiMap = mEmojiMap; Integer pos = emojiMap.get(label); if (pos == null) { return null; } // TODO: parameters in json int emojiSize = 30; int emojiPerRow = 10; int leftPos = (pos % emojiPerRow) * emojiSize; int topPos = (pos / emojiPerRow) * emojiSize; Bitmap sheet = getEmojiBitmap(); Bitmap sticker = Bitmap.createBitmap(sheet, leftPos, topPos, emojiSize, emojiSize); return sticker; } catch (IOException e) { Log.e(TAG, "asset error", e); return null; } } Bitmap getEmojiBitmap() throws IOException { if (mEmojiBitmap != null) { return mEmojiBitmap; } InputStream emoStream = mContext.getAssets().open("emoji/stickers.png"); Bitmap b = BitmapFactory.decodeStream(emoStream); mEmojiBitmap = b; return b; } } static class StickerSpan extends DynamicDrawableSpan { final FontMetricsInt mFontMetricsInt; Drawable mDrawable; public StickerSpan(Context context, Bitmap bitmap) { super(DynamicDrawableSpan.ALIGN_BASELINE); setBitmap(context, bitmap); mFontMetricsInt = new FontMetricsInt(); } public void setBitmap(Context context, Bitmap bitmap) { mDrawable = new BitmapDrawable(context.getResources(), bitmap); int width = mDrawable.getIntrinsicWidth(); int height = mDrawable.getIntrinsicHeight(); mDrawable.setBounds(0, 0, width > 0 ? width : 0, height > 0 ? height : 0); } @Override public Drawable getDrawable() { return mDrawable; } @Override public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) { Drawable b = mDrawable; canvas.save(); int transY = bottom - b.getBounds().bottom; paint.getFontMetricsInt(mFontMetricsInt); if (mVerticalAlignment == ALIGN_BASELINE) { int textLength = text.length(); for (int i = 0; i < textLength; i++) { if (Character.isLetterOrDigit(text.charAt(i))) { transY -= mFontMetricsInt.descent; break; } } } canvas.translate(x, transY); b.draw(canvas); canvas.restore(); } } }