SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/util/EmojiDecoder.java

321 lines
10 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package jp.juggler.subwaytooter.util;
import android.content.Context;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import java.util.ArrayList;
import java.util.regex.Pattern;
import jp.juggler.subwaytooter.App1;
import jp.juggler.subwaytooter.R;
import jp.juggler.subwaytooter.api.entity.CustomEmoji;
import jp.juggler.subwaytooter.api.entity.NicoProfileEmoji;
import jp.juggler.subwaytooter.table.HighlightWord;
@SuppressWarnings("WeakerAccess")
public class EmojiDecoder {
private static class DecodeEnv {
@NonNull final Context context;
@NonNull final SpannableStringBuilder sb = new SpannableStringBuilder();
@NonNull final DecodeOptions options;
int normal_char_start = -1;
DecodeEnv( @NonNull Context context ,@NonNull final DecodeOptions options){
this.context = context;
this.options = options;
}
void closeNormalText(){
if( normal_char_start != -1 ){
int end = sb.length();
applyHighlight(normal_char_start,end);
normal_char_start = -1;
}
}
private void applyHighlight( int start , int end ){
if( options.highlight_trie != null ){
ArrayList<WordTrieTree.Match > list = options.highlight_trie.matchList( sb,start,end );
if( list != null ){
for( WordTrieTree.Match range : list ){
HighlightWord word = HighlightWord.load( range.word );
if( word !=null ){
sb.setSpan( new HighlightSpan( word.color_fg,word.color_bg ), range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
if( word.sound_type != HighlightWord.SOUND_TYPE_NONE ){
options.highlight_sound = word;
}
}
}
}
}
}
void addUnicodeString( String s ){
int i = 0;
int end = s.length();
while( i < end ){
int remain = end - i;
String emoji = null;
Integer image_id = null;
for( int j = EmojiMap201709.utf16_max_length ; j > 0 ; -- j ){
if( j > remain ) continue;
String check = s.substring( i, i + j );
image_id = EmojiMap201709.sUTF16ToImageId.get( check );
if( image_id != null ){
if( j < remain && s.charAt( i + j ) == 0xFE0E ){
// 絵文字バリエーション・シーケンスEVSのU+FE0EVS-15が直後にある場合
// その文字を絵文字化しない
emoji = s.substring( i, i + j + 1 );
image_id = 0;
}else{
emoji = check;
}
break;
}
}
if( image_id != null ){
if( image_id == 0 ){
// 絵文字バリエーション・シーケンスEVSのU+FE0EVS-15が直後にある場合
// その文字を絵文字化しない
if( normal_char_start == - 1 ){
normal_char_start = sb.length();
}
sb.append( emoji );
}else{
addImageSpan( emoji, image_id );
}
i += emoji.length();
continue;
}
if( normal_char_start == - 1 ){
normal_char_start = sb.length();
}
int length = Character.charCount( s.codePointAt( i ) );
if( length == 1 ){
sb.append( s.charAt( i ) );
++ i;
}else{
sb.append( s.substring( i, i + length ) );
i += length;
}
}
}
void addImageSpan( String text, @DrawableRes int res_id ){
closeNormalText();
int start = sb.length();
sb.append( text );
int end = sb.length();
sb.setSpan( new EmojiImageSpan( context, res_id ), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
}
void addNetworkEmojiSpan( String text, @NonNull String url ){
closeNormalText();
int start = sb.length();
sb.append( text );
int end = sb.length();
sb.setSpan( new NetworkEmojiSpan( url ), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
}
}
public static boolean isShortCodeCharacter( int cp ){
return ( 'A' <= cp && cp <= 'Z' )
|| ( 'a' <= cp && cp <= 'z' )
|| ( '0' <= cp && cp <= '9' )
|| cp == '-'
|| cp == '+'
|| cp == '_'
|| cp == '@'
;
}
interface ShortCodeSplitterCallback {
void onString( @NonNull String part );
void onShortCode( @NonNull String part, @NonNull String name );
}
static void splitShortCode(
@NonNull String s
, @SuppressWarnings("SameParameterValue") int start
, int end
, @NonNull ShortCodeSplitterCallback callback
){
int i = start;
while( i < end ){
// 絵文字パターンの開始位置を探索する
start = i;
while( i < end ){
int c = s.codePointAt( i );
int width = Character.charCount( c );
if( c == ':' ){
if( App1.allow_non_space_before_emoji_shortcode ){
// アプリ設定により、: の手前に空白を要求しない
break;
}else if( i + width < end && s.codePointAt( i + width ) == '@' ){
// フレニコのプロフ絵文字 :@who: は手前の空白を要求しない
break;
}else if( i == 0 || CharacterGroup.isWhitespace( s.codePointBefore( i ) ) ){
// ショートコードの手前は始端か改行か空白文字でないとならない
// 空白文字の判定はサーバサイドのそれにあわせる
break;
}
// shortcodeの開始とみなせないケースだった
}
i += width;
}
if( i > start ){
callback.onString( s.substring( start, i ) );
}
if( i >= end ) break;
start = i++; // start=コロンの位置 i=その次の位置
int emoji_end = - 1;
while( i < end ){
int c = s.codePointAt( i );
if( c == ':' ){
emoji_end = i;
break;
}
if( ! isShortCodeCharacter( c ) ){
break;
}
i += Character.charCount( c );
}
// 絵文字がみつからなかったら、startの位置のコロンだけを処理して残りは次のループで処理する
if( emoji_end == - 1 || emoji_end - start < 3 ){
callback.onString( ":" );
i = start + 1;
continue;
}
callback.onShortCode(
s.substring( start, emoji_end + 1 ) // ":shortcode:"
, s.substring( start + 1, emoji_end ) // "shortcode"
);
i = emoji_end + 1;// コロンの次の位置
}
}
private static final Pattern reNicoru = Pattern.compile( "\\Anicoru\\d*\\z", Pattern.CASE_INSENSITIVE );
private static final Pattern reHohoemi = Pattern.compile( "\\Ahohoemi\\d*\\z", Pattern.CASE_INSENSITIVE );
public static Spannable decodeEmoji(
@NonNull final Context context
, @NonNull final String s
, @NonNull DecodeOptions options
){
final DecodeEnv decode_env = new DecodeEnv( context ,options);
final CustomEmoji.Map custom_map = options.customEmojiMap;
final NicoProfileEmoji.Map profile_emojis = options.profile_emojis;
splitShortCode( s, 0, s.length(), new ShortCodeSplitterCallback() {
@Override public void onString( @NonNull String part ){
decode_env.addUnicodeString( part );
}
@Override public void onShortCode( @NonNull String part, @NonNull String name ){
EmojiMap201709.EmojiInfo info = EmojiMap201709.sShortNameToImageId.get( name.toLowerCase().replace( '-', '_' ) );
if( info != null ){
decode_env.addImageSpan( part, info.image_id );
return;
}
// part=":@name:" name="@name"
if( name.length() >= 2 && name.charAt( 0 ) == '@' ){
if( profile_emojis != null ){
NicoProfileEmoji emoji = profile_emojis.get( name );
if( emoji == null ) emoji = profile_emojis.get( name.substring( 1 ) );
if( emoji != null ){
decode_env.addNetworkEmojiSpan( part, emoji.url );
}
}
return;
}
{
CustomEmoji emoji = ( custom_map == null ? null : custom_map.get( name ) );
if( emoji != null ){
String url = ( App1.disable_emoji_animation && ! TextUtils.isEmpty( emoji.static_url ) ) ? emoji.static_url : emoji.url;
decode_env.addNetworkEmojiSpan( part, url );
return;
}
}
if( reHohoemi.matcher( name ).find() ){
decode_env.addImageSpan( part, R.drawable.emoji_hohoemi );
}else if( reNicoru.matcher( name ).find() ){
decode_env.addImageSpan( part, R.drawable.emoji_nicoru );
}else{
decode_env.addUnicodeString( part );
}
}
} );
decode_env.closeNormalText();
return decode_env.sb;
}
// 投稿などの際、表示は不要だがショートコード=>Unicodeの解決を行いたい場合がある
// カスタム絵文字の変換も行わない
public static String decodeShortCode( @NonNull final String s ){
final StringBuilder sb = new StringBuilder();
splitShortCode( s, 0, s.length(), new ShortCodeSplitterCallback() {
@Override public void onString( @NonNull String part ){
sb.append( part );
}
@Override public void onShortCode( @NonNull String part, @NonNull String name ){
EmojiMap201709.EmojiInfo info = EmojiMap201709.sShortNameToImageId.get( name.toLowerCase().replace( '-', '_' ) );
sb.append( info != null ? info.unified : part );
}
} );
return sb.toString();
}
// 入力補完用。絵文字ショートコード一覧を部分一致で絞り込む
static ArrayList< CharSequence > searchShortCode( Context context, String prefix, int limit ){
ArrayList< CharSequence > dst = new ArrayList<>();
for( String shortCode : EmojiMap201709.sShortNameList ){
if( dst.size() >= limit ) break;
if( ! shortCode.contains( prefix ) ) continue;
EmojiMap201709.EmojiInfo info = EmojiMap201709.sShortNameToImageId.get( shortCode );
if( info == null ) continue;
SpannableStringBuilder sb = new SpannableStringBuilder();
sb.append( ' ' );
int start = 0;
int end = sb.length();
sb.setSpan( new EmojiImageSpan( context, info.image_id ), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
sb.append( ' ' );
sb.append( ':' );
sb.append( shortCode );
sb.append( ':' );
dst.add( sb );
}
return dst;
}
}