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