From b0c1207f1ac8cecea922297c61b5fc496d795515 Mon Sep 17 00:00:00 2001 From: tateisu Date: Tue, 19 Sep 2017 18:35:53 +0900 Subject: [PATCH] =?UTF-8?q?v1.5.6=20-=20(=E8=A9=A6=E9=A8=93=E5=AE=9F?= =?UTF-8?q?=E8=A3=85)=E3=83=9E=E3=82=B9=E3=83=88=E3=83=89=E3=83=B3?= =?UTF-8?q?=E3=81=AE=E3=82=AB=E3=82=B9=E3=82=BF=E3=83=A0=E7=B5=B5=E6=96=87?= =?UTF-8?q?=E5=AD=97=E3=81=AE=E8=A1=A8=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/dictionaries/tateisu.xml | 2 + app/build.gradle | 4 +- .../subwaytooter/ActAccountSetting.java | 8 +- .../java/jp/juggler/subwaytooter/ActPost.java | 8 +- .../java/jp/juggler/subwaytooter/ActText.java | 5 +- .../java/jp/juggler/subwaytooter/App1.java | 7 + .../java/jp/juggler/subwaytooter/Column.java | 3 +- .../subwaytooter/ColumnViewHolder.java | 16 ++ .../HeaderViewHolderInstance.java | 6 +- .../subwaytooter/HeaderViewHolderProfile.java | 2 +- .../HeaderViewHolderSearchDesc.java | 3 +- .../juggler/subwaytooter/ItemListAdapter.java | 2 +- .../juggler/subwaytooter/ItemViewHolder.java | 23 +- .../api/entity/CustomEmojiMap.java | 30 ++ .../subwaytooter/api/entity/NicoEnquete.java | 11 +- .../subwaytooter/api/entity/TootAccount.java | 5 +- .../subwaytooter/api/entity/TootStatus.java | 16 +- .../api/entity/TootStatusLike.java | 10 +- .../api_msp/entity/MSPAccount.java | 6 +- .../subwaytooter/api_msp/entity/MSPToot.java | 8 +- .../subwaytooter/util/CustomEmojiCache.java | 264 ++++++++++++++++++ .../subwaytooter/util/DecodeOptions.java | 49 ++++ .../juggler/subwaytooter/util/Emojione.java | 90 +++--- .../subwaytooter/util/HTMLDecoder.java | 29 +- .../subwaytooter/util/NetworkEmojiSpan.java | 81 ++++++ 25 files changed, 606 insertions(+), 82 deletions(-) create mode 100644 app/src/main/java/jp/juggler/subwaytooter/api/entity/CustomEmojiMap.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/util/DecodeOptions.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/util/NetworkEmojiSpan.java diff --git a/.idea/dictionaries/tateisu.xml b/.idea/dictionaries/tateisu.xml index 83b1d704..7d15c7ed 100644 --- a/.idea/dictionaries/tateisu.xml +++ b/.idea/dictionaries/tateisu.xml @@ -6,6 +6,7 @@ dont emoji emojione + emojis enquete enty favourited @@ -26,6 +27,7 @@ reblogs sephiroth sharedpref + shortname simeji styler subwaytooter diff --git a/app/build.gradle b/app/build.gradle index 15bae36a..c779abb9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.java b/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.java index cd7c29fb..91b3d440 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.java @@ -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( "" ); } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPost.java b/app/src/main/java/jp/juggler/subwaytooter/ActPost.java index b29b8307..b343eb23 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPost.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPost.java @@ -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() ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActText.java b/app/src/main/java/jp/juggler/subwaytooter/ActText.java index 8471a058..e7c58a29 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActText.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ActText.java @@ -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" ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/App1.java b/app/src/main/java/jp/juggler/subwaytooter/App1.java index 62e66f4c..0c310e6b 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/App1.java +++ b/app/src/main/java/jp/juggler/subwaytooter/App1.java @@ -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); + } + } diff --git a/app/src/main/java/jp/juggler/subwaytooter/Column.java b/app/src/main/java/jp/juggler/subwaytooter/Column.java index e110f707..5b9be668 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/Column.java +++ b/app/src/main/java/jp/juggler/subwaytooter/Column.java @@ -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(); diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java index 8161d026..ea04866c 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java @@ -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 ); + } + } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/HeaderViewHolderInstance.java b/app/src/main/java/jp/juggler/subwaytooter/HeaderViewHolderInstance.java index d35024da..e8fd57ed 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/HeaderViewHolderInstance.java +++ b/app/src/main/java/jp/juggler/subwaytooter/HeaderViewHolderInstance.java @@ -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, "

" + supplyEmpty( instance.description ) + "

", false, true, null, null ); + SpannableStringBuilder sb = new DecodeOptions() + .setDecodeEmoji( true ) + .decodeHTML( activity, access_info, "

" + supplyEmpty( instance.description ) + "

"); + int previous_br_count = 0; for( int i = 0 ; i < sb.length() ; ++ i ){ char c = sb.charAt( i ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/HeaderViewHolderProfile.java b/app/src/main/java/jp/juggler/subwaytooter/HeaderViewHolderProfile.java index 2a2797c5..3f346ffa 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/HeaderViewHolderProfile.java +++ b/app/src/main/java/jp/juggler/subwaytooter/HeaderViewHolderProfile.java @@ -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 ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/HeaderViewHolderSearchDesc.java b/app/src/main/java/jp/juggler/subwaytooter/HeaderViewHolderSearchDesc.java index c19a3a88..b6fb7140 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/HeaderViewHolderSearchDesc.java +++ b/app/src/main/java/jp/juggler/subwaytooter/HeaderViewHolderSearchDesc.java @@ -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 ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/ItemListAdapter.java b/app/src/main/java/jp/juggler/subwaytooter/ItemListAdapter.java index 1f877459..2670f02b 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ItemListAdapter.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ItemListAdapter.java @@ -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; } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolder.java b/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolder.java index 08af9306..9b8e967c 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolder.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolder.java @@ -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 ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/CustomEmojiMap.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/CustomEmojiMap.java new file mode 100644 index 00000000..ffd4bf58 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/CustomEmojiMap.java @@ -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 { + + // キー: 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 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 ); + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/DecodeOptions.java b/app/src/main/java/jp/juggler/subwaytooter/util/DecodeOptions.java new file mode 100644 index 00000000..e0ea100e --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/util/DecodeOptions.java @@ -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); + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/Emojione.java b/app/src/main/java/jp/juggler/subwaytooter/util/Emojione.java index 84708848..b88e7adf 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/Emojione.java +++ b/app/src/main/java/jp/juggler/subwaytooter/util/Emojione.java @@ -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 map_name2unicode = EmojiMap._shortNameToUnicode; - private static final HashSet 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(); diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/HTMLDecoder.java b/app/src/main/java/jp/juggler/subwaytooter/util/HTMLDecoder.java index 9ab05da4..04b9d179 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/HTMLDecoder.java +++ b/app/src/main/java/jp/juggler/subwaytooter/util/HTMLDecoder.java @@ -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( "" ); }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() ){ diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/NetworkEmojiSpan.java b/app/src/main/java/jp/juggler/subwaytooter/util/NetworkEmojiSpan.java new file mode 100644 index 00000000..9cf8b889 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/util/NetworkEmojiSpan.java @@ -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(); + } + } +}