From 82756a69383dafe68605049c7698b67c186c11ad Mon Sep 17 00:00:00 2001 From: tateisu Date: Thu, 4 Jan 2018 07:53:37 +0900 Subject: [PATCH] keyword highlight --- app/src/main/AndroidManifest.xml | 10 + .../subwaytooter/ActAccountSetting.java | 5 +- .../subwaytooter/ActHighlightWordEdit.java | 250 +++++++++++++ .../subwaytooter/ActHighlightWordList.java | 347 ++++++++++++++++++ .../java/jp/juggler/subwaytooter/ActMain.java | 5 +- .../jp/juggler/subwaytooter/ActMutedApp.java | 2 +- .../jp/juggler/subwaytooter/ActMutedWord.java | 2 +- .../java/jp/juggler/subwaytooter/ActPost.java | 9 +- .../java/jp/juggler/subwaytooter/App1.java | 15 +- .../jp/juggler/subwaytooter/AppState.java | 59 +++ .../java/jp/juggler/subwaytooter/Column.java | 128 ++++--- .../juggler/subwaytooter/PollingWorker.java | 11 +- .../jp/juggler/subwaytooter/StreamReader.java | 40 +- .../subwaytooter/action/Action_Toot.java | 19 +- .../subwaytooter/action/Action_User.java | 3 +- .../juggler/subwaytooter/api/TootParser.java | 76 ++++ .../subwaytooter/api/entity/TootContext.java | 9 +- .../api/entity/TootNotification.java | 15 +- .../subwaytooter/api/entity/TootResults.java | 9 +- .../subwaytooter/api/entity/TootStatus.java | 68 ++-- .../api/entity/TootStatusLike.java | 45 ++- .../api_msp/entity/MSPAccount.java | 7 +- .../subwaytooter/api_msp/entity/MSPToot.java | 25 +- .../api_tootsearch/entity/TSToot.java | 40 +- .../subwaytooter/dialog/DlgListMember.java | 3 +- .../subwaytooter/table/HighlightWord.java | 191 ++++++++++ .../subwaytooter/util/CharacterGroup.java | 339 +++++++++++++++++ .../subwaytooter/util/DecodeOptions.java | 13 + .../subwaytooter/util/EmojiDecoder.java | 86 ++--- .../subwaytooter/util/HTMLDecoder.java | 20 + .../subwaytooter/util/HighlightSpan.java | 28 ++ .../juggler/subwaytooter/util/PostHelper.java | 5 +- .../jp/juggler/subwaytooter/util/Utils.java | 4 +- .../subwaytooter/util/WordTrieTree.java | 179 ++++++--- .../main/res/drawable-hdpi/ic_volume_up.png | Bin 0 -> 668 bytes .../res/drawable-hdpi/ic_volume_up_dark.png | Bin 0 -> 604 bytes .../main/res/drawable-mdpi/ic_volume_up.png | Bin 0 -> 442 bytes .../res/drawable-mdpi/ic_volume_up_dark.png | Bin 0 -> 396 bytes .../main/res/drawable-xhdpi/ic_volume_up.png | Bin 0 -> 846 bytes .../res/drawable-xhdpi/ic_volume_up_dark.png | Bin 0 -> 756 bytes .../main/res/drawable-xxhdpi/ic_volume_up.png | Bin 0 -> 1408 bytes .../res/drawable-xxhdpi/ic_volume_up_dark.png | Bin 0 -> 1255 bytes .../main/res/layout/act_highlight_edit.xml | 128 +++++++ .../main/res/layout/act_highlight_list.xml | 47 +++ .../{act_mute_app.xml => act_word_list.xml} | 0 app/src/main/res/layout/lv_highlight_word.xml | 78 ++++ app/src/main/res/menu/menu_navi_drawer.xml | 4 + app/src/main/res/values-fr/strings.xml | 8 +- app/src/main/res/values-ja/strings.xml | 6 + app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/strings.xml | 6 + app/src/main/res/values/styles.xml | 3 + 52 files changed, 2028 insertions(+), 320 deletions(-) create mode 100644 app/src/main/java/jp/juggler/subwaytooter/ActHighlightWordEdit.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/ActHighlightWordList.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/api/TootParser.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/table/HighlightWord.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/util/CharacterGroup.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/util/HighlightSpan.java create mode 100644 app/src/main/res/drawable-hdpi/ic_volume_up.png create mode 100644 app/src/main/res/drawable-hdpi/ic_volume_up_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_volume_up.png create mode 100644 app/src/main/res/drawable-mdpi/ic_volume_up_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_volume_up.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_volume_up_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_volume_up.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_volume_up_dark.png create mode 100644 app/src/main/res/layout/act_highlight_edit.xml create mode 100644 app/src/main/res/layout/act_highlight_list.xml rename app/src/main/res/layout/{act_mute_app.xml => act_word_list.xml} (100%) create mode 100644 app/src/main/res/layout/lv_highlight_word.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0ecbb716..a5f48bd6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -181,6 +181,16 @@ android:name=".ActMutedWord" android:label="@string/muted_word" /> + + + + tmp_list = new ArrayList<>(); + try{ + Cursor cursor = HighlightWord.createCursor(); + if( cursor != null ){ + try{ + while( cursor.moveToNext() ){ + HighlightWord item = new HighlightWord( cursor ); + tmp_list.add( item ); + } + + }finally{ + cursor.close(); + } + } + }catch( Throwable ex ){ + log.trace( ex ); + } + listAdapter.setItemList( tmp_list ); + } + + @Override public void onClick( View v ){ + switch( v.getId() ){ + case R.id.btnAdd: + create(); + break; + } + } + + // リスト要素のViewHolder + class MyViewHolder extends DragItemAdapter.ViewHolder implements View.OnClickListener { + + final TextView tvName; + final View btnSound; + + MyViewHolder( final View viewRoot ){ + super( viewRoot + , R.id.ivDragHandle // View ID。 ここを押すとドラッグ操作をすぐに開始する + , false // 長押しでドラッグ開始するなら真 + ); + + tvName = viewRoot.findViewById( R.id.tvName ); + btnSound = viewRoot.findViewById( R.id.btnSound ); + + // リスト要素のビューが ListSwipeItem だった場合、Swipe操作を制御できる + if( viewRoot instanceof ListSwipeItem ){ + ListSwipeItem lsi = (ListSwipeItem) viewRoot; + lsi.setSwipeInStyle( ListSwipeItem.SwipeInStyle.SLIDE ); + lsi.setSupportedSwipeDirection( ListSwipeItem.SwipeDirection.LEFT ); + } + + } + + void bind( HighlightWord item ){ + itemView.setTag( item ); // itemView は親クラスのメンバ変数 + tvName.setText( item.name ); + + int c = item.color_bg; + if( c == 0 ){ + tvName.setBackgroundColor( 0 ); + }else{ + tvName.setBackgroundColor( c ); + } + + c = item.color_fg; + if( c == 0 ){ + tvName.setTextColor( Styler.getAttributeColor( ActHighlightWordList.this, android.R.attr.textColorPrimary ) ); + }else{ + tvName.setTextColor( c ); + } + + btnSound.setVisibility( item.sound_type == HighlightWord.SOUND_TYPE_NONE ? View.GONE :View.VISIBLE); + btnSound.setOnClickListener( this ); + btnSound.setTag( item ); + } + + // @Override + // public boolean onItemLongClicked( View view ){ + // return false; + // } + + @Override + public void onItemClicked( View view ){ + Object o = view.getTag(); + if( o instanceof HighlightWord ){ + HighlightWord adapterItem = (HighlightWord) o; + edit( adapterItem ); + } + } + + @Override public void onClick( View v ){ + Object o = v.getTag(); + if( o instanceof HighlightWord ){ + sound( (HighlightWord) o ); + } + + } + } + + // ドラッグ操作中のデータ + private class MyDragItem extends DragItem { + MyDragItem( Context context, int layoutId ){ + super( context, layoutId ); + } + + @Override + public void onBindDragView( View clickedView, View dragView ){ + ( (TextView) dragView.findViewById( R.id.tvName ) ).setText( + ( (TextView) clickedView.findViewById( R.id.tvName ) ).getText() + ); + ( dragView.findViewById( R.id.btnSound ) ).setVisibility( + ( clickedView.findViewById( R.id.btnSound ) ).getVisibility() + ); + dragView.findViewById( R.id.item_layout ).setBackgroundColor( + Styler.getAttributeColor( ActHighlightWordList.this, R.attr.list_item_bg_pressed_dragged ) + ); + } + } + + private class MyListAdapter extends DragItemAdapter< HighlightWord, MyViewHolder > { + + MyListAdapter(){ + super(); + setHasStableIds( true ); + setItemList( new ArrayList< HighlightWord >() ); + } + + @Override + public MyViewHolder onCreateViewHolder( ViewGroup parent, int viewType ){ + View view = getLayoutInflater().inflate( R.layout.lv_highlight_word, parent, false ); + return new MyViewHolder( view ); + } + + @Override + public void onBindViewHolder( MyViewHolder holder, int position ){ + super.onBindViewHolder( holder, position ); + holder.bind( getItemList().get( position ) ); + } + + @Override + public long getUniqueItemId( int position ){ + HighlightWord item = mItemList.get( position ); // mItemList は親クラスのメンバ変数 + return item.id; + } + } + + private void create(){ + DlgTextInput.show( this, getString( R.string.new_item ), "", new DlgTextInput.Callback() { + @Override public void onEmptyError(){ + Utils.showToast( ActHighlightWordList.this, true, R.string.word_empty ); + } + + @Override public void onOK( Dialog dialog, String text ){ + HighlightWord item = HighlightWord.load( text ); + if( item == null ){ + item = new HighlightWord( text ); + item.save(); + loadData(); + } + edit( item ); + try{ + dialog.dismiss(); + }catch( Throwable ignored ){ + + } + } + } ); + } + + private void edit( @NonNull HighlightWord item ){ + ActHighlightWordEdit.open( this, REQUEST_CODE_EDIT, item ); + } + + private static final int REQUEST_CODE_EDIT = 1; + + @Override protected void onActivityResult( int requestCode, int resultCode, Intent data ){ + if( requestCode == REQUEST_CODE_EDIT && resultCode == RESULT_OK && data != null ){ + try{ + HighlightWord item = new HighlightWord( new JSONObject( data.getStringExtra( ActHighlightWordEdit.EXTRA_ITEM ) ) ); + item.save(); + loadData(); + return; + }catch( Throwable ex ){ + throw new RuntimeException( "can't loading data", ex ); + } + } + super.onActivityResult( requestCode, resultCode, data ); + } + + WeakReference< Ringtone > last_ringtone; + + private void stopLastRingtone(){ + Ringtone r = last_ringtone == null ? null : last_ringtone.get(); + if( r != null ){ + try{ + r.stop(); + }catch( Throwable ex ){ + log.trace( ex ); + }finally{ + last_ringtone = null; + } + } + } + + private void sound( @NonNull HighlightWord item ){ + + stopLastRingtone(); + + if( item.sound_type == HighlightWord.SOUND_TYPE_NONE ) return; + + if( item.sound_type == HighlightWord.SOUND_TYPE_CUSTOM + && ! TextUtils.isEmpty( item.sound_uri ) + ){ + try{ + Ringtone ringtone = RingtoneManager.getRingtone( this, Uri.parse( item.sound_uri ) ); + if( ringtone != null ){ + last_ringtone = new WeakReference<>( ringtone ); + ringtone.play(); + return; + } + }catch( Throwable ex ){ + log.trace( ex ); + } + } + + Uri uri = RingtoneManager.getDefaultUri( RingtoneManager.TYPE_NOTIFICATION ); + try{ + Ringtone ringtone = RingtoneManager.getRingtone( this, uri ); + if( ringtone != null ){ + last_ringtone = new WeakReference<>( ringtone ); + ringtone.play(); + } + }catch( Throwable ex ){ + log.trace( ex ); + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMain.java b/app/src/main/java/jp/juggler/subwaytooter/ActMain.java index c09dd939..4a0a3767 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMain.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMain.java @@ -748,7 +748,10 @@ public class ActMain extends AppCompatActivity }else if( id == R.id.nav_muted_word ){ startActivity( new Intent( this, ActMutedWord.class ) ); - + + }else if( id == R.id.nav_highlight_word ){ + startActivity( new Intent( this, ActHighlightWordList.class ) ); + }else if( id == R.id.nav_app_about ){ startActivityForResult( new Intent( this, ActAbout.class ), ActMain.REQUEST_APP_ABOUT ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMutedApp.java b/app/src/main/java/jp/juggler/subwaytooter/ActMutedApp.java index 94e39fce..0272b2ec 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMutedApp.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMutedApp.java @@ -42,7 +42,7 @@ public class ActMutedApp extends AppCompatActivity { } private void initUI(){ - setContentView( R.layout.act_mute_app ); + setContentView( R.layout.act_word_list ); Styler.fixHorizontalPadding2( findViewById( R.id.llContent ) ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMutedWord.java b/app/src/main/java/jp/juggler/subwaytooter/ActMutedWord.java index a49841a0..9102c4af 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMutedWord.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMutedWord.java @@ -42,7 +42,7 @@ public class ActMutedWord extends AppCompatActivity { } private void initUI(){ - setContentView( R.layout.act_mute_app ); + setContentView( R.layout.act_word_list ); Styler.fixHorizontalPadding2( findViewById( R.id.llContent ) ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPost.java b/app/src/main/java/jp/juggler/subwaytooter/ActPost.java index 495d2c0c..30ec8098 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPost.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPost.java @@ -64,6 +64,7 @@ import jp.juggler.subwaytooter.api.entity.TootAttachment; import jp.juggler.subwaytooter.api.entity.TootMention; import jp.juggler.subwaytooter.api.entity.TootResults; import jp.juggler.subwaytooter.api.entity.TootStatus; +import jp.juggler.subwaytooter.api.TootParser; import jp.juggler.subwaytooter.dialog.AccountPicker; import jp.juggler.subwaytooter.dialog.DlgDraftPicker; import jp.juggler.subwaytooter.dialog.DlgTextInput; @@ -71,6 +72,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.CharacterGroup; import jp.juggler.subwaytooter.util.DecodeOptions; import jp.juggler.subwaytooter.util.EmojiDecoder; import jp.juggler.subwaytooter.util.LinkClickContext; @@ -409,7 +411,8 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener, sv = intent.getStringExtra( KEY_REPLY_STATUS ); if( sv != null ){ try{ - TootStatus reply_status = TootStatus.parse( ActPost.this, account, new JSONObject( sv ) ); + TootStatus reply_status = new TootParser( ActPost.this, account).status(new JSONObject( sv )); + if( reply_status != null ){ // CW をリプライ元に合わせる @@ -874,7 +877,7 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener, TootApiResult result = client.request( path ); if( result != null && result.object != null ){ - TootResults tmp = TootResults.parse( ActPost.this, access_info, result.object ); + TootResults tmp = new TootParser( ActPost.this, access_info).results( result.object ); if( tmp != null && tmp.statuses != null && ! tmp.statuses.isEmpty() ){ target_status = tmp.statuses.get( 0 ); } @@ -1351,7 +1354,7 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener, Editable e = etContent.getEditableText(); int len = e.length(); char last_char = ( len <= 0 ? ' ' : e.charAt( len - 1 ) ); - if( ! EmojiDecoder.isWhitespaceBeforeEmoji( last_char ) ){ + if( ! CharacterGroup.isWhitespace( last_char ) ){ e.append( " " ).append( pa.attachment.text_url ); }else{ e.append( pa.attachment.text_url ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/App1.java b/app/src/main/java/jp/juggler/subwaytooter/App1.java index d670d0d2..81048928 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/App1.java +++ b/app/src/main/java/jp/juggler/subwaytooter/App1.java @@ -20,7 +20,6 @@ import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory; import com.bumptech.glide.load.model.GlideUrl; import java.io.File; -import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.concurrent.BlockingQueue; @@ -33,6 +32,7 @@ import java.util.concurrent.atomic.AtomicInteger; import jp.juggler.subwaytooter.api.entity.TootAttachment; import jp.juggler.subwaytooter.table.AcctColor; import jp.juggler.subwaytooter.table.AcctSet; +import jp.juggler.subwaytooter.table.HighlightWord; import jp.juggler.subwaytooter.table.MutedApp; import jp.juggler.subwaytooter.table.ClientInfo; import jp.juggler.subwaytooter.table.ContentWarning; @@ -54,7 +54,6 @@ import okhttp3.CacheControl; import okhttp3.Call; import okhttp3.CipherSuite; import okhttp3.ConnectionSpec; -import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Response; import uk.co.chrisjenx.calligraphy.CalligraphyConfig; @@ -77,7 +76,7 @@ public class App1 extends Application { public static final String FILE_PROVIDER_AUTHORITY = "jp.juggler.subwaytooter.FileProvider"; static final String DB_NAME = "app_db"; - static final int DB_VERSION = 20; + static final int DB_VERSION = 21; // 2017/4/25 v10 1=>2 SavedAccount に通知設定を追加 // 2017/4/25 v10 1=>2 NotificationTracking テーブルを追加 @@ -97,6 +96,7 @@ public class App1 extends Application { // 2017/9/23 v161 17=>18 SavedAccountに項目追加 // 2017/9/23 v161 18=>19 ClientInfoテーブルを置き換える // 2017/12/01 v175 19=>20 UserRelation に項目追加 + // 2018/1/03 v197 20=>21 HighlightWord テーブルを追加 private static DBOpenHelper db_open_helper; @@ -125,6 +125,7 @@ public class App1 extends Application { MutedWord.onDBCreate( db ); PostDraft.onDBCreate( db ); TagSet.onDBCreate( db ); + HighlightWord.onDBCreate( db ); } @Override public void onUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){ @@ -142,6 +143,7 @@ public class App1 extends Application { MutedWord.onDBUpgrade( db, oldVersion, newVersion ); PostDraft.onDBUpgrade( db, oldVersion, newVersion ); TagSet.onDBUpgrade( db, oldVersion, newVersion ); + HighlightWord.onDBUpgrade( db, oldVersion, newVersion ); } } @@ -536,4 +538,11 @@ public class App1 extends Application { openCustomTab( activity, url ); } } + + public static void sound( @NonNull HighlightWord item ){ + if( app_state != null ){ + app_state.sound( item ); + } + } + } diff --git a/app/src/main/java/jp/juggler/subwaytooter/AppState.java b/app/src/main/java/jp/juggler/subwaytooter/AppState.java index 8594be89..33753f05 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/AppState.java +++ b/app/src/main/java/jp/juggler/subwaytooter/AppState.java @@ -6,6 +6,9 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.media.Ringtone; +import android.media.RingtoneManager; +import android.net.Uri; import android.os.AsyncTask; import android.os.Handler; import android.os.SystemClock; @@ -24,6 +27,7 @@ import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.InputStream; import java.io.OutputStream; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -36,6 +40,7 @@ import java.util.regex.Pattern; import jp.juggler.subwaytooter.api.entity.TootStatus; import jp.juggler.subwaytooter.api.entity.TootStatusLike; +import jp.juggler.subwaytooter.table.HighlightWord; import jp.juggler.subwaytooter.table.SavedAccount; import jp.juggler.subwaytooter.util.LogCategory; import jp.juggler.subwaytooter.util.MyClickableSpan; @@ -484,4 +489,58 @@ public class AppState { } }; + + private WeakReference< Ringtone > last_ringtone; + + private void stopLastRingtone(){ + Ringtone r = last_ringtone == null ? null : last_ringtone.get(); + if( r != null ){ + try{ + r.stop(); + }catch( Throwable ex ){ + log.trace( ex ); + }finally{ + last_ringtone = null; + } + } + } + + private long last_sound; + + void sound( @NonNull HighlightWord item ){ + // 短時間に何度もならないようにする + long now = SystemClock.elapsedRealtime(); + if( now - last_sound < 500L ) return; + last_sound = now; + + stopLastRingtone(); + + if( item.sound_type == HighlightWord.SOUND_TYPE_NONE ) return; + + if( item.sound_type == HighlightWord.SOUND_TYPE_CUSTOM + && ! TextUtils.isEmpty( item.sound_uri ) + ){ + try{ + Ringtone ringtone = RingtoneManager.getRingtone( context, Uri.parse( item.sound_uri ) ); + if( ringtone != null ){ + last_ringtone = new WeakReference<>( ringtone ); + ringtone.play(); + return; + } + }catch( Throwable ex ){ + log.trace( ex ); + } + } + + Uri uri = RingtoneManager.getDefaultUri( RingtoneManager.TYPE_NOTIFICATION ); + try{ + Ringtone ringtone = RingtoneManager.getRingtone( context, uri ); + if( ringtone != null ){ + last_ringtone = new WeakReference<>( ringtone ); + ringtone.play(); + } + }catch( Throwable ex ){ + log.trace( ex ); + } + } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/Column.java b/app/src/main/java/jp/juggler/subwaytooter/Column.java index 7320c100..6fb8752e 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/Column.java +++ b/app/src/main/java/jp/juggler/subwaytooter/Column.java @@ -39,6 +39,8 @@ import jp.juggler.subwaytooter.api.entity.TootRelationShip; import jp.juggler.subwaytooter.api.entity.TootReport; import jp.juggler.subwaytooter.api.entity.TootResults; import jp.juggler.subwaytooter.api.entity.TootStatus; +import jp.juggler.subwaytooter.api.TootParser; +import jp.juggler.subwaytooter.api.entity.TootStatusLike; import jp.juggler.subwaytooter.api.entity.TootTag; import jp.juggler.subwaytooter.api_msp.MSPClient; import jp.juggler.subwaytooter.api_msp.entity.MSPToot; @@ -46,6 +48,7 @@ import jp.juggler.subwaytooter.api_tootsearch.TSClient; import jp.juggler.subwaytooter.api_tootsearch.entity.TSToot; import jp.juggler.subwaytooter.table.AcctColor; import jp.juggler.subwaytooter.table.AcctSet; +import jp.juggler.subwaytooter.table.HighlightWord; import jp.juggler.subwaytooter.table.MutedApp; import jp.juggler.subwaytooter.table.MutedWord; import jp.juggler.subwaytooter.table.SavedAccount; @@ -1171,6 +1174,7 @@ import jp.juggler.subwaytooter.util.Utils; private Pattern column_regex_filter; private HashSet< String > muted_app; private WordTrieTree muted_word; + private WordTrieTree highlight_trie; private void initFilter(){ column_regex_filter = null; @@ -1184,6 +1188,7 @@ import jp.juggler.subwaytooter.util.Utils; muted_app = MutedApp.getNameSet(); muted_word = MutedWord.getNameSet(); + highlight_trie = HighlightWord.getNameSet(); } private boolean isFiltered( @NonNull TootStatus status ){ @@ -1378,6 +1383,8 @@ import jp.juggler.subwaytooter.util.Utils; fireShowContent(); @SuppressLint("StaticFieldLeak") AsyncTask< Void, Void, TootApiResult > task = this.last_task = new AsyncTask< Void, Void, TootApiResult >() { + TootParser parser = new TootParser( context, access_info).setHighlightTrie( highlight_trie ); + TootInstance instance_tmp; @@ -1398,7 +1405,10 @@ import jp.juggler.subwaytooter.util.Utils; TootApiResult result = client.request( path_base ); if( result != null && result.array != null ){ // - TootStatus.List src = TootStatus.parseList( context, access_info, result.array, true ); + TootStatus.List src = new TootParser( context, access_info) + .setPinned( true ) + .setHighlightTrie( highlight_trie ) + .statusList( result.array ); for( TootStatus status : src ){ log.d( "pinned: %s %s", status.id, status.decoded_content ); @@ -1407,52 +1417,7 @@ import jp.juggler.subwaytooter.util.Utils; list_pinned = new ArrayList<>( src.size() ); addWithFilter( list_pinned, src ); - // 1.6rc では以下の理由により、40overの固定トゥートを取得することは困難である - // - max_idを指定せずにAPIで取得すると適当な件数のリストが返ってくる。ソート順はpinした日時。max_idはリスト中の最後の要素のIDを返す - // - max_idを指定してAPIで取得すると「ステータスIDがmax_idより小さい&pinされている」トゥートをpin日時順にソートしたものが返ってくる - // - max_idはpin日時を考慮していないのだからページング用のパラメータとしては全く不適切である - // - 取得できるステータスにはpinされた日時は含まれない - // // - // // pinステータスは独自にページ管理する - // long time_start = SystemClock.elapsedRealtime(); - // String max_id = parseMaxId( result ); - // char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' ); - // for( ; ; ){ - // - // if( client.isCancelled() ){ - // log.d( "loading-statuses-pinned: cancelled." ); - // break; - // } - // if( max_id == null ){ - // log.d( "loading-statuses-pinned: max_id is null." ); - // break; - // } - // if( src.isEmpty() ){ - // log.d( "loading-statuses-pinned: previous response is empty." ); - // break; - // } - // if( SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){ - // log.d( "loading-statuses-pinned: timeout." ); - // break; - // } - // - // String path = path_base + delimiter + "max_id=" + max_id; - // TootApiResult result2 = client.request( path ); - // if( result2 == null || result2.array == null ){ - // log.d( "loading-statuses-pinned: error or cancelled." ); - // break; - // } - // - // src = TootStatus.parseList( context, access_info, result2.array ,true); - // for(TootStatus status : src ){ - // log.d("pinned: %s %s",status.id, status.decoded_content); - // } - // - // addWithFilter( list_pinned, src ); - // - // // pinnedステータスは独自にページ管理する - // max_id = parseMaxId( result2 ); - // } + // pinned tootにはページングの概念はない } log.d( "getStatusesPinned: list size=%s", list_pinned == null ? - 1 : list_pinned.size() ); } @@ -1460,12 +1425,14 @@ import jp.juggler.subwaytooter.util.Utils; ArrayList< Object > list_tmp; TootApiResult getStatuses( TootApiClient client, String path_base ){ + + long time_start = SystemClock.elapsedRealtime(); TootApiResult result = client.request( path_base ); if( result != null && result.array != null ){ saveRange( result, true, true ); // - TootStatus.List src = TootStatus.parseList( context, access_info, result.array ); + TootStatus.List src = parser.statusList( result.array ); list_tmp = new ArrayList<>( src.size() ); addWithFilter( list_tmp, src ); // @@ -1502,7 +1469,7 @@ import jp.juggler.subwaytooter.util.Utils; break; } - src = TootStatus.parseList( context, access_info, result2.array ); + src = parser.statusList( result2.array ); addWithFilter( list_tmp, src ); @@ -1564,7 +1531,7 @@ import jp.juggler.subwaytooter.util.Utils; if( result != null && result.array != null ){ saveRange( result, true, true ); // - TootNotification.List src = TootNotification.parseList( context, access_info, result.array ); + TootNotification.List src = parser.notificationList( result.array ); list_tmp = new ArrayList<>( src.size() ); addWithFilter( list_tmp, src ); // @@ -1605,7 +1572,7 @@ import jp.juggler.subwaytooter.util.Utils; break; } - src = TootNotification.parseList( context, access_info, result2.array ); + src = parser.notificationList( result2.array ); addWithFilter( list_tmp, src ); @@ -1741,7 +1708,7 @@ import jp.juggler.subwaytooter.util.Utils; result = client.request( String.format( Locale.JAPAN, PATH_STATUSES, status_id ) ); if( result == null || result.object == null ) return result; - TootStatus target_status = TootStatus.parse( context, access_info, result.object ); + TootStatus target_status = parser.status( result.object ); if( target_status == null ){ return new TootApiResult( "TootStatus parse failed." ); } @@ -1753,7 +1720,7 @@ import jp.juggler.subwaytooter.util.Utils; if( result == null || result.object == null ) return result; // 一つのリストにまとめる - TootContext conversation_context = TootContext.parse( context, access_info, result.object ); + TootContext conversation_context = parser.context( result.object ); if( conversation_context != null ){ list_tmp = new ArrayList<>( 1 + conversation_context.ancestors.size() + conversation_context.descendants.size() ); if( conversation_context.ancestors != null ) @@ -1792,7 +1759,7 @@ import jp.juggler.subwaytooter.util.Utils; result = client.request( path ); if( result == null || result.object == null ) return result; - TootResults tmp = TootResults.parse( context, access_info, result.object ); + TootResults tmp = parser.results( result.object ); if( tmp != null ){ list_tmp = new ArrayList<>(); list_tmp.addAll( tmp.hashtags ); @@ -1831,7 +1798,7 @@ import jp.juggler.subwaytooter.util.Utils; // max_id の更新 max_id = MSPClient.getMaxId( result.array, max_id ); // リストデータの用意 - MSPToot.List search_result = MSPToot.parseList( context, access_info, result.array ); + MSPToot.List search_result = MSPToot.parseList(parser, result.array ); if( search_result != null ){ list_tmp = new ArrayList<>(); addWithFilter( list_tmp, search_result ); @@ -1867,7 +1834,7 @@ import jp.juggler.subwaytooter.util.Utils; // max_id の更新 max_id = TSClient.getMaxId( result.object, max_id ); // リストデータの用意 - TSToot.List search_result = TSToot.parseList( context, access_info, result.object ); + TSToot.List search_result = TSToot.parseList( parser, result.object ); list_tmp = new ArrayList<>(); addWithFilter( list_tmp, search_result ); if( search_result.isEmpty() ){ @@ -2194,6 +2161,7 @@ import jp.juggler.subwaytooter.util.Utils; mRefreshLoadingError = null; @SuppressLint("StaticFieldLeak") AsyncTask< Void, Void, TootApiResult > task = this.last_task = new AsyncTask< Void, Void, TootApiResult >() { + TootParser parser = new TootParser( context, access_info).setHighlightTrie( highlight_trie ); TootApiResult getAccountList( TootApiClient client, String path_base ){ long time_start = SystemClock.elapsedRealtime(); @@ -2417,7 +2385,7 @@ import jp.juggler.subwaytooter.util.Utils; if( result != null && result.array != null ){ saveRange( result, bBottom, ! bBottom ); list_tmp = new ArrayList<>(); - TootNotification.List src = TootNotification.parseList( context, access_info, result.array ); + TootNotification.List src = parser.notificationList( result.array ); addWithFilter( list_tmp, src ); if( ! bBottom ){ @@ -2462,7 +2430,7 @@ import jp.juggler.subwaytooter.util.Utils; break; } - src = TootNotification.parseList( context, access_info, result2.array ); + src = parser.notificationList( result2.array ); if( ! src.isEmpty() ){ addWithFilter( list_tmp, src ); PollingWorker.injectData( context, access_info.db_id, src ); @@ -2507,7 +2475,7 @@ import jp.juggler.subwaytooter.util.Utils; break; } - src = TootNotification.parseList( context, access_info, result2.array ); + src = parser.notificationList( result2.array ); addWithFilter( list_tmp, src ); @@ -2524,6 +2492,7 @@ import jp.juggler.subwaytooter.util.Utils; ArrayList< Object > list_tmp; TootApiResult getStatusList( TootApiClient client, String path_base ){ + long time_start = SystemClock.elapsedRealtime(); char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' ); @@ -2532,7 +2501,7 @@ import jp.juggler.subwaytooter.util.Utils; TootApiResult result = client.request( addRange( bBottom, path_base ) ); if( result != null && result.array != null ){ saveRange( result, bBottom, ! bBottom ); - TootStatus.List src = TootStatus.parseList( context, access_info, result.array ); + TootStatus.List src = parser.statusList( result.array ); list_tmp = new ArrayList<>(); addWithFilter( list_tmp, src ); @@ -2576,7 +2545,7 @@ import jp.juggler.subwaytooter.util.Utils; break; } - src = TootStatus.parseList( context, access_info, result2.array ); + src = parser.statusList( result2.array ); addWithFilter( list_tmp, src ); @@ -2632,7 +2601,7 @@ import jp.juggler.subwaytooter.util.Utils; break; } - src = TootStatus.parseList( context, access_info, result2.array ); + src = parser.statusList( result2.array ); addWithFilter( list_tmp, src ); } } @@ -2773,7 +2742,7 @@ import jp.juggler.subwaytooter.util.Utils; // max_id の更新 max_id = MSPClient.getMaxId( result.array, max_id ); // リストデータの用意 - MSPToot.List search_result = MSPToot.parseList( context, access_info, result.array ); + MSPToot.List search_result = MSPToot.parseList( parser, result.array ); if( search_result != null ){ list_tmp = new ArrayList<>(); addWithFilter( list_tmp, search_result ); @@ -2813,7 +2782,7 @@ import jp.juggler.subwaytooter.util.Utils; // max_id の更新 max_id = TSClient.getMaxId( result.object, max_id ); // リストデータの用意 - TSToot.List search_result = TSToot.parseList( context, access_info, result.object ); + TSToot.List search_result = TSToot.parseList( parser, result.object ); list_tmp = new ArrayList<>(); addWithFilter( list_tmp, search_result ); } @@ -2863,6 +2832,8 @@ import jp.juggler.subwaytooter.util.Utils; return; } + + // 事前にスクロール位置を覚えておく ScrollPosition sp = null; ColumnViewHolder holder = getViewHolder(); @@ -2879,6 +2850,16 @@ import jp.juggler.subwaytooter.util.Utils; } }else{ + for( Object o : list_new ){ + if( o instanceof TootStatusLike){ + TootStatusLike s = (TootStatusLike) o; + if( s.highlight_sound != null ){ + App1.sound( s.highlight_sound ); + break; + } + } + } + int status_index = - 1; for( int i = 0, ie = list_new.size() ; i < ie ; ++ i ){ Object o = list_new.get( i ); @@ -2949,6 +2930,9 @@ import jp.juggler.subwaytooter.util.Utils; final String since_id = gap.since_id; ArrayList< Object > list_tmp; + TootParser parser = new TootParser( context, access_info).setHighlightTrie( highlight_trie ); + + TootApiResult getAccountList( TootApiClient client, String path_base ){ long time_start = SystemClock.elapsedRealtime(); char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' ); @@ -3076,7 +3060,7 @@ import jp.juggler.subwaytooter.util.Utils; } result = r2; - TootNotification.List src = TootNotification.parseList( context, access_info, r2.array ); + TootNotification.List src = parser.notificationList( r2.array ); if( src.isEmpty() ){ log.d( "gap-notification: empty." ); @@ -3132,7 +3116,7 @@ import jp.juggler.subwaytooter.util.Utils; // 成功した場合はそれを返したい result = r2; - TootStatus.List src = TootStatus.parseList( context, access_info, r2.array ); + TootStatus.List src = parser.statusList( r2.array ); if( src.size() == 0 ){ // 直前の取得でカラのデータが帰ってきたら終了 log.d( "gap-statuses: empty." ); @@ -3324,6 +3308,7 @@ import jp.juggler.subwaytooter.util.Utils; task.executeOnExecutor( App1.task_executor ); } + private static final int heightSpec = View.MeasureSpec.makeMeasureSpec( 0, View.MeasureSpec.UNSPECIFIED ); private static int getListItemHeight( ListView listView, int idx ){ @@ -3465,6 +3450,16 @@ import jp.juggler.subwaytooter.util.Utils; } } + for( Object o : list_new ){ + if( o instanceof TootStatusLike){ + TootStatusLike s = (TootStatusLike) o; + if( s.highlight_sound != null ){ + App1.sound( s.highlight_sound ); + break; + } + } + } + list_data.addAll( 0, list_new ); fireShowContent(); int added = list_new.size(); @@ -3702,6 +3697,7 @@ import jp.juggler.subwaytooter.util.Utils; app_state.stream_reader.register( access_info , stream_path + , highlight_trie , this ); } diff --git a/app/src/main/java/jp/juggler/subwaytooter/PollingWorker.java b/app/src/main/java/jp/juggler/subwaytooter/PollingWorker.java index e11a6c83..144a7842 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/PollingWorker.java +++ b/app/src/main/java/jp/juggler/subwaytooter/PollingWorker.java @@ -51,6 +51,7 @@ import jp.juggler.subwaytooter.api.TootApiClient; import jp.juggler.subwaytooter.api.TootApiResult; import jp.juggler.subwaytooter.api.entity.TootNotification; import jp.juggler.subwaytooter.api.entity.TootStatus; +import jp.juggler.subwaytooter.api.TootParser; import jp.juggler.subwaytooter.table.AcctColor; import jp.juggler.subwaytooter.table.MutedApp; import jp.juggler.subwaytooter.table.MutedWord; @@ -1031,6 +1032,8 @@ public class PollingWorker { ){ nr = NotificationTracking.load( account.db_id ); + TootParser parser = new TootParser( context,account ); + // まずキャッシュされたデータを処理する if( nr.last_data != null ){ try{ @@ -1038,7 +1041,7 @@ public class PollingWorker { for( int i = array.length() - 1 ; i >= 0 ; -- i ){ if( job.isJobCancelled() ) return; JSONObject src = array.optJSONObject( i ); - update_sub( src, data_list, muted_app, muted_word ); + update_sub( src, data_list, muted_app, muted_word ,parser); } }catch( JSONException ex ){ log.trace( ex ); @@ -1054,6 +1057,7 @@ public class PollingWorker { client.setAccount( account ); + for( int nTry = 0 ; nTry < 4 ; ++ nTry ){ if( job.isJobCancelled() ) return; @@ -1071,7 +1075,7 @@ public class PollingWorker { JSONArray array = result.array; for( int i = array.length() - 1 ; i >= 0 ; -- i ){ JSONObject src = array.optJSONObject( i ); - update_sub( src, data_list, muted_app, muted_word ); + update_sub( src, data_list, muted_app, muted_word ,parser); } }catch( JSONException ex ){ log.trace( ex ); @@ -1130,6 +1134,7 @@ public class PollingWorker { , @NonNull ArrayList< Data > data_list , @NonNull HashSet< String > muted_app , @NonNull WordTrieTree muted_word + , @NonNull TootParser parser ) throws JSONException{ if( nr.nid_read == 0 || nr.nid_show == 0 ){ @@ -1168,7 +1173,7 @@ public class PollingWorker { return; } - TootNotification notification = TootNotification.parse( context, account, src ); + TootNotification notification = parser.notification( src ); if( notification == null ){ return; } diff --git a/app/src/main/java/jp/juggler/subwaytooter/StreamReader.java b/app/src/main/java/jp/juggler/subwaytooter/StreamReader.java index 307ccb8c..13b086c9 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/StreamReader.java +++ b/app/src/main/java/jp/juggler/subwaytooter/StreamReader.java @@ -21,11 +21,11 @@ import jp.juggler.subwaytooter.api.TootApiClient; import jp.juggler.subwaytooter.api.TootApiResult; import jp.juggler.subwaytooter.api.TootTask; import jp.juggler.subwaytooter.api.TootTaskRunner; -import jp.juggler.subwaytooter.api.entity.TootNotification; -import jp.juggler.subwaytooter.api.entity.TootStatus; +import jp.juggler.subwaytooter.api.TootParser; import jp.juggler.subwaytooter.table.SavedAccount; import jp.juggler.subwaytooter.util.LogCategory; import jp.juggler.subwaytooter.util.Utils; +import jp.juggler.subwaytooter.util.WordTrieTree; import okhttp3.Request; import okhttp3.Response; import okhttp3.WebSocket; @@ -35,7 +35,7 @@ import okhttp3.WebSocketListener; static final LogCategory log = new LogCategory( "StreamReader" ); static final Pattern reNumber = Pattern.compile( "([-]?\\d+)" ); - static final Pattern reAuthorizeError = Pattern.compile( "authorize",Pattern.CASE_INSENSITIVE ); + static final Pattern reAuthorizeError = Pattern.compile( "authorize", Pattern.CASE_INSENSITIVE ); interface Callback { void onStreamingMessage( String event_type, Object o ); @@ -45,10 +45,16 @@ import okhttp3.WebSocketListener; final SavedAccount access_info; final String end_point; final LinkedList< Callback > callback_list = new LinkedList<>(); + final TootParser parser; - Reader( SavedAccount access_info, String end_point ){ + Reader( SavedAccount access_info, String end_point , WordTrieTree highlight_trie){ this.access_info = access_info; this.end_point = end_point; + this.parser = new TootParser( context, access_info ).setHighlightTrie( highlight_trie ); + } + + synchronized void updateHighlight( WordTrieTree highlight_trie ){ + this.parser.setHighlightTrie( highlight_trie ); } synchronized void addCallback( @NonNull Callback stream_callback ){ @@ -121,7 +127,7 @@ import okhttp3.WebSocketListener; static final String PAYLOAD = "payload"; // ストリーミングAPIのペイロード部分をTootStatus,TootNotification,整数IDのどれかに解釈する - private Object parsePayload( @NonNull String event, @NonNull JSONObject parent, @NonNull String parent_text ){ + synchronized private Object parsePayload( @NonNull String event, @NonNull JSONObject parent, @NonNull String parent_text ){ try{ if( parent.isNull( PAYLOAD ) ){ return null; @@ -135,11 +141,11 @@ import okhttp3.WebSocketListener; case "update": // ここを通るケースはまだ確認できていない - return TootStatus.parse( context, access_info, src ); + return parser.status( src ); case "notification": // ここを通るケースはまだ確認できていない - return TootNotification.parse( context, access_info, src ); + return parser.notification( src ); default: // ここを通るケースはまだ確認できていない @@ -161,11 +167,11 @@ import okhttp3.WebSocketListener; switch( event ){ case "update": // 2017/8/24 18:37 mastodon.juggler.jpでここを通った - return TootStatus.parse( context, access_info, src ); + return parser.status( src ); case "notification": // 2017/8/24 18:37 mastodon.juggler.jpでここを通った - return TootNotification.parse( context, access_info, src ); + return parser.notification( src ); default: // ここを通るケースはまだ確認できていない @@ -222,14 +228,13 @@ import okhttp3.WebSocketListener; public void onFailure( WebSocket webSocket, Throwable ex, Response response ){ log.e( ex, "WebSocket onFailure. url=%s .", webSocket.request().url() ); - bListening.set( false ); handler.removeCallbacks( proc_reconnect ); - + if( ex instanceof ProtocolException ){ String msg = ex.getMessage(); - if(msg != null && reAuthorizeError.matcher( msg).find() ){ - log.e("seems old instance that does not support streaming public timeline without access token. don't retry..."); + if( msg != null && reAuthorizeError.matcher( msg ).find() ){ + log.e( "seems old instance that does not support streaming public timeline without access token. don't retry..." ); return; } } @@ -285,16 +290,17 @@ import okhttp3.WebSocketListener; this.handler = handler; } - private Reader prepareReader( @NonNull SavedAccount access_info, @NonNull String end_point ){ + private Reader prepareReader( @NonNull SavedAccount access_info, @NonNull String end_point ,WordTrieTree highlight_trie ){ synchronized( reader_list ){ for( Reader reader : reader_list ){ if( reader.access_info.db_id == access_info.db_id && reader.end_point.equals( end_point ) ){ + if( highlight_trie != null ) reader.updateHighlight( highlight_trie ); return reader; } } - Reader reader = new Reader( access_info, end_point ); + Reader reader = new Reader( access_info, end_point ,highlight_trie); reader_list.add( reader ); return reader; } @@ -332,9 +338,9 @@ import okhttp3.WebSocketListener; } // onResume や ロード完了ののタイミングで登録される - void register( @NonNull SavedAccount access_info, @NonNull String end_point, @NonNull Callback stream_callback ){ + void register( @NonNull SavedAccount access_info, @NonNull String end_point, @Nullable WordTrieTree highlight_trie , @NonNull Callback stream_callback ){ - final Reader reader = prepareReader( access_info, end_point ); + final Reader reader = prepareReader( access_info, end_point ,highlight_trie); reader.addCallback( stream_callback ); if( ! reader.bListening.get() ){ diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Toot.java b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Toot.java index d70707f0..f666b74d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Toot.java +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Toot.java @@ -21,6 +21,7 @@ import jp.juggler.subwaytooter.api.TootTaskRunner; import jp.juggler.subwaytooter.api.entity.TootResults; import jp.juggler.subwaytooter.api.entity.TootStatus; import jp.juggler.subwaytooter.api.entity.TootStatusLike; +import jp.juggler.subwaytooter.api.TootParser; import jp.juggler.subwaytooter.api_msp.entity.MSPToot; import jp.juggler.subwaytooter.api_tootsearch.entity.TSToot; import jp.juggler.subwaytooter.dialog.AccountPicker; @@ -95,7 +96,7 @@ public class Action_Toot { return result; } target_status = null; - TootResults tmp = TootResults.parse( activity, access_info, result.object ); + TootResults tmp = new TootParser( activity, access_info).results( result.object ); if( tmp != null ){ if( tmp.statuses != null && ! tmp.statuses.isEmpty() ){ target_status = tmp.statuses.get( 0 ); @@ -125,7 +126,7 @@ public class Action_Toot { ) , request_builder ); if( result != null && result.object != null ){ - new_status = TootStatus.parse( activity, access_info, result.object ); + new_status = new TootParser( activity, access_info).status( result.object ); } return result; @@ -265,6 +266,8 @@ public class Action_Toot { new TootTaskRunner( activity, false ).run( access_info, new TootTask() { @Override public TootApiResult background( @NonNull TootApiClient client ){ + TootParser parser = new TootParser( activity, access_info); + TootApiResult result; TootStatusLike target_status; @@ -278,7 +281,7 @@ public class Action_Toot { return result; } target_status = null; - TootResults tmp = TootResults.parse( activity, access_info, result.object ); + TootResults tmp = parser.results( result.object ); if( tmp != null ){ if( tmp.statuses != null && ! tmp.statuses.isEmpty() ){ target_status = tmp.statuses.get( 0 ); @@ -306,7 +309,7 @@ public class Action_Toot { if( result != null && result.object != null ){ - new_status = TootStatus.parse( activity, access_info, result.object ); + new_status = parser .status( result.object ); // reblogはreblogを表すStatusを返す // unreblogはreblogしたStatusを返す @@ -612,7 +615,7 @@ public class Action_Toot { path = path + "&resolve=1"; result = client.request( path ); if( result != null && result.object != null ){ - TootResults tmp = TootResults.parse( activity, access_info, result.object ); + TootResults tmp = new TootParser( activity, access_info).results( result.object ); if( tmp != null && tmp.statuses != null && ! tmp.statuses.isEmpty() ){ TootStatus status = tmp.statuses.get( 0 ); local_status_id = status.id; @@ -672,7 +675,7 @@ public class Action_Toot { ) , request_builder ); if( result != null && result.object != null ){ - new_status = TootStatus.parse( activity, access_info, result.object ); + new_status = new TootParser( activity, access_info).status( result.object ); } return result; @@ -759,7 +762,7 @@ public class Action_Toot { TootApiResult result = client.request( path ); if( result != null && result.object != null ){ - TootResults tmp = TootResults.parse( activity, access_info, result.object ); + TootResults tmp = new TootParser( activity, access_info).results( result.object ); if( tmp != null && tmp.statuses != null && ! tmp.statuses.isEmpty() ){ local_status = tmp.statuses.get( 0 ); log.d( "status id conversion %s => %s", remote_status_url, local_status.id ); @@ -808,7 +811,7 @@ public class Action_Toot { ); if( result != null && result.object != null ){ - local_status = TootStatus.parse( activity, access_info, result.object ); + local_status = new TootParser( activity, access_info).status( result.object ); } return result; diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_User.java b/app/src/main/java/jp/juggler/subwaytooter/action/Action_User.java index aba67f5c..f7fc1c20 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_User.java +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_User.java @@ -22,6 +22,7 @@ import jp.juggler.subwaytooter.api.entity.TootAccount; import jp.juggler.subwaytooter.api.entity.TootRelationShip; import jp.juggler.subwaytooter.api.entity.TootResults; import jp.juggler.subwaytooter.api.entity.TootStatus; +import jp.juggler.subwaytooter.api.TootParser; import jp.juggler.subwaytooter.dialog.AccountPicker; import jp.juggler.subwaytooter.dialog.ReportForm; import jp.juggler.subwaytooter.table.AcctColor; @@ -224,7 +225,7 @@ public class Action_User { if( result != null && result.object != null ){ - TootResults tmp = TootResults.parse( activity, access_info, result.object ); + TootResults tmp = new TootParser( activity, access_info ).results( result.object ); if( tmp != null ){ if( tmp.accounts != null && ! tmp.accounts.isEmpty() ){ who_local = tmp.accounts.get( 0 ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootParser.java b/app/src/main/java/jp/juggler/subwaytooter/api/TootParser.java new file mode 100644 index 00000000..cac95b79 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootParser.java @@ -0,0 +1,76 @@ +package jp.juggler.subwaytooter.api; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONObject; + +import jp.juggler.subwaytooter.api.entity.TootAccount; +import jp.juggler.subwaytooter.api.entity.TootContext; +import jp.juggler.subwaytooter.api.entity.TootNotification; +import jp.juggler.subwaytooter.api.entity.TootResults; +import jp.juggler.subwaytooter.api.entity.TootStatus; +import jp.juggler.subwaytooter.table.SavedAccount; +import jp.juggler.subwaytooter.util.WordTrieTree; + +public class TootParser { + @NonNull public final Context context; + @NonNull public final SavedAccount access_info; + + public TootParser( @NonNull Context context, @NonNull SavedAccount access_info ){ + this.context = context; + this.access_info = access_info; + } + + //////////////////////////////////////////////////////// + // parser options + + // プロフィールカラムからpinned TL を読んだ時だけ真 + public boolean isPinned; + + public TootParser setPinned( boolean isPinned ){ + this.isPinned = isPinned; + return this; + } + + @Nullable public WordTrieTree highlight_trie; + public TootParser setHighlightTrie( @Nullable WordTrieTree highlight_trie ){ + this.highlight_trie = highlight_trie; + return this; + } + + ///////////////////////////////////////////////////////// + // parser methods + + @Nullable public TootAccount account( @Nullable JSONObject src ){ + return TootAccount.parse( context,access_info, src ); + } + + @Nullable public TootStatus status( @Nullable JSONObject src ){ + return TootStatus.parse( this, src ); + } + + @NonNull public TootStatus.List statusList( @Nullable JSONArray array ){ + return TootStatus.parseList( this, array ); + } + + @Nullable public TootNotification notification( @Nullable JSONObject src ){ + return TootNotification.parse(this,src); + } + + @NonNull public TootNotification.List notificationList( @Nullable JSONArray src ){ + return TootNotification.parseList( this, src ); + } + + @Nullable public TootResults results( @Nullable JSONObject src ){ + return TootResults.parse( this, src ); + } + + @Nullable public TootContext context( @Nullable JSONObject src ){ + return TootContext.parse(this, src ); + } + + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootContext.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootContext.java index 45c8e5bc..48295a5e 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootContext.java +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootContext.java @@ -1,12 +1,11 @@ package jp.juggler.subwaytooter.api.entity; -import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import org.json.JSONObject; -import jp.juggler.subwaytooter.table.SavedAccount; +import jp.juggler.subwaytooter.api.TootParser; import jp.juggler.subwaytooter.util.LogCategory; public class TootContext { @@ -19,12 +18,12 @@ public class TootContext { public TootStatus.List descendants; @Nullable - public static TootContext parse( @NonNull Context context, @NonNull SavedAccount access_info, JSONObject src ){ + public static TootContext parse( @NonNull TootParser parser , JSONObject src ){ if( src == null ) return null; try{ TootContext dst = new TootContext(); - dst.ancestors = TootStatus.parseList( context, access_info, src.optJSONArray( "ancestors" ) ); - dst.descendants = TootStatus.parseList( context, access_info, src.optJSONArray( "descendants" ) ); + dst.ancestors = TootStatus.parseList( parser, src.optJSONArray( "ancestors" ) ); + dst.descendants = TootStatus.parseList( parser, src.optJSONArray( "descendants" ) ); return dst; }catch( Throwable ex ){ log.trace( ex ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.java index c972bad2..5dfd24f1 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.java +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.java @@ -1,6 +1,5 @@ package jp.juggler.subwaytooter.api.entity; -import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -9,7 +8,7 @@ import org.json.JSONObject; import java.util.ArrayList; -import jp.juggler.subwaytooter.table.SavedAccount; +import jp.juggler.subwaytooter.api.TootParser; import jp.juggler.subwaytooter.util.LogCategory; import jp.juggler.subwaytooter.util.Utils; @@ -41,16 +40,16 @@ public class TootNotification extends TootId { public JSONObject json; @Nullable - public static TootNotification parse( @NonNull Context context, @NonNull SavedAccount access_info, JSONObject src ){ + public static TootNotification parse( @NonNull TootParser parser, JSONObject src ){ if( src == null ) return null; try{ TootNotification dst = new TootNotification(); dst.json = src; - dst.id = Utils.optLongX(src, "id" ); + dst.id = Utils.optLongX( src, "id" ); dst.type = Utils.optStringX( src, "type" ); dst.created_at = Utils.optStringX( src, "created_at" ); - dst.account = TootAccount.parse( context, access_info, src.optJSONObject( "account" ) ); - dst.status = TootStatus.parse( context, access_info, src.optJSONObject( "status" ) ); + dst.account = TootAccount.parse( parser.context, parser.access_info, src.optJSONObject( "account" ) ); + dst.status = TootStatus.parse( parser, src.optJSONObject( "status" ) ); dst.time_created_at = TootStatus.parseTime( dst.created_at ); @@ -73,7 +72,7 @@ public class TootNotification extends TootId { } @NonNull - public static List parseList( @NonNull Context context, @NonNull SavedAccount access_info, JSONArray array ){ + public static List parseList( @NonNull TootParser parser, JSONArray array ){ List result = new List(); if( array != null ){ int array_size = array.length(); @@ -81,7 +80,7 @@ public class TootNotification extends TootId { for( int i = 0 ; i < array_size ; ++ i ){ JSONObject src = array.optJSONObject( i ); if( src == null ) continue; - TootNotification item = parse( context, access_info, src ); + TootNotification item = parse( parser, src ); if( item != null ) result.add( item ); } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootResults.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootResults.java index 27c5fafe..9f7c4280 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootResults.java +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootResults.java @@ -1,6 +1,5 @@ package jp.juggler.subwaytooter.api.entity; -import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -8,7 +7,7 @@ import org.json.JSONObject; import java.util.ArrayList; -import jp.juggler.subwaytooter.table.SavedAccount; +import jp.juggler.subwaytooter.api.TootParser; import jp.juggler.subwaytooter.util.LogCategory; import jp.juggler.subwaytooter.util.Utils; @@ -26,12 +25,12 @@ public class TootResults { public ArrayList< String > hashtags; @Nullable - public static TootResults parse( @NonNull Context context, @NonNull SavedAccount access_info, JSONObject src ){ + public static TootResults parse( @NonNull TootParser parser, JSONObject src ){ try{ if( src == null ) return null; TootResults dst = new TootResults(); - dst.accounts = TootAccount.parseList( context, access_info, src.optJSONArray( "accounts" ) ); - dst.statuses = TootStatus.parseList( context, access_info, src.optJSONArray( "statuses" ) ); + dst.accounts = TootAccount.parseList( parser.context, parser.access_info, src.optJSONArray( "accounts" ) ); + dst.statuses = TootStatus.parseList( parser, src.optJSONArray( "statuses" ) ); dst.hashtags = Utils.parseStringArray( src.optJSONArray( "hashtags" ) ); return dst; }catch( Throwable ex ){ diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.java index a76264bf..75ba28dd 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.java +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.java @@ -23,6 +23,7 @@ import java.util.regex.Pattern; import jp.juggler.subwaytooter.App1; import jp.juggler.subwaytooter.Pref; import jp.juggler.subwaytooter.R; +import jp.juggler.subwaytooter.api.TootParser; import jp.juggler.subwaytooter.table.SavedAccount; import jp.juggler.subwaytooter.util.DecodeOptions; import jp.juggler.subwaytooter.util.HTMLDecoder; @@ -88,28 +89,19 @@ public class TootStatus extends TootStatusLike { public NicoEnquete enquete; @Nullable - public static TootStatus parse( @NonNull Context context, @NonNull SavedAccount access_info, JSONObject src ){ - return parse( context,access_info,src,false); - } - - @Nullable - public static TootStatus parse( @NonNull Context context, @NonNull SavedAccount access_info, JSONObject src ,boolean bPinned){ - /* - bPinned 引数がtrueになるのはプロフィールカラムからpinned TL を読んだ時だけである - */ + public static TootStatus parse( @NonNull TootParser parser, @Nullable JSONObject src ){ if( src == null ) return null; - // log.d( "parse: %s", src.toString() ); try{ TootStatus status = new TootStatus(); status.json = src; // 絵文字マップは割と最初の方で読み込んでおきたい - status.custom_emojis = CustomEmoji.parseMap( src.optJSONArray( "emojis" ),access_info.host); + status.custom_emojis = CustomEmoji.parseMap( src.optJSONArray( "emojis" ), parser.access_info.host ); status.profile_emojis = NicoProfileEmoji.parseMap( src.optJSONArray( "profile_emojis" ) ); - - status.account = TootAccount.parse( context, access_info, src.optJSONObject( "account" ) ); + + status.account = TootAccount.parse( parser.context, parser.access_info, src.optJSONObject( "account" ) ); if( status.account == null ) return null; @@ -117,20 +109,21 @@ public class TootStatus extends TootStatusLike { status.uri = Utils.optStringX( src, "uri" ); status.url = Utils.optStringX( src, "url" ); - status.host_access = access_info.host; + status.host_access = parser.access_info.host; status.host_original = status.account.getAcctHost(); if( status.host_original == null ){ - status.host_original = access_info.host; + status.host_original = parser.access_info.host; } status.in_reply_to_id = Utils.optStringX( src, "in_reply_to_id" ); // null status.in_reply_to_account_id = Utils.optStringX( src, "in_reply_to_account_id" ); // null - status.reblog = TootStatus.parse( context, access_info, src.optJSONObject( "reblog" ) ,false ); - /* Pinned TL を取得した時にreblogが登場することはないので、reblogをパースするときのbPinnedはfalseでよい */ - status.content = Utils.optStringX( src, "content" ); + + // Pinned TL を取得した時にreblogが登場することはないので、reblogについてpinned 状態を気にする必要はない + status.reblog = TootStatus.parse( parser, src.optJSONObject( "reblog" ) ); + status.created_at = Utils.optStringX( src, "created_at" ); // "2017-04-16T09:37:14.000Z" - status.reblogs_count = Utils.optLongX(src, "reblogs_count" ); - status.favourites_count = Utils.optLongX(src, "favourites_count" ); + status.reblogs_count = Utils.optLongX( src, "reblogs_count" ); + status.favourites_count = Utils.optLongX( src, "favourites_count" ); status.reblogged = src.optBoolean( "reblogged" ); status.favourited = src.optBoolean( "favourited" ); status.sensitive = src.optBoolean( "sensitive" ); // false @@ -140,29 +133,21 @@ public class TootStatus extends TootStatusLike { status.tags = TootTag.parseList( src.optJSONArray( "tags" ) ); status.application = TootApplication.parse( src.optJSONObject( "application" ) ); // null - status.pinned = bPinned || src.optBoolean( "pinned" ); + status.pinned = parser.isPinned || src.optBoolean( "pinned" ); - status.setSpoilerText( context, Utils.optStringX( src, "spoiler_text" ) ); + status.setSpoilerText( parser, Utils.optStringX( src, "spoiler_text" ) ); status.muted = src.optBoolean( "muted" ); status.language = Utils.optStringX( src, "language" ); - status.time_created_at = parseTime( status.created_at ); - status.decoded_content = new DecodeOptions() - .setShort( true ) - .setDecodeEmoji( true) - .setAttachment( status.media_attachments ) - .setCustomEmojiMap( status.custom_emojis ) - .setProfileEmojis( status.profile_emojis ) - .setLinkTag( status ) - .decodeHTML( context, access_info, status.content ); - + + status.setContent( parser, status.media_attachments, Utils.optStringX( src, "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 ); + status.decoded_mentions = HTMLDecoder.decodeMentions( parser.access_info, status.mentions, status ); + status.enquete = NicoEnquete.parse( parser.context, parser.access_info, status.media_attachments, Utils.optStringX( src, "enquete" ), status.id, status.time_created_at, status ); return status; }catch( Throwable ex ){ @@ -173,12 +158,7 @@ public class TootStatus extends TootStatusLike { } @NonNull - public static List parseList( @NonNull Context context, @NonNull SavedAccount access_info, JSONArray array ){ - return parseList( context,access_info,array,false ); - } - - @NonNull - public static List parseList( @NonNull Context context, @NonNull SavedAccount access_info, JSONArray array ,boolean bPinned){ + public static List parseList( @NonNull TootParser parser, JSONArray array ){ List result = new List(); if( array != null ){ int array_size = array.length(); @@ -186,7 +166,7 @@ public class TootStatus extends TootStatusLike { for( int i = 0 ; i < array_size ; ++ i ){ JSONObject src = array.optJSONObject( i ); if( src == null ) continue; - TootStatus item = parse( context, access_info, src ,bPinned ); + TootStatus item = parse( parser, src ); if( item != null ) result.add( item ); } } @@ -296,11 +276,11 @@ public class TootStatus extends TootStatusLike { } // word mute - if( decoded_content != null && muted_word.containsWord( decoded_content.toString() ) ){ + if( muted_word.matchShort( decoded_content ) ){ return true; } - if( decoded_spoiler_text != null && muted_word.containsWord( decoded_spoiler_text.toString() ) ){ + if( muted_word.matchShort( decoded_spoiler_text ) ){ return true; } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatusLike.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatusLike.java index 3454296b..0573f9b2 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatusLike.java +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatusLike.java @@ -9,10 +9,14 @@ import android.text.TextUtils; import org.json.JSONObject; import java.lang.ref.WeakReference; +import java.util.ArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; +import jp.juggler.subwaytooter.api.TootParser; +import jp.juggler.subwaytooter.api_msp.entity.MSPToot; import jp.juggler.subwaytooter.api_tootsearch.entity.TSToot; +import jp.juggler.subwaytooter.table.HighlightWord; import jp.juggler.subwaytooter.table.SavedAccount; import jp.juggler.subwaytooter.util.DecodeOptions; import jp.juggler.subwaytooter.util.LogCategory; @@ -20,7 +24,7 @@ import jp.juggler.subwaytooter.util.Utils; public abstract class TootStatusLike extends TootId { - static final LogCategory log = new LogCategory("TootStatusLike"); + static final LogCategory log = new LogCategory( "TootStatusLike" ); //URL to the status page (can be remote) public String url; @@ -95,8 +99,9 @@ public abstract class TootStatusLike extends TootId { private static final Pattern reWhitespace = Pattern.compile( "[\\s\\t\\x0d\\x0a]+" ); + @Nullable public HighlightWord highlight_sound; - public void setSpoilerText( Context context, String sv ){ + public void setSpoilerText( @NonNull TootParser parser, String sv ){ if( TextUtils.isEmpty( sv ) ){ this.spoiler_text = null; this.decoded_spoiler_text = null; @@ -105,10 +110,38 @@ public abstract class TootStatusLike extends TootId { // remove white spaces sv = reWhitespace.matcher( this.spoiler_text ).replaceAll( " " ); // decode emoji code - this.decoded_spoiler_text = new DecodeOptions() + + DecodeOptions options = new DecodeOptions() .setCustomEmojiMap( custom_emojis ) .setProfileEmojis( this.profile_emojis ) - .decodeEmoji( context, sv ); + .setHighlightTrie(parser.highlight_trie) + ; + + this.decoded_spoiler_text = options.decodeEmoji( parser.context, sv ); + + if( options.highlight_sound != null && this.highlight_sound == null ){ + this.highlight_sound = options.highlight_sound; + } + } + } + + public void setContent( @NonNull TootParser parser, TootAttachment.List list_attachment, @Nullable String content ){ + this.content = content; + + DecodeOptions options =new DecodeOptions() + .setShort( true ) + .setDecodeEmoji( true ) + .setCustomEmojiMap( this.custom_emojis ) + .setProfileEmojis( this.profile_emojis ) + .setLinkTag( this ) + .setAttachment( list_attachment ) + .setHighlightTrie(parser.highlight_trie) + ; + + this.decoded_content = options.decodeHTML( parser.context, parser.access_info, content ); + + if( options.highlight_sound != null && this.highlight_sound == null ){ + this.highlight_sound = options.highlight_sound; } } @@ -120,8 +153,8 @@ public abstract class TootStatusLike extends TootId { public Spannable decoded_spoiler_text; public int originalLineCount; } - public AutoCW auto_cw; + public AutoCW auto_cw; // OStatus static final Pattern reTootUriOS = Pattern.compile( "tag:([^,]*),[^:]*:objectId=(\\d+):objectType=Status", Pattern.CASE_INSENSITIVE ); @@ -132,7 +165,7 @@ public abstract class TootStatusLike extends TootId { // 投稿元タンスでのステータスIDを調べる public long parseStatusId(){ - return TootStatusLike.parseStatusId(this); + return TootStatusLike.parseStatusId( this ); } // 投稿元タンスでのステータスIDを調べる diff --git a/app/src/main/java/jp/juggler/subwaytooter/api_msp/entity/MSPAccount.java b/app/src/main/java/jp/juggler/subwaytooter/api_msp/entity/MSPAccount.java index da3a9ea6..02ffa8d1 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api_msp/entity/MSPAccount.java +++ b/app/src/main/java/jp/juggler/subwaytooter/api_msp/entity/MSPAccount.java @@ -9,6 +9,7 @@ import org.json.JSONObject; import java.util.regex.Matcher; +import jp.juggler.subwaytooter.api.TootParser; import jp.juggler.subwaytooter.api.entity.TootAccount; import jp.juggler.subwaytooter.table.SavedAccount; import jp.juggler.subwaytooter.util.DecodeOptions; @@ -19,7 +20,7 @@ public class MSPAccount extends TootAccount { private static final LogCategory log = new LogCategory( "MSPAccount" ); @Nullable - static TootAccount parseAccount( @NonNull Context context, @NonNull SavedAccount access_info, @Nullable JSONObject src ){ + static TootAccount parseAccount( @NonNull TootParser parser , @Nullable JSONObject src ){ if( src == null ) return null; @@ -29,7 +30,7 @@ public class MSPAccount extends TootAccount { dst.avatar = dst.avatar_static = Utils.optStringX( src, "avatar" ); String sv = Utils.optStringX( src, "display_name" ); - dst.setDisplayName( context, dst.username, sv ); + dst.setDisplayName( parser.context, dst.username, sv ); dst.id = Utils.optLongX( src, "id" ); @@ -38,7 +39,7 @@ public class MSPAccount extends TootAccount { .setShort( true ) .setDecodeEmoji( true ) .setProfileEmojis( dst.profile_emojis ) - .decodeHTML( context, access_info, dst.note ); + .decodeHTML( parser.context, parser.access_info, dst.note ); if( TextUtils.isEmpty( dst.url ) ){ log.e( "parseAccount: missing url" ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/api_msp/entity/MSPToot.java b/app/src/main/java/jp/juggler/subwaytooter/api_msp/entity/MSPToot.java index 64cfa6f8..2f384599 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api_msp/entity/MSPToot.java +++ b/app/src/main/java/jp/juggler/subwaytooter/api_msp/entity/MSPToot.java @@ -16,6 +16,7 @@ import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; +import jp.juggler.subwaytooter.api.TootParser; import jp.juggler.subwaytooter.api.entity.TootStatusLike; import jp.juggler.subwaytooter.table.SavedAccount; import jp.juggler.subwaytooter.util.DecodeOptions; @@ -37,11 +38,11 @@ public class MSPToot extends TootStatusLike { // private long msp_id; @Nullable - private static MSPToot parse( @NonNull Context context, SavedAccount access_info, JSONObject src ){ + private static MSPToot parse( @NonNull TootParser parser, JSONObject src ){ if( src == null ) return null; MSPToot dst = new MSPToot(); - dst.account = MSPAccount.parseAccount( context, access_info, src.optJSONObject( "account" ) ); + dst.account = MSPAccount.parseAccount( parser, src.optJSONObject( "account" ) ); if( dst.account == null ){ log.e( "missing status account" ); return null; @@ -76,26 +77,18 @@ public class MSPToot extends TootStatusLike { // dst.msp_id = Utils.optLongX(src, "msp_id" ); dst.sensitive = ( src.optInt( "sensitive", 0 ) != 0 ); - dst.setSpoilerText( context, Utils.optStringX( src, "spoiler_text" ) ); - - dst.content = Utils.optStringX( src, "content" ); - dst.decoded_content = new DecodeOptions() - .setShort( true ) - .setDecodeEmoji( true ) - .setCustomEmojiMap( dst.custom_emojis ) - .setProfileEmojis( dst.profile_emojis ) - .setLinkTag( dst ) - .decodeHTML( context, access_info, dst.content ); + dst.setSpoilerText( parser, Utils.optStringX( src, "spoiler_text" ) ); + dst.setContent( parser, null, Utils.optStringX( src, "content" ) ); return dst; } - public static List parseList( @NonNull Context context, SavedAccount access_info, JSONArray array ){ + public static List parseList( @NonNull TootParser parser, JSONArray array ){ List list = new List(); for( int i = 0, ie = array.length() ; i < ie ; ++ i ){ JSONObject src = array.optJSONObject( i ); if( src == null ) continue; - MSPToot item = parse( context, access_info, src ); + MSPToot item = parse( parser, src ); if( item == null ) continue; list.add( item ); } @@ -146,11 +139,11 @@ public class MSPToot extends TootStatusLike { // } // // word mute - if( decoded_content != null && muted_word.containsWord( decoded_content.toString() ) ){ + if( muted_word.matchShort( decoded_content ) ){ return true; } - if( decoded_spoiler_text != null && muted_word.containsWord( decoded_spoiler_text.toString() ) ){ + if( muted_word.matchShort( decoded_spoiler_text ) ){ return true; } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api_tootsearch/entity/TSToot.java b/app/src/main/java/jp/juggler/subwaytooter/api_tootsearch/entity/TSToot.java index 62a9083d..e60dd480 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api_tootsearch/entity/TSToot.java +++ b/app/src/main/java/jp/juggler/subwaytooter/api_tootsearch/entity/TSToot.java @@ -12,6 +12,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.regex.Matcher; +import jp.juggler.subwaytooter.api.TootParser; import jp.juggler.subwaytooter.api.entity.CustomEmoji; import jp.juggler.subwaytooter.api.entity.NicoProfileEmoji; import jp.juggler.subwaytooter.api.entity.TootAccount; @@ -37,11 +38,11 @@ public class TSToot extends TootStatusLike { public String uri; @Nullable - private static TSToot parse( @NonNull Context context, SavedAccount access_info, JSONObject src ){ + private static TSToot parse( @NonNull TootParser parser, JSONObject src ){ if( src == null ) return null; TSToot dst = new TSToot(); - dst.account = parseAccount( context, access_info, src.optJSONObject( "account" ) ); + dst.account = parseAccount( parser, src.optJSONObject( "account" ) ); if( dst.account == null ){ log.e( "missing status account" ); return null; @@ -50,7 +51,7 @@ public class TSToot extends TootStatusLike { dst.json = src; // 絵文字マップは割と最初の方で読み込んでおきたい - dst.custom_emojis = CustomEmoji.parseMap( src.optJSONArray( "emojis" ), access_info.host ); + dst.custom_emojis = CustomEmoji.parseMap( src.optJSONArray( "emojis" ), parser.access_info.host ); dst.profile_emojis = NicoProfileEmoji.parseMap( src.optJSONArray( "profile_emojis" ) ); dst.url = Utils.optStringX( src, "url" ); @@ -65,7 +66,7 @@ public class TSToot extends TootStatusLike { log.e( "missing status uri or url or host or id" ); return null; } - + // uri から投稿元タンスでのIDを調べる dst.id = TootStatusLike.parseStatusId( dst ); @@ -76,18 +77,8 @@ public class TSToot extends TootStatusLike { dst.sensitive = src.optBoolean( "sensitive", false ); - dst.setSpoilerText( context, Utils.optStringX( src, "spoiler_text" ) ); - - dst.content = Utils.optStringX( src, "content" ); - dst.decoded_content = new DecodeOptions() - .setShort( true ) - .setDecodeEmoji( true ) - .setCustomEmojiMap( dst.custom_emojis ) - .setProfileEmojis( dst.profile_emojis ) - .setLinkTag( dst ) - .decodeHTML( context, access_info, dst.content ); - - + dst.setSpoilerText( parser, Utils.optStringX( src, "spoiler_text" ) ); + dst.setContent( parser, dst.media_attachments, Utils.optStringX( src, "content" ) ); return dst; } @@ -96,7 +87,7 @@ public class TSToot extends TootStatusLike { } @NonNull - public static TSToot.List parseList( @NonNull Context context, SavedAccount access_info, @NonNull JSONObject root ){ + public static TSToot.List parseList( @NonNull TootParser parser, @NonNull JSONObject root ){ TSToot.List list = new TSToot.List(); JSONArray array = TSClient.getHits( root ); if( array != null ){ @@ -105,11 +96,11 @@ public class TSToot extends TootStatusLike { JSONObject src = array.optJSONObject( i ); if( src == null ) continue; JSONObject src2 = src.optJSONObject( "_source" ); - TSToot item = parse( context, access_info, src2 ); + TSToot item = parse( parser, src2 ); if( item == null ) continue; list.add( item ); - }catch(Throwable ex){ - log.trace(ex); + }catch( Throwable ex ){ + log.trace( ex ); } } } @@ -119,11 +110,11 @@ public class TSToot extends TootStatusLike { public boolean checkMuted( @SuppressWarnings("UnusedParameters") @NonNull HashSet< String > muted_app, @NonNull WordTrieTree muted_word ){ // word mute - if( decoded_content != null && muted_word.containsWord( decoded_content.toString() ) ){ + if( decoded_content != null && muted_word.matchShort( decoded_content.toString() ) ){ return true; } - if( decoded_spoiler_text != null && muted_word.containsWord( decoded_spoiler_text.toString() ) ){ + if( decoded_spoiler_text != null && muted_word.matchShort( decoded_spoiler_text.toString() ) ){ return true; } @@ -143,9 +134,10 @@ public class TSToot extends TootStatusLike { } @Nullable - private static TootAccount parseAccount( @NonNull Context context, @NonNull SavedAccount access_info, @Nullable JSONObject src ){ + private static TootAccount parseAccount( @NonNull TootParser parser, @Nullable JSONObject src ){ + + TootAccount dst = parser.account( src ); - TootAccount dst = TootAccount.parse( context, access_info, src ); if( dst != null ){ // tootsearch のアカウントのIDはどのタンス上のものか分からない diff --git a/app/src/main/java/jp/juggler/subwaytooter/dialog/DlgListMember.java b/app/src/main/java/jp/juggler/subwaytooter/dialog/DlgListMember.java index b3ae9f70..fd1aa896 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/dialog/DlgListMember.java +++ b/app/src/main/java/jp/juggler/subwaytooter/dialog/DlgListMember.java @@ -35,6 +35,7 @@ import jp.juggler.subwaytooter.api.TootTaskRunner; import jp.juggler.subwaytooter.api.entity.TootAccount; import jp.juggler.subwaytooter.api.entity.TootList; import jp.juggler.subwaytooter.api.entity.TootResults; +import jp.juggler.subwaytooter.api.TootParser; import jp.juggler.subwaytooter.table.AcctColor; import jp.juggler.subwaytooter.table.SavedAccount; import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator; @@ -190,7 +191,7 @@ public class DlgListMember implements View.OnClickListener { return result; } - TootResults search_result = TootResults.parse( activity, list_owner, result.object ); + TootResults search_result = new TootParser(activity, list_owner).results( result.object ); if( search_result != null ){ for( TootAccount a : search_result.accounts ){ if( target_user_full_acct.equalsIgnoreCase( list_owner.getFullAcct( a ) ) ){ diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/HighlightWord.java b/app/src/main/java/jp/juggler/subwaytooter/table/HighlightWord.java new file mode 100644 index 00000000..153061c1 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/table/HighlightWord.java @@ -0,0 +1,191 @@ +package jp.juggler.subwaytooter.table; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +import jp.juggler.subwaytooter.App1; +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; +import jp.juggler.subwaytooter.util.WordTrieTree; + +public class HighlightWord { + + private static final LogCategory log = new LogCategory( "HighlightWord" ); + + public static final int SOUND_TYPE_NONE = 0; + public static final int SOUND_TYPE_DEFAULT = 1; + public static final int SOUND_TYPE_CUSTOM = 2; + + public static final String table = "highlight_word"; + public static final String COL_ID = "_id"; + public static final String COL_NAME = "name"; + private static final String COL_TIME_SAVE = "time_save"; + private static final String COL_COLOR_BG = "color_bg"; + private static final String COL_COLOR_FG = "color_fg"; + private static final String COL_SOUND_TYPE = "sound_type"; + private static final String COL_SOUND_URI = "sound_uri"; + + public static void onDBCreate( SQLiteDatabase db ){ + log.d( "onDBCreate!" ); + db.execSQL( + "create table if not exists " + table + + "(_id INTEGER PRIMARY KEY" + + ",name text not null" + + ",time_save integer not null" + + ",color_bg integer not null default 0" + + ",color_fg integer not null default 0" + + ",sound_type integer not null default 1" + + ",sound_uri text default null" + + ")" + ); + db.execSQL( + "create unique index if not exists " + table + "_name on " + table + "(name)" + ); + } + + public static void onDBUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){ + if( oldVersion < 21 && newVersion >= 21 ){ + onDBCreate( db ); + } + } + + public long id = -1L; + @NonNull public String name; + public int color_bg; + public int color_fg; + public int sound_type; + @Nullable public String sound_uri; + + public JSONObject encodeJson() throws JSONException{ + JSONObject dst = new JSONObject( ); + dst.put(COL_ID,id); + dst.put(COL_NAME,name); + dst.put(COL_COLOR_BG,color_bg); + dst.put(COL_COLOR_FG,color_fg); + dst.put(COL_SOUND_TYPE,sound_type); + if( sound_uri != null ) dst.put(COL_SOUND_URI,sound_uri); + return dst; + } + + public HighlightWord(@NonNull JSONObject src ){ + this.id = Utils.optLongX( src,COL_ID ); + String sv = Utils.optStringX(src,COL_NAME); + if( TextUtils.isEmpty( sv )) throw new RuntimeException( "HighlightWord: name is empty" ); + this.name = sv; + this.color_bg = src.optInt( COL_COLOR_BG ); + this.color_fg = src.optInt( COL_COLOR_FG ); + this.sound_type = src.optInt( COL_SOUND_TYPE ); + this.sound_uri = Utils.optStringX( src,COL_SOUND_URI ); + } + + + public HighlightWord(@NonNull String name){ + this.name = name; + this.sound_type = SOUND_TYPE_DEFAULT; + this.color_fg = 0xFFFF0000; + } + + public HighlightWord(@NonNull Cursor cursor){ + this.id = cursor.getLong( cursor.getColumnIndex( COL_ID )); + this.name = cursor.getString( cursor.getColumnIndex( COL_NAME )); + this.color_bg = cursor.getInt( cursor.getColumnIndex( COL_COLOR_BG )); + this.color_fg = cursor.getInt( cursor.getColumnIndex( COL_COLOR_FG )); + this.sound_type = cursor.getInt( cursor.getColumnIndex( COL_SOUND_TYPE )); + int colIdx_sound_uri = cursor.getColumnIndex( COL_SOUND_URI ); + this.sound_uri = cursor.isNull( colIdx_sound_uri ) ? null : cursor.getString( colIdx_sound_uri); + } + + private static final String selection_name = COL_NAME+"=?"; + private static final String selection_id = COL_ID+"=?"; + + + @Nullable public static HighlightWord load(@NonNull String name){ + try{ + Cursor cursor = App1.getDB().query( table, null, selection_name, new String[]{ name }, null, null, null ); + if( cursor != null ){ + try{ + if( cursor.moveToNext() ){ + return new HighlightWord( cursor ); + } + }finally{ + cursor.close(); + } + } + }catch( Throwable ex ){ + log.trace( ex ); + } + return null; + } + + public void save(){ + + if( TextUtils.isEmpty( name )) throw new RuntimeException( "HighlightWord: name is empty" ); + + try{ + ContentValues cv = new ContentValues(); + cv.put( COL_NAME, name ); + cv.put( COL_TIME_SAVE, System.currentTimeMillis() ); + cv.put( COL_COLOR_BG, color_bg ); + cv.put( COL_COLOR_FG, color_fg ); + cv.put( COL_SOUND_TYPE, sound_type ); + if( TextUtils.isEmpty(sound_uri) ){ + cv.putNull( COL_SOUND_URI ); + + }else{ + cv.put( COL_SOUND_URI, sound_uri ); + } + if( id == -1L ){ + App1.getDB().replace( table, null, cv ); + }else{ + App1.getDB().update( table, cv, selection_id, new String[]{ Long.toString( id ) } ); + } + }catch( Throwable ex ){ + log.e( ex, "save failed." ); + } + } + + public static Cursor createCursor(){ + return App1.getDB().query( table, null, null, null, null, null, COL_NAME + " asc" ); + } + + public void delete(){ + try{ + App1.getDB().delete( table, selection_id, new String[]{ Long.toString( id ) } ); + }catch( Throwable ex ){ + log.e( ex, "delete failed." ); + } + } + + private static final String[] columns_name = new String[]{ COL_NAME }; + + + @Nullable public static WordTrieTree getNameSet(){ + WordTrieTree dst = null; + try{ + Cursor cursor = App1.getDB().query( table, columns_name, null, null, null, null, null ); + if( cursor != null ){ + try{ + int idx_name = cursor.getColumnIndex( COL_NAME ); + while( cursor.moveToNext() ){ + if( dst == null) dst = new WordTrieTree(); + String s = cursor.getString( idx_name ); + dst.add( s ); + } + }finally{ + cursor.close(); + } + } + }catch( Throwable ex ){ + log.trace( ex ); + } + return dst; + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/CharacterGroup.java b/app/src/main/java/jp/juggler/subwaytooter/util/CharacterGroup.java new file mode 100644 index 00000000..e6b85a98 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/util/CharacterGroup.java @@ -0,0 +1,339 @@ +package jp.juggler.subwaytooter.util; + +import android.support.annotation.NonNull; +import android.support.v4.util.SparseArrayCompat; + +import java.util.Locale; + +public class CharacterGroup { + + // Tokenizerが終端に達したことを示す + static final int END = - 1; + + // 同じと見なす文字列の集合 + static class Group { + + // 文字列の配列 + @NonNull final String[] list; + + // 代表する文字のコード + final int id; + + Group( @NonNull String[] list ){ + this.list = list; + this.id = findGroupId(); + } + + private int findGroupId(){ + // グループのIDは、グループ中の文字(長さ1)のunicodeのどれか + for( String s : list ){ + if( s.length() == 1 ){ + return s.charAt( 0 ); + } + } + throw new RuntimeException( "group has not id!!" ); + } + } + + + // テキスト中に出現した文字列からグループを探すためのマップ + + // キー文字数1 + private final SparseArrayCompat< Group > map1 = new SparseArrayCompat<>(); + + // キー文字数2 + private final SparseArrayCompat< Group > map2 = new SparseArrayCompat<>(); + + // グループをmapに登録する + private void addGroup( @NonNull String[] list ){ + + // グループを生成 + Group g = new Group( list ); + + // 含まれる各文字列をマップに登録する + for( String s : list ){ + int len = s.length(); + int v1 = s.charAt( 0 ); + + SparseArrayCompat< Group > map; + int key; + if( len == 1 ){ + map = map1; + key = v1; + }else{ + map = map2; + int v2 = s.charAt( 1 ); + key = v1 | ( v2 << 16 ); + } + + Group old = map.get( key ); + if( old != null && old != g ){ + throw new RuntimeException( String.format( Locale.JAPAN, "group conflict: %s", s ) ); + } + map.put( key, g ); + } + } + + // CharSequence の範囲 から 文字,グループ,終端 のどれかを列挙する + class Tokenizer { + public CharSequence text; + public int end; + + // next() を読むと以下の変数が更新される + public int offset; + public int c; // may END or group.id or UTF-16 character + public Group group; + + Tokenizer( @NonNull CharSequence text, int start, int end ){ + reset( text, start, end ); + } + + public void reset( CharSequence text, int start, int end ){ + this.text = text; + this.offset = start; + this.end = end; + } + + public void next(){ + + int pos = offset; + + // 空白を読み飛ばす + while( pos < end && isWhitespace( text.charAt( pos ) ) ) ++ pos; + + // 終端までの文字数 + int remain = end - pos; + if( remain <= 0 ){ + // 空白を読み飛ばしたら終端になった + // 終端の場合、末尾の空白はoffsetに含めない + this.group = null; + this.c = END; + return; + } + + int v1 = text.charAt( pos ); + int v2 = remain > 1 ? text.charAt( pos + 1 ) : 0; + + // グループに登録された文字を長い順にチェック + int check_len = remain > 2 ? 2 : remain; + while( check_len > 0 ){ + Group g = check_len == 1 ? map1.get( v1 ) : map2.get( v1 | ( v2 << 16 ) ); + if( g != null ){ + this.group = g; + this.c = g.id; + this.offset = pos + check_len; + return; + } + -- check_len; + } + + this.group = null; + this.c = v1; + this.offset = pos + 1; + } + } + + Tokenizer tokenizer( CharSequence text, int start, int end ){ + return new Tokenizer( text, start, end ); + } + + public static boolean isWhitespace( int cp ){ + switch( cp ){ + case 0x0009: // HORIZONTAL TABULATION + case 0x000A: // LINE FEED + case 0x000B: // VERTICAL TABULATION + case 0x000C: // FORM FEED + case 0x000D: // CARRIAGE RETURN + case 0x001C: // FILE SEPARATOR + case 0x001D: // GROUP SEPARATOR + case 0x001E: // RECORD SEPARATOR + case 0x001F: // UNIT SEPARATOR + case 0x0020: + case 0x0085: // next line (latin-1) + case 0x00A0: //非区切りスペース + case 0x1680: + case 0x180E: + case 0x2000: + case 0x2001: + case 0x2002: + case 0x2003: + case 0x2004: + case 0x2005: + case 0x2006: + case 0x2007: //非区切りスペース + case 0x2008: + case 0x2009: + case 0x200A: + case 0x200B: + case 0x200C: + case 0x200D: + case 0x2028: // line separator + case 0x2029: // paragraph separator + + case 0x202F: //非区切りスペース + case 0x205F: + case 0x2060: + case 0x3000: + case 0x3164: + case 0xFEFF: + return true; + default: + return Character.isWhitespace( cp ); + } + } + + // 文字コードから文字列を作る + private static String c2s( char[] tmp, int c ){ + tmp[ 0 ] = (char) c; + return new String( tmp, 0, 1 ); + } + + CharacterGroup(){ + char[] tmp = new char[ 1 ]; + + // 数字 + for( int i = 0 ; i < 9 ; ++ i ){ + String[] list = new String[ 2 ]; + list[ 0 ] = c2s( tmp, '0' + i ); + list[ 1 ] = c2s( tmp, '0' + i ); + addGroup( list ); + } + + // 英字 + for( int i = 0 ; i < 26 ; ++ i ){ + String[] list = new String[ 4 ]; + list[ 0 ] = c2s( tmp, 'a' + i ); + list[ 1 ] = c2s( tmp, 'A' + i ); + list[ 2 ] = c2s( tmp, 'a' + i ); + list[ 3 ] = c2s( tmp, 'A' + i ); + addGroup( list ); + } + + // ハイフン + addGroup( new String[]{ + c2s( tmp, 0x002D ), // ASCIIのハイフン + c2s( tmp, 0x30FC ), // 全角カナの長音 Shift_JIS由来 + c2s( tmp, 0x2010 ), + c2s( tmp, 0x2011 ), + c2s( tmp, 0x2013 ), + c2s( tmp, 0x2014 ), + c2s( tmp, 0x2015 ), // 全角カナのダッシュ Shift_JIS由来 + c2s( tmp, 0x2212 ), + c2s( tmp, 0xFF0d ), // 全角カナの長音 MS932由来 + c2s( tmp, 0xFF70 ), // 半角カナの長音 MS932由来 + } ); + + addGroup( new String[]{ "!", "!" } ); + addGroup( new String[]{ """, "\"" } ); + addGroup( new String[]{ "#", "#" } ); + addGroup( new String[]{ "$", "$" } ); + addGroup( new String[]{ "%", "%" } ); + addGroup( new String[]{ "&", "&" } ); + addGroup( new String[]{ "'", "'" } ); + addGroup( new String[]{ "(", "(" } ); + addGroup( new String[]{ ")", ")" } ); + addGroup( new String[]{ "*", "*" } ); + addGroup( new String[]{ "+", "+" } ); + addGroup( new String[]{ ",", ",", "、", "、" } ); + addGroup( new String[]{ ".", ".", "。", "。" } ); + addGroup( new String[]{ "/", "/" } ); + addGroup( new String[]{ ":", ":" } ); + addGroup( new String[]{ ";", ";" } ); + addGroup( new String[]{ "<", "<" } ); + addGroup( new String[]{ "=", "=" } ); + addGroup( new String[]{ ">", ">" } ); + addGroup( new String[]{ "?", "?" } ); + addGroup( new String[]{ "@", "@" } ); + addGroup( new String[]{ "[", "[" } ); + addGroup( new String[]{ "\", "\\", "¥" } ); + addGroup( new String[]{ "]", "]" } ); + addGroup( new String[]{ "^", "^" } ); + addGroup( new String[]{ "_", "_" } ); + addGroup( new String[]{ "`", "`" } ); + addGroup( new String[]{ "{", "{" } ); + addGroup( new String[]{ "|", "|", "¦" } ); + addGroup( new String[]{ "}", "}" } ); + + addGroup( new String[]{ "・", "・", "・" } ); + addGroup( new String[]{ "「", "「", "「" } ); + addGroup( new String[]{ "」", "」", "」" } ); + + // チルダ + addGroup( new String[]{ "~", c2s(tmp,0x301C),c2s(tmp,0xFF5E) } ); + + // 半角カナの濁音,半濁音は2文字になる + addGroup( new String[]{ "ガ", "が", "ガ" } ); + addGroup( new String[]{ "ギ", "ぎ", "ギ" } ); + addGroup( new String[]{ "グ", "ぐ", "グ" } ); + addGroup( new String[]{ "ゲ", "げ", "ゲ" } ); + addGroup( new String[]{ "ゴ", "ご", "ゴ" } ); + addGroup( new String[]{ "ザ", "ざ", "ザ" } ); + addGroup( new String[]{ "ジ", "じ", "ジ" } ); + addGroup( new String[]{ "ズ", "ず", "ズ" } ); + addGroup( new String[]{ "ゼ", "ぜ", "ゼ" } ); + addGroup( new String[]{ "ゾ", "ぞ", "ゾ" } ); + addGroup( new String[]{ "ダ", "だ", "ダ" } ); + addGroup( new String[]{ "ヂ", "ぢ", "ヂ" } ); + addGroup( new String[]{ "ヅ", "づ", "ヅ" } ); + addGroup( new String[]{ "デ", "で", "デ" } ); + addGroup( new String[]{ "ド", "ど", "ド" } ); + addGroup( new String[]{ "バ", "ば", "バ" } ); + addGroup( new String[]{ "ビ", "び", "ビ" } ); + addGroup( new String[]{ "ブ", "ぶ", "ブ" } ); + addGroup( new String[]{ "ベ", "べ", "ベ" } ); + addGroup( new String[]{ "ボ", "ぼ", "ボ" } ); + addGroup( new String[]{ "パ", "ぱ", "パ" } ); + addGroup( new String[]{ "ピ", "ぴ", "ピ" } ); + addGroup( new String[]{ "プ", "ぷ", "プ" } ); + addGroup( new String[]{ "ペ", "ぺ", "ペ" } ); + addGroup( new String[]{ "ポ", "ぽ", "ポ" } ); + addGroup( new String[]{ "ヴ", "う゛", "ヴ" } ); + + addGroup( new String[]{ "あ", "ア", "ア", "ぁ", "ァ", "ァ" } ); + addGroup( new String[]{ "い", "イ", "イ", "ぃ", "ィ", "ィ" } ); + addGroup( new String[]{ "う", "ウ", "ウ", "ぅ", "ゥ", "ゥ" } ); + addGroup( new String[]{ "え", "エ", "エ", "ぇ", "ェ", "ェ" } ); + addGroup( new String[]{ "お", "オ", "オ", "ぉ", "ォ", "ォ" } ); + addGroup( new String[]{ "か", "カ", "カ" } ); + addGroup( new String[]{ "き", "キ", "キ" } ); + addGroup( new String[]{ "く", "ク", "ク" } ); + addGroup( new String[]{ "け", "ケ", "ケ" } ); + addGroup( new String[]{ "こ", "コ", "コ" } ); + addGroup( new String[]{ "さ", "サ", "サ" } ); + addGroup( new String[]{ "し", "シ", "シ" } ); + addGroup( new String[]{ "す", "ス", "ス" } ); + addGroup( new String[]{ "せ", "セ", "セ" } ); + addGroup( new String[]{ "そ", "ソ", "ソ" } ); + addGroup( new String[]{ "た", "タ", "タ" } ); + addGroup( new String[]{ "ち", "チ", "チ" } ); + addGroup( new String[]{ "つ", "ツ", "ツ", "っ", "ッ", "ッ" } ); + addGroup( new String[]{ "て", "テ", "テ" } ); + addGroup( new String[]{ "と", "ト", "ト" } ); + addGroup( new String[]{ "な", "ナ", "ナ" } ); + addGroup( new String[]{ "に", "ニ", "ニ" } ); + addGroup( new String[]{ "ぬ", "ヌ", "ヌ" } ); + addGroup( new String[]{ "ね", "ネ", "ネ" } ); + addGroup( new String[]{ "の", "ノ", "ノ" } ); + addGroup( new String[]{ "は", "ハ", "ハ" } ); + addGroup( new String[]{ "ひ", "ヒ", "ヒ" } ); + addGroup( new String[]{ "ふ", "フ", "フ" } ); + addGroup( new String[]{ "へ", "ヘ", "ヘ" } ); + addGroup( new String[]{ "ほ", "ホ", "ホ" } ); + addGroup( new String[]{ "ま", "マ", "マ" } ); + addGroup( new String[]{ "み", "ミ", "ミ" } ); + addGroup( new String[]{ "む", "ム", "ム" } ); + addGroup( new String[]{ "め", "メ", "メ" } ); + addGroup( new String[]{ "も", "モ", "モ" } ); + addGroup( new String[]{ "や", "ヤ", "ヤ", "ゃ", "ャ", "ャ" } ); + addGroup( new String[]{ "ゆ", "ユ", "ユ", "ゅ", "ュ", "ュ" } ); + addGroup( new String[]{ "よ", "ヨ", "ヨ", "ょ", "ョ", "ョ" } ); + addGroup( new String[]{ "ら", "ラ", "ラ" } ); + addGroup( new String[]{ "り", "リ", "リ" } ); + addGroup( new String[]{ "る", "ル", "ル" } ); + addGroup( new String[]{ "れ", "レ", "レ" } ); + addGroup( new String[]{ "ろ", "ロ", "ロ" } ); + addGroup( new String[]{ "わ", "ワ", "ワ" } ); + addGroup( new String[]{ "を", "ヲ", "ヲ" } ); + addGroup( new String[]{ "ん", "ン", "ン" } ); + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/DecodeOptions.java b/app/src/main/java/jp/juggler/subwaytooter/util/DecodeOptions.java index 39484be2..477a4b70 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/DecodeOptions.java +++ b/app/src/main/java/jp/juggler/subwaytooter/util/DecodeOptions.java @@ -6,9 +6,13 @@ import android.support.annotation.Nullable; import android.text.Spannable; import android.text.SpannableStringBuilder; +import java.util.ArrayList; + +import jp.juggler.subwaytooter.api.TootParser; import jp.juggler.subwaytooter.api.entity.CustomEmoji; import jp.juggler.subwaytooter.api.entity.NicoProfileEmoji; import jp.juggler.subwaytooter.api.entity.TootAttachment; +import jp.juggler.subwaytooter.table.HighlightWord; @SuppressWarnings("WeakerAccess") public class DecodeOptions { @@ -62,4 +66,13 @@ public class DecodeOptions { public Spannable decodeEmoji( @NonNull final Context context, @NonNull final String s ){ return EmojiDecoder.decodeEmoji( context, s, this ); } + + // highlight first found + @Nullable public HighlightWord highlight_sound; + + @Nullable public WordTrieTree highlight_trie; + public DecodeOptions setHighlightTrie( WordTrieTree highlight_trie ){ + this.highlight_trie = highlight_trie; + return this; + } } \ No newline at end of file diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/EmojiDecoder.java b/app/src/main/java/jp/juggler/subwaytooter/util/EmojiDecoder.java index 3399827c..e6647e2a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/EmojiDecoder.java +++ b/app/src/main/java/jp/juggler/subwaytooter/util/EmojiDecoder.java @@ -15,6 +15,7 @@ import jp.juggler.subwaytooter.App1; import jp.juggler.subwaytooter.R; import jp.juggler.subwaytooter.api.entity.CustomEmoji; import jp.juggler.subwaytooter.api.entity.NicoProfileEmoji; +import jp.juggler.subwaytooter.table.HighlightWord; @SuppressWarnings("WeakerAccess") public class EmojiDecoder { @@ -22,9 +23,38 @@ public class EmojiDecoder { private static class DecodeEnv { @NonNull final Context context; @NonNull final SpannableStringBuilder sb = new SpannableStringBuilder(); + @NonNull final DecodeOptions options; + int normal_char_start = -1; - DecodeEnv( @NonNull Context context ){ + DecodeEnv( @NonNull Context context ,@NonNull final DecodeOptions options){ this.context = context; + this.options = options; + } + + + void closeNormalText(){ + if( normal_char_start != -1 ){ + int end = sb.length(); + applyHighlight(normal_char_start,end); + normal_char_start = -1; + } + } + + private void applyHighlight( int start , int end ){ + if( options.highlight_trie != null ){ + ArrayList list = options.highlight_trie.matchList( sb,start,end ); + if( list != null ){ + for( WordTrieTree.Match range : list ){ + HighlightWord word = HighlightWord.load( range.word ); + if( word !=null ){ + sb.setSpan( new HighlightSpan( word.color_fg,word.color_bg ), range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ); + if( word.sound_type != HighlightWord.SOUND_TYPE_NONE ){ + options.highlight_sound = word; + } + } + } + } + } } void addUnicodeString( String s ){ @@ -34,6 +64,7 @@ public class EmojiDecoder { int remain = end - i; String emoji = null; Integer image_id = null; + for( int j = EmojiMap201709.utf16_max_length ; j > 0 ; -- j ){ if( j > remain ) continue; String check = s.substring( i, i + j ); @@ -55,6 +86,9 @@ public class EmojiDecoder { if( image_id == 0 ){ // 絵文字バリエーション・シーケンス(EVS)のU+FE0E(VS-15)が直後にある場合 // その文字を絵文字化しない + if( normal_char_start == - 1 ){ + normal_char_start = sb.length(); + } sb.append( emoji ); }else{ addImageSpan( emoji, image_id ); @@ -63,6 +97,9 @@ public class EmojiDecoder { continue; } + if( normal_char_start == - 1 ){ + normal_char_start = sb.length(); + } int length = Character.charCount( s.codePointAt( i ) ); if( length == 1 ){ sb.append( s.charAt( i ) ); @@ -75,6 +112,7 @@ public class EmojiDecoder { } void addImageSpan( String text, @DrawableRes int res_id ){ + closeNormalText(); int start = sb.length(); sb.append( text ); int end = sb.length(); @@ -82,6 +120,7 @@ public class EmojiDecoder { } void addNetworkEmojiSpan( String text, @NonNull String url ){ + closeNormalText(); int start = sb.length(); sb.append( text ); int end = sb.length(); @@ -89,44 +128,7 @@ public class EmojiDecoder { } } - public static boolean isWhitespaceBeforeEmoji( int cp ){ - switch( cp ){ - case 0x0009: // HORIZONTAL TABULATION - case 0x000A: // LINE FEED - case 0x000B: // VERTICAL TABULATION - case 0x000C: // FORM FEED - case 0x000D: // CARRIAGE RETURN - case 0x001C: // FILE SEPARATOR - case 0x001D: // GROUP SEPARATOR - case 0x001E: // RECORD SEPARATOR - case 0x001F: // UNIT SEPARATOR - case 0x0020: - case 0x00A0: //非区切りスペース - case 0x1680: - case 0x180E: - case 0x2000: - case 0x2001: - case 0x2002: - case 0x2003: - case 0x2004: - case 0x2005: - case 0x2006: - case 0x2007: //非区切りスペース - case 0x2008: - case 0x2009: - case 0x200A: - case 0x200B: - case 0x202F: //非区切りスペース - case 0x205F: - case 0x2060: - case 0x3000: - case 0x3164: - case 0xFEFF: - return true; - default: - return Character.isWhitespace( cp ); - } - } + public static boolean isShortCodeCharacter( int cp ){ return ( 'A' <= cp && cp <= 'Z' ) @@ -165,7 +167,7 @@ public class EmojiDecoder { }else if( i + width < end && s.codePointAt( i + width ) == '@' ){ // フレニコのプロフ絵文字 :@who: は手前の空白を要求しない break; - }else if( i == 0 || isWhitespaceBeforeEmoji( s.codePointBefore( i ) ) ){ + }else if( i == 0 || CharacterGroup.isWhitespace( s.codePointBefore( i ) ) ){ // ショートコードの手前は始端か改行か空白文字でないとならない // 空白文字の判定はサーバサイドのそれにあわせる break; @@ -218,7 +220,7 @@ public class EmojiDecoder { , @NonNull final String s , @NonNull DecodeOptions options ){ - final DecodeEnv decode_env = new DecodeEnv( context ); + final DecodeEnv decode_env = new DecodeEnv( context ,options); final CustomEmoji.Map custom_map = options.customEmojiMap; final NicoProfileEmoji.Map profile_emojis = options.profile_emojis; @@ -265,6 +267,8 @@ public class EmojiDecoder { } } ); + decode_env.closeNormalText(); + return decode_env.sb; } 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 83342d53..140c3e65 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/HTMLDecoder.java +++ b/app/src/main/java/jp/juggler/subwaytooter/util/HTMLDecoder.java @@ -21,6 +21,7 @@ import jp.juggler.subwaytooter.R; import jp.juggler.subwaytooter.api.entity.TootAccount; import jp.juggler.subwaytooter.api.entity.TootAttachment; import jp.juggler.subwaytooter.api.entity.TootMention; +import jp.juggler.subwaytooter.table.HighlightWord; import jp.juggler.subwaytooter.table.SavedAccount; @SuppressWarnings("WeakerAccess") @@ -202,6 +203,9 @@ public class HTMLDecoder { sb.append( sb_tmp.toString() ); } end = sb.length(); + + + }else if( sb_tmp != sb ){ // style もscript も読み捨てる } @@ -213,6 +217,22 @@ public class HTMLDecoder { String link_text = sb.subSequence( start, end ).toString(); MyClickableSpan span = new MyClickableSpan( account, link_text, href, account.findAcctColor( href ), options.link_tag ); sb.setSpan( span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ); + + // リンクスパンを設定した後に色をつける + if( options.highlight_trie != null ){ + ArrayList list = options.highlight_trie.matchList( sb,start,end ); + if( list != null ){ + for( WordTrieTree.Match range : list ){ + HighlightWord word = HighlightWord.load( range.word ); + if( word !=null ){ + sb.setSpan( new HighlightSpan( word.color_fg,word.color_bg ), range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ); + if( word.sound_type != HighlightWord.SOUND_TYPE_NONE ){ + options.highlight_sound = word; + } + } + } + } + } } } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/HighlightSpan.java b/app/src/main/java/jp/juggler/subwaytooter/util/HighlightSpan.java new file mode 100644 index 00000000..52f70849 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/util/HighlightSpan.java @@ -0,0 +1,28 @@ +package jp.juggler.subwaytooter.util; + +import android.text.TextPaint; +import android.text.style.CharacterStyle; + +public class HighlightSpan extends CharacterStyle { + + public final int color_fg; + public final int color_bg; + + HighlightSpan( int color_fg ,int color_bg ){ + super(); + this.color_fg = color_fg; + this.color_bg = color_bg; + } + + @Override public void updateDrawState( TextPaint ds ){ + // super.updateDrawState( ds ); + + if( color_fg != 0 ){ + ds.setColor( color_fg ); + } + if( color_bg != 0 ){ + ds.bgColor = color_bg; + } + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.java b/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.java index c7fd0dd8..612163f4 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.java +++ b/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.java @@ -35,6 +35,7 @@ import jp.juggler.subwaytooter.api.entity.CustomEmoji; import jp.juggler.subwaytooter.api.entity.TootAccount; import jp.juggler.subwaytooter.api.entity.TootInstance; import jp.juggler.subwaytooter.api.entity.TootStatus; +import jp.juggler.subwaytooter.api.TootParser; import jp.juggler.subwaytooter.dialog.DlgConfirm; import jp.juggler.subwaytooter.dialog.EmojiPicker; import jp.juggler.subwaytooter.table.AcctColor; @@ -315,7 +316,7 @@ public class PostHelper implements CustomEmojiLister.Callback, EmojiPicker.Callb TootApiResult result = client.request( "/api/v1/statuses", request_builder ); if( result != null && result.object != null ){ - status = TootStatus.parse( activity, account, result.object ); + status = new TootParser( activity, account).status( result.object ); if( status != null ){ Spannable s = status.decoded_content; MyClickableSpan[] span_list = s.getSpans( 0, s.length(), MyClickableSpan.class ); @@ -599,7 +600,7 @@ public class PostHelper implements CustomEmojiLister.Callback, EmojiPicker.Callb } // : の手前は始端か改行か空白でなければならない - if( last_colon > 0 && ! EmojiDecoder.isWhitespaceBeforeEmoji( src.codePointBefore( last_colon ) ) ){ + if( last_colon > 0 && ! CharacterGroup.isWhitespace( src.codePointBefore( last_colon ) ) ){ log.d( "checkEmoji: invalid character before shortcode." ); closeAcctPopup(); return; diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/Utils.java b/app/src/main/java/jp/juggler/subwaytooter/util/Utils.java index a9d300c7..2fb8d076 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/Utils.java +++ b/app/src/main/java/jp/juggler/subwaytooter/util/Utils.java @@ -173,11 +173,11 @@ public class Utils { } } - public static String optStringX( JSONObject src, String key ){ + @Nullable public static String optStringX( JSONObject src, String key ){ return src.isNull( key ) ? null : src.optString( key ); } - public static String optStringX( JSONArray src, int key ){ + @Nullable public static String optStringX( JSONArray src, int key ){ return src.isNull( key ) ? null : src.optString( key ); } diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/WordTrieTree.java b/app/src/main/java/jp/juggler/subwaytooter/util/WordTrieTree.java index 2fbb4ac5..2ff08fea 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/WordTrieTree.java +++ b/app/src/main/java/jp/juggler/subwaytooter/util/WordTrieTree.java @@ -1,76 +1,143 @@ package jp.juggler.subwaytooter.util; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.util.SparseArrayCompat; +import java.util.ArrayList; + public class WordTrieTree { + static class Match { + String word; + int start; + int end; + } + + // private static class Matcher { + // + // // ミュートの場合などは短いマッチでも構わない + // final boolean allowShortMatch; + // + // // マッチ範囲の始端を覚えておく + // int start; + // + // Matcher( boolean allowShortMatch ){ + // this.allowShortMatch = allowShortMatch; + // } + // + // void setTokenizer( CharacterGroup grouper, CharSequence src, int start, int end ){ + // this.match = null; + // this.start = start; + // if( t == null ){ + // + // }else{ + // t.reset( src, start, end ); + // } + // } + // } + + private static final CharacterGroup grouper = new CharacterGroup(); + private static class Node { - final SparseArrayCompat< Node > child_nodes = new SparseArrayCompat<>(); + // 続くノード + @NonNull final SparseArrayCompat< Node > child_nodes = new SparseArrayCompat<>(); - boolean is_end; + // このノードが終端なら、マッチした単語の元の表記がある + @Nullable String match_word; - boolean match( String s, int offset, int remain ){ - - if( is_end ){ - // ワードの始端から終端までマッチした - return true; - } - - if( remain <= 0 ){ - // テスト文字列の終端に達した - return false; - } - - int c = s.charAt( offset ); - ++ offset; - -- remain; - - Node n = child_nodes.get( c ); - return n != null && n.match( s, offset, remain ); - } - - public void add( String s, int offset, int remain ){ - - if( is_end ){ - // NGワード用なので、既に終端を含むなら後続ノードの情報は不要 - return; - } - - if( remain <= 0 ){ - - // 終端マークを設定 - is_end = true; - - // 後続ノードは不要になる - child_nodes.clear(); - - return; - } - - int c = s.charAt( offset ); - ++ offset; - -- remain; - - // 文字別に後続ノードを作成 - Node n = child_nodes.get( c ); - if( n == null ) child_nodes.put( c, n = new Node() ); - - n.add( s, offset, remain ); - } + // Trieツリー的には終端単語と続くノードの両方が存在する場合がありうる。 + // たとえば ABC と ABCDEF を登録してからABCDEF を探索したら、単語 ABC と単語 DEF にマッチする。 } private final Node node_root = new Node(); - public void add( @NonNull String s ){ - node_root.add( s, 0, s.length() ); + // 単語の追加 + public void add( @NonNull String s ){ + CharacterGroup.Tokenizer t = grouper.tokenizer( s, 0, s.length() ); + Node node = node_root; + for( ; ; ){ + t.next(); + int id = t.c; + if( id == CharacterGroup.END ){ + // より長いマッチ単語を覚えておく + if( node.match_word == null || node.match_word.length() < t.text.length() ){ + node.match_word = t.text.toString(); + } + return; + } + Node child = node.child_nodes.get( t.c ); + if( child == null ){ + node.child_nodes.put( id, child = new Node() ); + } + node = child; + } } - public boolean containsWord( @NonNull String src ){ - for( int i = 0, ie = src.length() ; i < ie ; ++ i ){ - if( node_root.match( src, i, ie - i ) ) return true; + // 前方一致でマッチング + @Nullable + private Match match( boolean allowShortMatch, @NonNull CharacterGroup.Tokenizer t ){ + + int start = t.offset; + Match dst = null; + + Node node = node_root; + for( ; ; ){ + + // このノードは単語の終端でもある + if( node.match_word != null ){ + dst = new Match(); + dst.word = node.match_word; + dst.start = start; + dst.end = t.offset; + + // 最短マッチのみを調べるのなら、以降の処理は必要ない + if( allowShortMatch ) break; + } + + t.next(); + int id = t.c; + if( id == CharacterGroup.END ) break; + Node child = node.child_nodes.get( id ); + if( child == null ) break; + node = child; } - return false; + return dst; } + + public boolean matchShort( @Nullable CharSequence src ){ + return null != src && null != matchShort( src, 0, src.length() ); + } + + private Match matchShort( @NonNull CharSequence src, int start, int end ){ + CharacterGroup.Tokenizer t = grouper.tokenizer( src, start, end ); + for( int i = start ; i < end ; ++ i ){ + int c = src.charAt( i ); + if( CharacterGroup.isWhitespace( c ) ) continue; + t.reset( src, i, end ); + Match item = match( true, t ); + if( item != null ) return item; + } + return null; + } + + @Nullable ArrayList< Match > matchList( @NonNull CharSequence src, int start, int end ){ + ArrayList< Match > dst = null; + + CharacterGroup.Tokenizer t = grouper.tokenizer( src, start, end ); + for( int i = start ; i < end ; ++ i ){ + int c = src.charAt( i ); + if( CharacterGroup.isWhitespace( c ) ) continue; + t.reset( src, i, end ); + Match item = match( false, t ); + if( item != null ){ + if( dst == null ) dst = new ArrayList<>(); + dst.add( item ); + i = item.end - 1; + } + } + return dst; + } + } diff --git a/app/src/main/res/drawable-hdpi/ic_volume_up.png b/app/src/main/res/drawable-hdpi/ic_volume_up.png new file mode 100644 index 0000000000000000000000000000000000000000..83e570cfe760a30d31f2bd68c5f27dc96517a645 GIT binary patch literal 668 zcmV;N0%QG&P)@QOFASv}J>^b55TMZzR@VUN zYx^A7tu-wPs7@MO-()9JUDq8TC*2cdlwt&KwIu+s5B$GDYKGnOq%(n6A+W`8{oQt7 zTW11qil8h*d%Xc5#KSAb5?JLnTs2F-)o zbPjk$0N^mX0!`@_coMV<`tkvQs)05^Xm8s|H(-Yl-peUZn?~?F0Q2t@ zK>KVvM%SRc(zIa%^5`=GdLSUGk*5_RoMSTwnB8RFXC# z0JnWg;42Hd=lb7-x=wfnPXORFdH@;}_$v}K)I#KG;{kx{z%yt};CGMnfvJ^jCP1eZ zk}PBhF+hwv!u<>QyH7^|$DMsr#Cvka`B~BxfShUI+8XDlq$>bwn;3hVYjdeHz%4O$ zKiB3_XMk5?>>TKWY4a90fNY!q?gS~st}#z>dx?v2dWmNQTNJj`6B{;Lq%D|4gvu8M~Sp)6u2^~YD1O` zz-==O+7VRfE!P-#w0JBjEmoD67Ws4vP~2TdK;0_TmX9OlIiA3D{XUQs0N`H# q1T+`=JRj00rk;c`W5$gA3Ht^s)Nsr9jc5M=0000f};v7IFA?8dm*7;tSx~NXT35fA9tHVgO9^v)8!*pblL52Dq8`7JMfmPQh#${>&wUn(NMl6T}Bv>JTjd^ zn9u9FUVwkZuVaj~)1H7skS`3wWt{h%_MF7=Svu*82x3-CroxU9JM9YB+k5c k$J8?* z1MwOpN1O)YNSaxW>;NQr79dW8S`KrBD@`oN<^UwMUPz7r=@F;81h9u4d^*u;@ z61^O-0L{#0Q1Pwwa=;-pGbcgCkI~BkhX>jL3voojHhMWA97&uTT0w!tC)2|L%h0l+ zA4%B|hokgC35y5^EJLn_p#{+@sM-@0RnfQ{fg~@Cr6^88Qrkd{$_T>|c+|Qh6+ru_ z)<8jb1QvN_AWlZI97MZNyG;Oh1d0pK5~hNrAf9Qh@EKNF4V`- qD6ONo0YZjj^nf^m?iP=NArAm^&rGW3WQULd00006(%Bdya7PzE*BXn`C*cyp-&zlCONogq<}W^3IKk8Z88s_ zjlBYZwkiCd_Z7fC`Q`x=X{D%$1m8*n0M~Dp0l=1!5#Ceu|Lofwux2s|;eLv)GRG46T+fZlGBBxi*Gaj04h<=IqmVmT}= zYO2-h5mnuQt2_QTqk29b{9)c3BKVtXUQo)c8 zo;)H_+mISc|EKz1vQ0M4zBjYm*6zH{H~C)L**Eh(o$kJwS;w)mva+(Wva*Vhs@3XU za8D-#*aDZq2jJ)6xYR^%G5{(b=fP{>q^RV<$C?0YJ_j#@uZu_?lL`o+<|FVO7yxeU31%j|HYWM70Ng(2h?qriuMx>J zk3mc>rP0?W5jST<@`^$TZ)%Se{iVRaiCGYjeDWHV;1zg5(eEoF_RD4^R%xX*N@0XB z$&4=!{^Jw-Pzr!r-9NyL@f}eirlbHsZ!lvVBjQg>0)XXiAJM*xD30Dp0>BnI@ZG*o z`+aEuwgcV=zDon}lNsl#Py1ie02CA}#rpOa(f|~haX#w=0Nh_-#i#w5GyppRZ-PIh z0mv}p-1TYyMG}BARSx4p39%k{D+$0SW{jgm{3lWX;IDg|8DmcJVhESRkD2k^AP!KD z|M!MS5BOUPHbj;b{icc7FWRvs78n8p0H^(wqQ4~g)F&=}7HikOU?2!YjHlp!jlMn; zakK5-7T1Q*Kp!*Aws`wrq|k*BGWIyQM8r%->1a#|AwO7i6VLeia(C7-N%ar{ zE{E%j9kX7N@`Df(zyWtsWE6ye0PtRrG5$qKF9b~hcvPMh`_3={03U68?rv|w=_EnI mBh-u3R#sM4R#sM4&C)-NZ>|iTj)zbH0000`Wo36?lHF`;)TJV#h>E&`3JRhkL`CCAv{)KjgB44osj2#J zy+}-;K?;IFJ*`1laXXebzy?4eWAqavX2!bF8f*=TjAc$ z(AU@ZgAnlB;Nal3{{H^j@bK^|kV4DM5r&3_eiY+RO8Mx@q!4klTCJ8Ejm9EcD1yWo z3HZMV5ics0%AsPhxSVDT!1(z1H)6;Wp#(S*K8Qs9SuU4<=2~GK`7pj1?R`50G#8l- z4{NpB4w_bH{L?T#3>S+4SKdiERVWm`qnTUC&W!OpK){SFJxJ4ZEiL{P*=aUdYyz6f zgtWDbW}iiNv;@@a^~Ko#L*M1Q(*YzvlG0qC9cGgqEdk9$2nk!|^XH_V*A^1w^Ka$P zYFy6~@?36!07377$9gw#EXMORBgZq@{!?8G1dz~M{k zIPSDwBS$Y~VPBtn*@;C!Q+uh?dW|IA&+@5RB|DJ`_>5w)xJBB1t~n=QG0(N^bb;SGJU8xsx3&0SHNo5l7NA-EQq%Z*T7s>D!oZL6*9Y z34r5Gwx#Qo1YGrON6(S~`wW2LYRcWvVNX3bNI(DqQZ6cgd+M1@Pf0)k0b;l(n%^!X zyw|f3PmqAH8K5#8!j`QLWXL%p0bvA8Di`53o^!<%3FsgJ@b{1aN>C;j;ip`L=l2|N zS$Pg9d+Pd?1OyPEbtU6cpNGhsBp|>Qq0RS{&tIV{B0d3k4ZnS2-h(n(b4N=V&~8Cn zEx^d0kg|;g*a9+R+JFp3w?}?n)$8@8B)|}olu^CMkV-S+>B|U1lD43HBT?S_DwRr` zeL;Dt^B@UO5`f$wi(H6tFZj|@;3w*e#4MzPXzyipC^2iSP$+zF2t%%r^7n#C%xV|N zR*p%6mH;O^qxd)Dj6dz`Ypqt>mvk906B;o{&=BBqfEw1cku*&=`Lti^eU^0T9pWf* z)T#mZV%D8XrLxo7Ib>4fhP~pxST*TZ#E>H&$$=L%F#3-uE@(WTlVq%gEJvN_ko0G- zls5`5>Muz~5G+TrJK};ani#@prtS-e%2CWdApKDK@Q1?q*!OQtG6=IA#cCKXY4^mX z=A!n|B*QStQH=IVrBcCn3#Qq99lEe*QjXef2fH2Ro#4+N&pbTVVUnYEBnO$%hWl!h zA?JzA%aOH09M0r>=e1ANxRDQe>PzwnpB!oZ`7(MpNF67AIsUU~{u}vTG08c;Dn~xU z%lG=RaJpy5J{pZboyVujOmd|4h6WfPPa5y0**$U;n*iswz9q(ANsBAwC?)}r9NCs0 z=hOBEIf_Mq)b$v-pERvej@lu>$;~qv^@DuAK5!<45`aY(m-F~idI#|(0m$E%avdW+ z^ZuNL0QX|n??|e}VG4gw_@DU99dHoKLdcLH2!bF8f*=TjAP9oU20j7Y3d(5xwO}s* O0000Kc505v!A#J*_9=~nK;9V3s3;&AL317F*2RT zcIJ~Gf&lZj)JNQej4xt4$q5TVfO!rRtC0BxY)2tLA+g^0`?P}yY)Q4T9y=^#J5m8s zgs3F85x*)$m?YN4QiR29M=L-|nd^uh#J>s=rua#K?F1k|Qu+Ku^xDc#5upmU6OaI= zo+9E^;%}h{qr@$-x1FE_F!kk|j&@Qg!YktPSldpJ0wmSFgE%N0C$z@ecES+A#85#T z5!5>!sc8$X?Sv(OiDkFYj^0FK%EHBV!W6*7bVMk^g;?5-VkGReBx!DnE5`5B)jyQ8 z>j{2rMrt5c_`Ji+$jOEnwW)QaHI2+3K z^$=Go<82efQ4=fM@i`scr}1qwcl6y72ishJrjnRgN^}r_`I1ibGIsus7infnh$~p}J^aA`OZWgxvfu*cl5I`>gvtHQt^OkZRLjX4c?h4ypYbj>{ z0=NnAUfA|(%M{8m1aK2zSlIR*mIeH85Wr17lhB^Q&ABtCz7l1e+XUYKA!FMTm!ch6>fr6^&xv~-;wv_NMZz+U zsPl3aLD-Q%o7mWqPs{06*xf|zK4B)YBZo41aCTd8jK1WIzBO^?ov&YWaU-R^v~6GT&L3AQJ!p0000000000006*Jz<(h#8EXKO R!~_5U002ovPDHLkV1mMnIO_la literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/act_highlight_edit.xml b/app/src/main/res/layout/act_highlight_edit.xml new file mode 100644 index 00000000..e82e5fc7 --- /dev/null +++ b/app/src/main/res/layout/act_highlight_edit.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + +