- (試験実装)マストドンのカスタム絵文字の表示
This commit is contained in:
tateisu 2017-09-19 18:35:53 +09:00
parent c89f14b737
commit b0c1207f1a
25 changed files with 606 additions and 82 deletions

View File

@ -6,6 +6,7 @@
<w>dont</w>
<w>emoji</w>
<w>emojione</w>
<w>emojis</w>
<w>enquete</w>
<w>enty</w>
<w>favourited</w>
@ -26,6 +27,7 @@
<w>reblogs</w>
<w>sephiroth</w>
<w>sharedpref</w>
<w>shortname</w>
<w>simeji</w>
<w>styler</w>
<w>subwaytooter</w>

View File

@ -9,8 +9,8 @@ android {
applicationId "jp.juggler.subwaytooter"
minSdkVersion 21
targetSdkVersion 26
versionCode 155
versionName "1.5.5"
versionCode 156
versionName "1.5.6"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

View File

@ -51,6 +51,7 @@ package jp.juggler.subwaytooter;
import jp.juggler.subwaytooter.dialog.ActionsDialog;
import jp.juggler.subwaytooter.table.AcctColor;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.DecodeOptions;
import jp.juggler.subwaytooter.util.Emojione;
import jp.juggler.subwaytooter.util.HTMLDecoder;
import jp.juggler.subwaytooter.util.LogCategory;
@ -751,12 +752,13 @@ public class ActAccountSetting extends AppCompatActivity
ivProfileAvatar.setImageUrl( App1.pref, 16f, src.avatar_static, src.avatar );
ivProfileHeader.setImageUrl( App1.pref, 0f, src.header_static, src.header );
etDisplayName.setText( Emojione.decodeEmoji( this, src.display_name == null ? "" : src.display_name ) );
etDisplayName.setText( Emojione.decodeEmoji( this, src.display_name == null ? "" : src.display_name ,null) );
if( src.source != null && src.source.note != null ){
etNote.setText( Emojione.decodeEmoji( this, src.source.note ) );
etNote.setText( Emojione.decodeEmoji( this, src.source.note ,null) );
}else if( src.note != null ){
etNote.setText( HTMLDecoder.decodeHTML( ActAccountSetting.this, account, src.note, false, false, null ,null) );
etNote.setText( new DecodeOptions().decodeHTML(ActAccountSetting.this, account, src.note) );
}else{
etNote.setText( "" );
}

View File

@ -68,6 +68,7 @@ import jp.juggler.subwaytooter.table.AcctColor;
import jp.juggler.subwaytooter.table.PostDraft;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.dialog.ActionsDialog;
import jp.juggler.subwaytooter.util.DecodeOptions;
import jp.juggler.subwaytooter.util.HTMLDecoder;
import jp.juggler.subwaytooter.util.LinkClickContext;
import jp.juggler.subwaytooter.util.LogCategory;
@ -1553,7 +1554,10 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener,
llReply.setVisibility( View.GONE );
}else{
llReply.setVisibility( View.VISIBLE );
tvReplyTo.setText( HTMLDecoder.decodeHTML( ActPost.this, account, in_reply_to_text, true, true, null, null ) );
tvReplyTo.setText( new DecodeOptions()
.setShort( true )
.setDecodeEmoji( true )
.decodeHTML( ActPost.this, account, in_reply_to_text));
ivReply.setImageUrl( pref, 16f, in_reply_to_image );
}
}
@ -1960,7 +1964,7 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener,
return null;
}
};
CharSequence sv = HTMLDecoder.decodeHTML( ActPost.this, lcc, text, false, false, null, null );
CharSequence sv = new DecodeOptions().decodeHTML( ActPost.this, lcc, text);
tvText.setText( sv );
tvText.setMovementMethod( LinkMovementMethod.getInstance() );

View File

@ -23,6 +23,7 @@ import jp.juggler.subwaytooter.api_msp.entity.MSPAccount;
import jp.juggler.subwaytooter.api_msp.entity.MSPToot;
import jp.juggler.subwaytooter.table.MutedWord;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.DecodeOptions;
import jp.juggler.subwaytooter.util.HTMLDecoder;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
@ -71,7 +72,7 @@ public class ActText extends AppCompatActivity implements View.OnClickListener {
addAfterLine( sb, "\n" );
intent.putExtra( EXTRA_CONTENT_START, sb.length() );
sb.append( HTMLDecoder.decodeHTML( context,access_info, status.content, false, false, null ,null) );
sb.append( new DecodeOptions().decodeHTML( context,access_info, status.content));
intent.putExtra( EXTRA_CONTENT_END, sb.length() );
if( status instanceof TootStatus ){
@ -121,7 +122,7 @@ public class ActText extends AppCompatActivity implements View.OnClickListener {
addAfterLine( sb, "\n" );
sb.append( HTMLDecoder.decodeHTML( context, access_info, ( who.note != null ? who.note : null ), false, false, null ,null) );
sb.append( new DecodeOptions().decodeHTML( context, access_info, who.note != null ? who.note : null ) );
addAfterLine( sb, "\n" );

View File

@ -38,6 +38,7 @@ import jp.juggler.subwaytooter.table.PostDraft;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.table.TagSet;
import jp.juggler.subwaytooter.table.UserRelation;
import jp.juggler.subwaytooter.util.CustomEmojiCache;
import jp.juggler.subwaytooter.util.LogCategory;
import okhttp3.CipherSuite;
import okhttp3.ConnectionSpec;
@ -200,6 +201,8 @@ public class App1 extends Application {
static OkHttpUrlLoader.Factory glide_okhttp3_factory;
public static CustomEmojiCache custom_emoji_cache;
private static boolean bPrepared = false;
public static void prepare( final Context app_context ){
@ -340,6 +343,10 @@ public class App1 extends Application {
Glide.get( app_context ).register( GlideUrl.class, InputStream.class, glide_okhttp3_factory );
}
if( custom_emoji_cache == null ){
custom_emoji_cache = new CustomEmojiCache(app_context);
}
}

View File

@ -57,6 +57,7 @@ import jp.juggler.subwaytooter.util.Utils;
@SuppressWarnings("WeakerAccess") class Column implements StreamReader.Callback {
private static final LogCategory log = new LogCategory( "Column" );
interface Callback {
boolean isActivityStart();
}
@ -956,7 +957,7 @@ import jp.juggler.subwaytooter.util.Utils;
return _holder_list.size() > 1;
}
private ColumnViewHolder getViewHolder(){
ColumnViewHolder getViewHolder(){
if( is_dispose.get() ) return null;
// 複数のリスナがある場合最も新しいものを返す
return _holder_list.isEmpty() ? null : _holder_list.getFirst();

View File

@ -7,6 +7,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewCompat;
import android.text.Editable;
import android.text.Spannable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.KeyEvent;
@ -27,7 +28,9 @@ import com.omadahealth.github.swipyrefreshlayout.library.SwipyRefreshLayoutDirec
import java.util.regex.Pattern;
import jp.juggler.subwaytooter.table.AcctColor;
import jp.juggler.subwaytooter.util.CustomEmojiCache;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.NetworkEmojiSpan;
import jp.juggler.subwaytooter.view.MyListView;
import jp.juggler.subwaytooter.util.ScrollPosition;
import jp.juggler.subwaytooter.util.Utils;
@ -935,4 +938,17 @@ class ColumnViewHolder
}
}, 20L );
}
private final CustomEmojiCache.Callback emoji_load_callback = new CustomEmojiCache.Callback() {
@Override public void onComplete( Bitmap b ){
showContent();
}
};
void applyEmojiLoadCallback( @Nullable Spannable dst){
if( dst == null ) return;
for( NetworkEmojiSpan span : dst.getSpans( 0,dst.length(), NetworkEmojiSpan.class ) ){
span.setLoadCompleteCallback( emoji_load_callback );
}
}
}

View File

@ -11,6 +11,7 @@ import android.widget.Button;
import android.widget.TextView;
import jp.juggler.subwaytooter.api.entity.TootInstance;
import jp.juggler.subwaytooter.util.DecodeOptions;
import jp.juggler.subwaytooter.util.HTMLDecoder;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
@ -104,7 +105,10 @@ class HeaderViewHolderInstance extends HeaderViewHolderBase implements View.OnCl
btnEmail.setText( supplyEmpty( instance.email ) );
btnEmail.setEnabled( ! TextUtils.isEmpty( instance.email ) );
SpannableStringBuilder sb = HTMLDecoder.decodeHTML( activity, access_info, "<p>" + supplyEmpty( instance.description ) + "</p>", false, true, null, null );
SpannableStringBuilder sb = new DecodeOptions()
.setDecodeEmoji( true )
.decodeHTML( activity, access_info, "<p>" + supplyEmpty( instance.description ) + "</p>");
int previous_br_count = 0;
for( int i = 0 ; i < sb.length() ; ++ i ){
char c = sb.charAt( i );

View File

@ -105,7 +105,7 @@ class HeaderViewHolderProfile extends HeaderViewHolderBase implements View.OnCli
if( who.locked ){
s += " " + Emojione.map_name2unicode.get( "lock" );
}
tvAcct.setText( Emojione.decodeEmoji( activity, s ) );
tvAcct.setText( Emojione.decodeEmoji( activity, s ,null) );
tvNote.setText( who.decoded_note );
btnStatusCount.setText( activity.getString( R.string.statuses ) + "\n" + who.statuses_count );

View File

@ -4,6 +4,7 @@ import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import jp.juggler.subwaytooter.util.DecodeOptions;
import jp.juggler.subwaytooter.util.HTMLDecoder;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
@ -37,7 +38,7 @@ class HeaderViewHolderSearchDesc extends HeaderViewHolderBase {
}
} );
CharSequence sv = HTMLDecoder.decodeHTML( activity, access_info, html, false, true, null ,null);
CharSequence sv = new DecodeOptions().setDecodeEmoji( true ).decodeHTML( activity, access_info, html);
TextView tvSearchDesc = viewRoot.findViewById( R.id.tvSearchDesc );
tvSearchDesc.setVisibility( View.VISIBLE );

View File

@ -75,7 +75,7 @@ class ItemListAdapter extends BaseAdapter implements AdapterView.OnItemClickList
}else{
holder = (ItemViewHolder) view.getTag();
}
holder.bind( o );
holder.bind( o ,column.getViewHolder() );
return view;
}

View File

@ -251,7 +251,7 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener {
}
void bind( Object item ){
void bind( Object item ,@Nullable ColumnViewHolder cvh){
this.status = null;
this.account_thumbnail = null;
this.account_boost = null;
@ -293,7 +293,7 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener {
}
if( item instanceof MSPToot ){
showStatus( activity, (MSPToot) item );
showStatus( activity, (MSPToot) item ,cvh );
}else if( item instanceof String ){
showSearchTag( (String) item );
}else if( item instanceof TootAccount ){
@ -307,7 +307,7 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener {
, access_info.isNicoru( n.account ) ? R.attr.ic_nicoru : R.attr.btn_favourite
, Utils.formatSpannable1( activity, R.string.display_name_favourited_by, n.account.decoded_display_name )
);
if( n.status != null ) showStatus( activity, n.status );
if( n.status != null ) showStatus( activity, n.status,cvh );
}else if( TootNotification.TYPE_REBLOG.equals( n.type ) ){
showBoost(
n.account
@ -315,7 +315,7 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener {
, R.attr.btn_boost
, Utils.formatSpannable1( activity, R.string.display_name_boosted_by, n.account.decoded_display_name )
);
if( n.status != null ) showStatus( activity, n.status );
if( n.status != null ) showStatus( activity, n.status,cvh );
}else if( TootNotification.TYPE_FOLLOW.equals( n.type ) ){
showBoost(
n.account
@ -334,7 +334,7 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener {
, Utils.formatSpannable1( activity, R.string.display_name_replied_by, n.account.decoded_display_name )
);
}
if( n.status != null ) showStatus( activity, n.status );
if( n.status != null ) showStatus( activity, n.status ,cvh);
}
}else if( item instanceof TootStatus ){
TootStatus status = (TootStatus) item;
@ -347,9 +347,9 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener {
, Utils.formatSpannable1( activity, R.string.display_name_boosted_by, status.account.decoded_display_name )
);
}
showStatus( activity, status.reblog );
showStatus( activity, status.reblog ,cvh);
}else{
showStatus( activity, status );
showStatus( activity, status ,cvh);
}
}else if( item instanceof TootGap ){
showGap( (TootGap) item );
@ -399,10 +399,15 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener {
Styler.setFollowIcon( activity, btnFollow, ivFollowedBy, relation ,who );
}
private void showStatus( @NonNull ActMain activity, @NonNull TootStatusLike status ){
private void showStatus( @NonNull ActMain activity, @NonNull TootStatusLike status ,@Nullable ColumnViewHolder cvh){
this.status = status;
llStatus.setVisibility( View.VISIBLE );
if( cvh != null ){
cvh.applyEmojiLoadCallback( status.decoded_content );
cvh.applyEmojiLoadCallback( status.decoded_spoiler_text );
}
showStatusTime( activity, status );
TootAccount who = account_thumbnail = status.account;
@ -463,6 +468,8 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener {
tvContent.setMinLines( r != null ? r.originalLineCount : - 1 );
if( ! TextUtils.isEmpty( status.spoiler_text ) ){
// 元データに含まれるContent Warning を使う
llContentWarning.setVisibility( View.VISIBLE );

View File

@ -0,0 +1,30 @@
package jp.juggler.subwaytooter.api.entity;
import android.text.TextUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.HashMap;
import jp.juggler.subwaytooter.util.Utils;
public class CustomEmojiMap extends HashMap<String,String> {
// キー shortcode (コロンを含まない)
// url
public static CustomEmojiMap parse( JSONArray src ){
if( src==null ) return null;
CustomEmojiMap dst = new CustomEmojiMap();
for(int i=0,ie=src.length();i<ie;++i){
JSONObject it = src.optJSONObject( i );
String k = Utils.optStringX(it,"shortcode");
String v = Utils.optStringX(it,"url");
if( ! TextUtils.isEmpty( k ) && ! TextUtils.isEmpty( v ) ){
dst.put( k,v);
}
}
return dst;
}
}

View File

@ -22,6 +22,7 @@ import jp.juggler.subwaytooter.R;
import jp.juggler.subwaytooter.api.TootApiClient;
import jp.juggler.subwaytooter.api.TootApiResult;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.DecodeOptions;
import jp.juggler.subwaytooter.util.Emojione;
import jp.juggler.subwaytooter.util.HTMLDecoder;
import jp.juggler.subwaytooter.util.LogCategory;
@ -77,7 +78,13 @@ public class NicoEnquete {
dst.time_start = time_start;
dst.status_id = status_id;
if( dst.question != null ){
dst.question = HTMLDecoder.decodeHTML( context, access_info, dst.question.toString(), true, true, list_attachment ,status);
dst.question = new DecodeOptions()
.setShort(true)
.setDecodeEmoji( true )
.setAttachment( list_attachment )
.setLinkTag( status )
.setEmojiMap( status.emojis )
.decodeHTML( context, access_info, dst.question.toString());
}
if( dst.items != null ){
for( int i = 0, ie = dst.items.size() ; i < ie ; ++ i ){
@ -86,7 +93,7 @@ public class NicoEnquete {
// remove white spaces
sv = reWhitespace.matcher( sv ).replaceAll( " " );
// decode emoji code
dst.items.set( i, Emojione.decodeEmoji( context, sv ) );
dst.items.set( i, Emojione.decodeEmoji( context, sv ,status.emojis) );
}
}
return dst;

View File

@ -6,6 +6,7 @@ import android.support.annotation.Nullable;
import android.text.Spannable;
import android.text.TextUtils;
import jp.juggler.subwaytooter.util.DecodeOptions;
import jp.juggler.subwaytooter.util.Emojione;
import org.json.JSONArray;
@ -126,7 +127,7 @@ public class TootAccount {
dst.statuses_count = src.optLong( "statuses_count" );
dst.note = Utils.optStringX( src, "note" );
dst.decoded_note = HTMLDecoder.decodeHTML( context, account, ( dst.note != null ? dst.note : null ), true, true, null ,null );
dst.decoded_note = new DecodeOptions().setShort( true).setDecodeEmoji( true).decodeHTML( context, account, ( dst.note != null ? dst.note : null ));
dst.url = Utils.optStringX( src, "url" );
dst.avatar = Utils.optStringX( src, "avatar" ); // "https:\/\/mastodon.juggler.jp\/system\/accounts\/avatars\/000\/000\/148\/original\/0a468974fac5a448.PNG?1492081886",
@ -191,7 +192,7 @@ public class TootAccount {
sv = reWhitespace.matcher( this.display_name ).replaceAll( " " );
// decode emoji code
this.decoded_display_name = Emojione.decodeEmoji( context, sv );
this.decoded_display_name = Emojione.decodeEmoji( context, sv ,null);
}

View File

@ -24,6 +24,7 @@ import jp.juggler.subwaytooter.App1;
import jp.juggler.subwaytooter.Pref;
import jp.juggler.subwaytooter.R;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.DecodeOptions;
import jp.juggler.subwaytooter.util.HTMLDecoder;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
@ -104,6 +105,10 @@ public class TootStatus extends TootStatusLike {
TootStatus status = new TootStatus();
status.json = src;
// 絵文字マップは割と最初の方で読み込んでおきたい
status.emojis = CustomEmojiMap.parse( src.optJSONArray( "emojis" ));
status.account = TootAccount.parse( context, access_info, src.optJSONObject( "account" ) );
if( status.account == null ) return null;
@ -142,13 +147,22 @@ public class TootStatus extends TootStatusLike {
status.muted = src.optBoolean( "muted" );
status.language = Utils.optStringX( src, "language" );
status.time_created_at = parseTime( status.created_at );
status.decoded_content = HTMLDecoder.decodeHTML( context, access_info, status.content, true, true, status.media_attachments ,status);
status.decoded_content = new DecodeOptions()
.setShort( true )
.setDecodeEmoji( true)
.setAttachment( status.media_attachments )
.setEmojiMap( status.emojis )
.setLinkTag( status )
.decodeHTML( context, access_info, status.content );
// status.decoded_tags = HTMLDecoder.decodeTags( account,status.tags );
status.decoded_mentions = HTMLDecoder.decodeMentions( access_info, status.mentions ,status);
status.enquete = NicoEnquete.parse( context,access_info , status.media_attachments , Utils.optStringX( src, "enquete"),status.id,status.time_created_at,status );
return status;
}catch( Throwable ex ){
log.trace( ex );

View File

@ -60,6 +60,12 @@ public abstract class TootStatusLike extends TootId {
//Application from which the status was posted
@Nullable public TootApplication application;
@Nullable public CustomEmojiMap emojis;
/////////////////////////
// 以下はアプリ内部で使用する
public long time_created_at;
public JSONObject json;
@ -81,7 +87,7 @@ public abstract class TootStatusLike extends TootId {
private static final Pattern reWhitespace = Pattern.compile( "[\\s\\t\\x0d\\x0a]+" );
public void setSpoilerText( Context context, String sv){
public void setSpoilerText( Context context, String sv ){
if( TextUtils.isEmpty( sv ) ){
this.spoiler_text = null;
this.decoded_spoiler_text = null;
@ -90,7 +96,7 @@ public abstract class TootStatusLike extends TootId {
// remove white spaces
sv = reWhitespace.matcher( this.spoiler_text ).replaceAll( " " );
// decode emoji code
this.decoded_spoiler_text = Emojione.decodeEmoji( context, sv );
this.decoded_spoiler_text = Emojione.decodeEmoji( context, sv ,emojis);
}
}

View File

@ -12,6 +12,7 @@ import java.util.regex.Pattern;
import jp.juggler.subwaytooter.api.entity.TootAccount;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.DecodeOptions;
import jp.juggler.subwaytooter.util.HTMLDecoder;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
@ -37,7 +38,10 @@ public class MSPAccount extends TootAccount {
dst.id = src.optLong( "id" );
dst.note = Utils.optStringX( src, "note" );
dst.decoded_note = HTMLDecoder.decodeHTML( context, access_info, ( dst.note != null ? dst.note : null ), true, true, null ,null);
dst.decoded_note = new DecodeOptions()
.setShort( true )
.setDecodeEmoji( true )
.decodeHTML( context, access_info, dst.note != null ? dst.note : null );
if( TextUtils.isEmpty( dst.url ) ){
log.e( "parseAccount: missing url" );

View File

@ -18,6 +18,7 @@ import java.util.regex.Pattern;
import jp.juggler.subwaytooter.api.entity.TootStatusLike;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.DecodeOptions;
import jp.juggler.subwaytooter.util.HTMLDecoder;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
@ -80,7 +81,12 @@ public class MSPToot extends TootStatusLike {
dst.setSpoilerText( context, Utils.optStringX( src, "spoiler_text" ) );
dst.content = Utils.optStringX( src, "content" );
dst.decoded_content = HTMLDecoder.decodeHTML( context, access_info, dst.content, true, true, null ,dst);
dst.decoded_content = new DecodeOptions()
.setShort( true )
.setDecodeEmoji( true )
.setEmojiMap( dst.emojis )
.setLinkTag( dst )
.decodeHTML( context, access_info, dst.content);
return dst;
}

View File

@ -0,0 +1,264 @@
package jp.juggler.subwaytooter.util;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import jp.juggler.subwaytooter.App1;
import okhttp3.Call;
import okhttp3.Response;
@SuppressWarnings("WeakerAccess")
public class CustomEmojiCache {
private static final LogCategory log = new LogCategory( "CustomEmojiCache" );
static final int PIXEL_MAX = 64;
static final int CACHE_MAX = 512; // 使用中のビットマップは掃除しないので頻度によってはこれより多くなることもある
static final long ERROR_EXPIRE = ( 60000L * 10 );
private static long getNow(){
return SystemClock.elapsedRealtime();
}
////////////////////////////////
// エラーキャッシュ
final ConcurrentHashMap< String, Long > cache_error = new ConcurrentHashMap<>();
////////////////////////////////
// 成功キャッシュ
static class CacheItem {
@NonNull String url;
@NonNull Bitmap bitmap;
// 参照された時刻
long time_used;
CacheItem( @NonNull String url, @NonNull Bitmap bitmap ){
this.url = url;
this.bitmap = bitmap;
time_used = getNow();
}
}
final ConcurrentHashMap< String, CacheItem > cache = new ConcurrentHashMap<>();
////////////////////////////////
// リクエスト
public interface Callback {
void onComplete( Bitmap b );
}
static class Request {
@NonNull String url;
@NonNull Callback callback;
public Request( @NonNull String url, @NonNull Callback callback ){
this.url = url;
this.callback = callback;
}
}
final ConcurrentLinkedQueue< Request > queue = new ConcurrentLinkedQueue<>();
////////////////////////////////
@Nullable public Bitmap get( @NonNull String url, @NonNull Callback callback ){
synchronized( cache ){
long now = getNow();
// 成功キャッシュ
CacheItem item = cache.get( url );
if( item != null && ! item.bitmap.isRecycled() ){
item.time_used = now;
return item.bitmap;
}
// エラーキャッシュ
Long time_error = cache_error.get( url );
if( time_error != null && now < time_error + ERROR_EXPIRE ){
return null;
}
}
queue.add( new Request( url, callback ) );
worker.notifyEx();
return null;
}
////////////////////////////////
Handler handler;
public CustomEmojiCache( Context context ){
this.handler = new Handler( context.getMainLooper() );
this.worker = new Worker();
worker.start();
}
Worker worker;
class Worker extends WorkerBase {
final AtomicBoolean bCancelled = new AtomicBoolean( false );
@Override public void cancel(){
}
@Override public void run(){
while( ! bCancelled.get() ){
Request request = queue.poll();
if( request == null ){
waitEx(86400000L);
continue;
}
long now = getNow();
synchronized( cache ){
// 成功キャッシュ
CacheItem item = cache.get( request.url );
if( item != null && ! item.bitmap.isRecycled() ){
fireCallback( request.callback, item.bitmap );
continue;
}
// エラーキャッシュ
Long time_error = cache_error.get( request.url );
if( time_error != null && now < time_error + ERROR_EXPIRE ){
continue;
}
sweep_cache();
}
Bitmap b = null;
try{
byte[] data = getHttp( request.url );
if( data != null ){
b = decode( data, request.url );
}
}catch( Throwable ex ){
log.trace( ex );
}
synchronized( cache ){
if( b != null ){
CacheItem item = cache.get( request.url );
if( item == null ){
item = new CacheItem( request.url, b );
cache.put( request.url, item );
}else{
item.bitmap = b;
}
fireCallback( request.callback, b );
}else{
cache_error.put( request.url, getNow() );
}
}
}
}
private void fireCallback( final Callback callback, final Bitmap bitmap ){
handler.post( new Runnable() {
@Override public void run(){
callback.onComplete( bitmap );
}
} );
}
private byte[] getHttp( String url ){
Response response;
try{
okhttp3.Request.Builder request_builder = new okhttp3.Request.Builder();
request_builder.url( url );
Call call = App1.ok_http_client.newCall( request_builder.build() );
response = call.execute();
}catch( Throwable ex ){
log.e( ex, "getHttp network error." );
return null;
}
if( ! response.isSuccessful() ){
log.e( "getHttp response error. %s", response );
return null;
}
try{
//noinspection ConstantConditions
return response.body().bytes();
}catch( Throwable ex ){
log.e( ex, "getHttp content error." );
return null;
}
}
private void sweep_cache(){
// キャッシュの掃除
if( cache.size() >= CACHE_MAX ){
ArrayList< CacheItem > list = new ArrayList<>();
list.addAll( cache.values() );
// 降順ソート
Collections.sort( list, new Comparator< CacheItem >() {
@Override
public int compare( CacheItem a, CacheItem b ){
long delta = b.time_used - a.time_used;
return delta < 0L ? - 1 : delta > 0L ? 1 : 0;
}
} );
// 古い物から順にチェック
long now = getNow();
for( int i = list.size()-1; i>= CACHE_MAX-1; --i){
CacheItem item = list.get( i );
// あまり古くないなら無理に掃除しない
if( now - item.time_used < 1000L ) break;
cache.remove( item.url );
item.bitmap.recycle();
}
}
}
private final BitmapFactory.Options options = new BitmapFactory.Options();
private Bitmap decode( byte[] data, String url ){
options.inJustDecodeBounds = true;
options.inScaled = false;
options.outWidth = 0;
options.outHeight = 0;
BitmapFactory.decodeByteArray( data, 0, data.length, options );
int w = options.outWidth;
int h = options.outHeight;
if( w <= 0 || h <= 0 ){
log.e( "can't decode bounds. %s", url );
return null;
}
int bits = 0;
while( w > PIXEL_MAX || h > PIXEL_MAX ){
++ bits;
w >>= 1;
h >>= 1;
}
options.inJustDecodeBounds = false;
options.inSampleSize = 1 << bits;
return BitmapFactory.decodeByteArray( data, 0, data.length, options );
}
}
}

View File

@ -0,0 +1,49 @@
package jp.juggler.subwaytooter.util;
import android.content.Context;
import android.support.annotation.Nullable;
import android.text.SpannableStringBuilder;
import jp.juggler.subwaytooter.ActAccountSetting;
import jp.juggler.subwaytooter.api.entity.CustomEmojiMap;
import jp.juggler.subwaytooter.api.entity.TootAttachment;
import jp.juggler.subwaytooter.table.SavedAccount;
@SuppressWarnings("WeakerAccess")
public class DecodeOptions {
boolean bShort = false;
public DecodeOptions setShort(boolean b){
bShort = b;
return this;
}
boolean bDecodeEmoji;
public DecodeOptions setDecodeEmoji(boolean b){
bDecodeEmoji = b;
return this;
}
@Nullable TootAttachment.List list_attachment;
public DecodeOptions setAttachment(TootAttachment.List list_attachment){
this.list_attachment = list_attachment;
return this;
}
@Nullable Object link_tag;
public DecodeOptions setLinkTag(Object link_tag){
this.link_tag = link_tag;
return this;
}
@Nullable CustomEmojiMap customEmojiMap;
public DecodeOptions setEmojiMap(CustomEmojiMap customEmojiMap){
this.customEmojiMap = customEmojiMap;
return this;
}
public SpannableStringBuilder decodeHTML( Context context, LinkClickContext lcc, String html ){
return HTMLDecoder.decodeHTML( context,lcc,html ,this);
}
}

View File

@ -1,9 +1,12 @@
package jp.juggler.subwaytooter.util;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import java.util.HashMap;
import java.util.HashSet;
@ -12,19 +15,24 @@ import java.util.regex.Pattern;
import jp.juggler.subwaytooter.App1;
import jp.juggler.subwaytooter.R;
import uk.co.chrisjenx.calligraphy.CalligraphyTypefaceSpan;
import jp.juggler.subwaytooter.api.entity.CustomEmojiMap;
public abstract class Emojione
{
private static final Pattern SHORTNAME_PATTERN = Pattern.compile(":([-+\\w]+):");
public abstract class Emojione {
private static final Pattern SHORTNAME_PATTERN = Pattern.compile( ":([-+\\w]+):" );
public static final HashMap<String,String> map_name2unicode = EmojiMap._shortNameToUnicode;
private static final HashSet<String> set_unicode = EmojiMap._unicode_set;
private static class DecodeEnv{
public static final HashMap< String, String > map_name2unicode = EmojiMap._shortNameToUnicode;
private static final HashSet< String > set_unicode = EmojiMap._unicode_set;
private static class DecodeEnv {
SpannableStringBuilder sb = new SpannableStringBuilder();
int last_span_start = -1;
int last_span_end = -1;
int last_span_start = - 1;
int last_span_end = - 1;
@Nullable CustomEmojiMap custom_map;
DecodeEnv( @Nullable CustomEmojiMap custom_map ){
this.custom_map = custom_map;
}
void closeSpan(){
if( last_span_start >= 0 ){
@ -32,25 +40,25 @@ public abstract class Emojione
EmojiSpan typefaceSpan = new EmojiSpan( App1.typeface_emoji );
sb.setSpan( typefaceSpan, last_span_start, last_span_end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
}
last_span_start = -1;
last_span_start = - 1;
}
}
void addEmoji(String s){
void addEmoji( String s ){
if( last_span_start < 0 ){
last_span_start = sb.length();
}
sb.append(s);
sb.append( s );
last_span_end = sb.length();
}
void addUnicodeString(String s){
void addUnicodeString( String s ){
int i = 0;
int end = s.length();
while( i < end ){
int remain = end - i;
String emoji = null;
for(int j = EmojiMap.max_length; j>0;--j ){
for( int j = EmojiMap.max_length ; j > 0 ; -- j ){
if( j > remain ) continue;
String check = s.substring( i, i + j );
if( ! set_unicode.contains( check ) ) continue;
@ -63,48 +71,60 @@ public abstract class Emojione
continue;
}
closeSpan();
int length = Character.charCount( s.codePointAt( i ) );
if( length == 1){
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;
sb.append( s.substring( i, i + length ) );
i += length;
}
}
}
void addImageSpan( String text,Context context, int res_id){
void addImageSpan( String text, Context context, int res_id ){
closeSpan();
int start = sb.length();
sb.append(text);
sb.append( text );
int end = sb.length();
sb.setSpan( new EmojiImageSpan(context,res_id ), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
sb.setSpan( new EmojiImageSpan( context, res_id ), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
}
void addNetworkEmojiSpan( String text, @NonNull String url ){
closeSpan();
int start = sb.length();
sb.append( text );
int end = sb.length();
sb.setSpan( new NetworkEmojiSpan( url ), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
}
}
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 );
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( Context context, String s ){
DecodeEnv decode_env = new DecodeEnv();
Matcher matcher = SHORTNAME_PATTERN.matcher(s);
public static Spannable decodeEmoji( Context context, String s, @Nullable CustomEmojiMap custom_map ){
DecodeEnv decode_env = new DecodeEnv( custom_map );
Matcher matcher = SHORTNAME_PATTERN.matcher( s );
int last_end = 0;
while( matcher.find() ){
int start = matcher.start();
int end = matcher.end();
if( start > last_end ){
decode_env.addUnicodeString(s.substring( last_end,start ));
decode_env.addUnicodeString( s.substring( last_end, start ) );
}
last_end = end;
//
String unicode = map_name2unicode.get(matcher.group(1));
String unicode = map_name2unicode.get( matcher.group( 1 ) );
if( unicode == null ){
if( reHohoemi.matcher( matcher.group(1) ).find() ){
String url = ( custom_map == null ? null : custom_map.get( matcher.group( 1 ) ) );
if( ! TextUtils.isEmpty( url ) ){
decode_env.addNetworkEmojiSpan( s.substring( start, end ), url );
}else if( reHohoemi.matcher( matcher.group( 1 ) ).find() ){
decode_env.addImageSpan( s.substring( start, end ), context, R.drawable.emoji_hohoemi );
}else if( reNicoru.matcher( matcher.group(1) ).find() ){
decode_env.addImageSpan( s.substring( start, end ), context, R.drawable.emoji_nicoru );
}else if( reNicoru.matcher( matcher.group( 1 ) ).find() ){
decode_env.addImageSpan( s.substring( start, end ), context, R.drawable.emoji_nicoru );
}else{
decode_env.addUnicodeString( s.substring( start, end ) );
}
@ -115,7 +135,7 @@ public abstract class Emojione
// copy remain
int end = s.length();
if( end > last_end ){
decode_env.addUnicodeString(s.substring( last_end, end ));
decode_env.addUnicodeString( s.substring( last_end, end ) );
}
// close span
decode_env.closeSpan();

View File

@ -2,6 +2,7 @@ package jp.juggler.subwaytooter.util;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
@ -15,6 +16,7 @@ import java.util.regex.Pattern;
import jp.juggler.subwaytooter.App1;
import jp.juggler.subwaytooter.Pref;
import jp.juggler.subwaytooter.api.entity.CustomEmojiMap;
import jp.juggler.subwaytooter.api.entity.TootAttachment;
import jp.juggler.subwaytooter.api.entity.TootMention;
import jp.juggler.subwaytooter.table.SavedAccount;
@ -160,14 +162,11 @@ public class HTMLDecoder {
Context context
, LinkClickContext account
, SpannableStringBuilder sb
, boolean bShort
, boolean bDecodeEmoji
, @Nullable TootAttachment.List list_attachment
, @Nullable Object link_tag
){
, @NonNull DecodeOptions options
){
if( TAG_TEXT.equals( tag ) ){
if( bDecodeEmoji ){
sb.append( Emojione.decodeEmoji( context, decodeEntity( text ) ) );
if( options.bDecodeEmoji ){
sb.append( Emojione.decodeEmoji( context, decodeEntity( text ) ,options.customEmojiMap ) );
}else{
sb.append( decodeEntity( text ) );
}
@ -188,7 +187,7 @@ public class HTMLDecoder {
sb_tmp.append( "<img/>" );
}else{
for( Node child : child_nodes ){
child.encodeSpan( context, account, sb_tmp, bShort, bDecodeEmoji, list_attachment ,link_tag);
child.encodeSpan( context, account, sb_tmp, options);
}
}
@ -196,7 +195,7 @@ public class HTMLDecoder {
if( "a".equals( tag ) ){
start = sb.length();
sb.append( encodeUrl( bShort, context, sb_tmp.toString(), getHref(), list_attachment ) );
sb.append( encodeUrl( options.bShort, context, sb_tmp.toString(), getHref(), options.list_attachment ) );
end = sb.length();
}else if( sb_tmp != sb ){
// style もscript も読み捨てる
@ -206,7 +205,7 @@ public class HTMLDecoder {
String href = getHref();
if( href != null ){
String link_text = sb.subSequence( start,end ).toString();
MyClickableSpan span = new MyClickableSpan( account, link_text, href, account.findAcctColor( href ),link_tag );
MyClickableSpan span = new MyClickableSpan( account, link_text, href, account.findAcctColor( href ),options.link_tag );
sb.setSpan( span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE );
}
}
@ -281,7 +280,7 @@ public class HTMLDecoder {
}
if( is_media_attachment( list_attachment, href ) ){
return Emojione.decodeEmoji( context, ":frame_photo:" );
return Emojione.decodeEmoji( context, ":frame_photo:",null );
}
try{
@ -310,14 +309,12 @@ public class HTMLDecoder {
return Character.isWhitespace( c ) || c == 0x0a || c == 0x0d;
}
public static SpannableStringBuilder decodeHTML(
Context context
, LinkClickContext account
, String src
, boolean bShort
, boolean bDecodeEmoji
, @Nullable TootAttachment.List list_attachment
, @Nullable Object link_tag
,@NonNull DecodeOptions options
){
prepareTagInformation();
SpannableStringBuilder sb = new SpannableStringBuilder();
@ -329,7 +326,7 @@ public class HTMLDecoder {
rootNode.addChild( tracker, "" );
}
rootNode.encodeSpan( context, account, sb, bShort, bDecodeEmoji, list_attachment , link_tag );
rootNode.encodeSpan( context, account, sb,options);
int end = sb.length();
while( end > 0 && isWhitespace( sb.charAt( end - 1 ) ) ) -- end;
if( end < sb.length() ){

View File

@ -0,0 +1,81 @@
package jp.juggler.subwaytooter.util;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.style.ReplacementSpan;
import jp.juggler.subwaytooter.App1;
public class NetworkEmojiSpan extends ReplacementSpan {
private static final float scale_ratio = 1.14f;
private static final float descent_ratio = 0.211f;
@NonNull private final String url;
@NonNull private final Paint mPaint = new Paint();
@NonNull private final Rect rect_src = new Rect();
@NonNull private final RectF rect_dst = new RectF();
NetworkEmojiSpan( @NonNull String url ){
super();
this.url = url;
mPaint.setFilterBitmap( true );
}
private CustomEmojiCache.Callback load_callback;
public void setLoadCompleteCallback(CustomEmojiCache.Callback load_callback){
this.load_callback = load_callback;
}
@Override
public int getSize(
@NonNull Paint paint
, CharSequence text
, @IntRange(from = 0) int start
, @IntRange(from = 0) int end
, @Nullable Paint.FontMetricsInt fm
){
int size = (int) ( 0.5f + scale_ratio * paint.getTextSize() );
if( fm != null ){
int c_descent = (int) ( 0.5f + size * descent_ratio );
int c_ascent = c_descent - size;
if( fm.ascent > c_ascent ) fm.ascent = c_ascent;
if( fm.top > c_ascent ) fm.top = c_ascent;
if( fm.descent < c_descent ) fm.descent = c_descent;
if( fm.bottom < c_descent ) fm.bottom = c_descent;
}
return size;
}
@Override public void draw(
@NonNull Canvas canvas
, CharSequence text, int start, int end
, float x, int top, int baseline, int bottom
, @NonNull Paint textPaint
){
if(load_callback == null ) return;
int size = (int) ( 0.5f + scale_ratio * textPaint.getTextSize() );
int c_descent = (int) ( 0.5f + size * descent_ratio );
Bitmap b = App1.custom_emoji_cache.get( url ,load_callback);
if( b != null && ! b.isRecycled() ){
rect_src.set(0,0,b.getWidth(),b.getHeight() );
rect_dst.set(0,0,size,size);
int transY = baseline - size + c_descent;
canvas.save();
canvas.translate( x, transY );
canvas.drawBitmap( b, rect_src,rect_dst,mPaint );
canvas.restore();
}
}
}