mobisocial.musubi.ui.util.EmojiSpannableFactory.java Source code

Java tutorial

Introduction

Here is the source code for mobisocial.musubi.ui.util.EmojiSpannableFactory.java

Source

/*
 * 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();
        }
    }
}