From 492ec81fb44213f3974f81fd8a504cbac0338995 Mon Sep 17 00:00:00 2001 From: tateisu Date: Mon, 8 May 2017 10:16:43 +0900 Subject: [PATCH] v0.4.1 --- app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 11 +- .../java/jp/juggler/subwaytooter/ActMain.java | 54 +---- .../jp/juggler/subwaytooter/ActMutedApp.java | 1 + .../jp/juggler/subwaytooter/ActMutedWord.java | 217 ++++++++++++++++++ .../java/jp/juggler/subwaytooter/ActPost.java | 40 +++- .../java/jp/juggler/subwaytooter/ActText.java | 180 +++++++++++++++ .../jp/juggler/subwaytooter/AlarmService.java | 36 +-- .../java/jp/juggler/subwaytooter/App1.java | 6 +- .../java/jp/juggler/subwaytooter/Column.java | 51 ++-- .../juggler/subwaytooter/DlgContextMenu.java | 8 +- .../subwaytooter/api/entity/TootAccount.java | 15 +- .../subwaytooter/api/entity/TootStatus.java | 36 ++- .../juggler/subwaytooter/table/AcctColor.java | 3 +- .../juggler/subwaytooter/table/MutedWord.java | 114 +++++++++ .../juggler/subwaytooter/util/Emojione.java | 3 +- .../jp/juggler/subwaytooter/util/Utils.java | 48 ++++ app/src/main/res/layout/act_text.xml | 87 +++++++ app/src/main/res/layout/dlg_context_menu.xml | 4 +- app/src/main/res/layout/lv_status.xml | 2 +- app/src/main/res/layout/lv_status_simple.xml | 2 +- app/src/main/res/menu/men_navi_drawer.xml | 5 + app/src/main/res/values-fr/strings.xml | 8 + app/src/main/res/values-ja/strings.xml | 8 + app/src/main/res/values/colors.xml | 4 +- app/src/main/res/values/strings.xml | 9 + 26 files changed, 842 insertions(+), 114 deletions(-) create mode 100644 app/src/main/java/jp/juggler/subwaytooter/ActMutedWord.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/ActText.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/table/MutedWord.java create mode 100644 app/src/main/res/layout/act_text.xml diff --git a/app/build.gradle b/app/build.gradle index 74973757..26ec1eac 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,8 +9,8 @@ android { applicationId "jp.juggler.subwaytooter" minSdkVersion 21 targetSdkVersion 25 - versionCode 40 - versionName "0.4.0" + versionCode 41 + versionName "0.4.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 486885d4..9ce22b2f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -131,7 +131,10 @@ android:name=".ActMutedApp" android:label="@string/muted_app" /> - + - + diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMain.java b/app/src/main/java/jp/juggler/subwaytooter/ActMain.java index 73fb4649..21a7a185 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMain.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMain.java @@ -12,6 +12,7 @@ import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; import android.support.customtabs.CustomTabsIntent; +import android.support.v4.text.BidiFormatter; import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewPager; import android.support.v7.app.AlertDialog; @@ -454,6 +455,10 @@ public class ActMain extends AppCompatActivity }else if( id == R.id.nav_muted_app ){ startActivity( new Intent( this, ActMutedApp.class ) ); + + }else if( id == R.id.nav_muted_word ){ + startActivity( new Intent( this, ActMutedWord.class ) ); + // Handle the camera action // }else if( id == R.id.nav_gallery ){ // @@ -1630,8 +1635,14 @@ public class ActMain extends AppCompatActivity ); return; }else if( bFollow ){ + BidiFormatter bidiFormatter = BidiFormatter.getInstance(); + String msg = getString( R.string.confirm_follow_who_from + , bidiFormatter.unicodeWrap(who.display_name ) + , AcctColor.getNickname( access_info.acct ) + ); + DlgConfirm.open( this - , getString( R.string.confirm_follow_who_from, who.display_name, AcctColor.getNickname( access_info.acct ) ) + , msg , new DlgConfirm.Callback() { @Override public boolean isConfirmEnabled(){ return access_info.confirm_follow; @@ -2212,47 +2223,6 @@ public class ActMain extends AppCompatActivity //////////////////////////////////////////////// - void sendStatus( SavedAccount access_info, TootStatus status ){ - try{ - StringBuilder sb = new StringBuilder(); - sb.append( getString( R.string.send_header_url ) ); - sb.append( ": " ); - sb.append( status.url ); - sb.append( "\n" ); - sb.append( getString( R.string.send_header_date ) ); - sb.append( ": " ); - sb.append( TootStatus.formatTime( status.time_created_at ) ); - sb.append( "\n" ); - sb.append( getString( R.string.send_header_from_acct ) ); - sb.append( ": " ); - sb.append( access_info.getFullAcct( status.account ) ); - sb.append( "\n" ); - sb.append( getString( R.string.send_header_from_name ) ); - sb.append( ": " ); - sb.append( status.account.display_name ); - sb.append( "\n" ); - if( ! TextUtils.isEmpty( status.spoiler_text ) ){ - sb.append( getString( R.string.send_header_content_warning ) ); - sb.append( ": " ); - sb.append( HTMLDecoder.decodeHTMLForClipboard( access_info, status.spoiler_text ) ); - sb.append( "\n" ); - } - sb.append( "\n" ); - sb.append( HTMLDecoder.decodeHTMLForClipboard( access_info, status.content ) ); - - Intent intent = new Intent(); - intent.setAction( Intent.ACTION_SEND ); - intent.setType( "text/plain" ); - intent.putExtra( Intent.EXTRA_TEXT, sb.toString() ); - startActivity( intent ); - - }catch( Throwable ex ){ - log.e( ex, "sendStatus failed." ); - ex.printStackTrace(); - Utils.showToast( this, ex, "sendStatus failed." ); - } - } - //////////////////////////////////////////////// final RelationChangedCallback follow_complete_callback = new RelationChangedCallback() { diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMutedApp.java b/app/src/main/java/jp/juggler/subwaytooter/ActMutedApp.java index 6bc4327f..0853baf1 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMutedApp.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMutedApp.java @@ -156,6 +156,7 @@ public class ActMutedApp extends AppCompatActivity { } void bind( MyItem item ){ + itemView.setTag( item ); // itemView は親クラスのメンバ変数 tvName.setText( item.name ); } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMutedWord.java b/app/src/main/java/jp/juggler/subwaytooter/ActMutedWord.java new file mode 100644 index 00000000..498b2bd2 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMutedWord.java @@ -0,0 +1,217 @@ +package jp.juggler.subwaytooter; + +import android.content.Context; +import android.database.Cursor; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.woxthebox.draglistview.DragItem; +import com.woxthebox.draglistview.DragItemAdapter; +import com.woxthebox.draglistview.DragListView; +import com.woxthebox.draglistview.swipe.ListSwipeHelper; +import com.woxthebox.draglistview.swipe.ListSwipeItem; + +import java.util.ArrayList; + +import jp.juggler.subwaytooter.table.MutedWord; + +public class ActMutedWord extends AppCompatActivity { + + DragListView listView; + MyListAdapter listAdapter; + + @Override + protected void onCreate( @Nullable Bundle savedInstanceState ){ + super.onCreate( savedInstanceState ); + App1.setActivityTheme(this,false); + initUI(); + loadData(); + } + + @Override public void onBackPressed(){ + setResult( RESULT_OK ); + super.onBackPressed(); + } + + private void initUI(){ + setContentView( R.layout.act_mute_app ); + + // リストのアダプター + listAdapter = new MyListAdapter(); + + // ハンドル部分をドラッグで並べ替えできるRecyclerView + listView = (DragListView) findViewById( R.id.drag_list_view ); + listView.setLayoutManager( new LinearLayoutManager( this ) ); + listView.setAdapter( listAdapter, false ); + + listView.setCanDragHorizontally( true ); + listView.setDragEnabled( false ); + listView.setCustomDragItem( new MyDragItem( this, R.layout.lv_mute_app ) ); + + listView.getRecyclerView().setVerticalScrollBarEnabled( true ); +// listView.setDragListListener( new DragListView.DragListListenerAdapter() { +// @Override +// public void onItemDragStarted( int position ){ +// // 操作中はリフレッシュ禁止 +// // mRefreshLayout.setEnabled( false ); +// } +// +// @Override +// public void onItemDragEnded( int fromPosition, int toPosition ){ +// // 操作完了でリフレッシュ許可 +// // mRefreshLayout.setEnabled( USE_SWIPE_REFRESH ); +// +//// if( fromPosition != toPosition ){ +//// // 並べ替えが発生した +//// } +// } +// } ); + + // リストを左右スワイプした + listView.setSwipeListener( new ListSwipeHelper.OnSwipeListenerAdapter() { + + @Override + public void onItemSwipeStarted( ListSwipeItem item ){ + // 操作中はリフレッシュ禁止 + // mRefreshLayout.setEnabled( false ); + } + + @Override + public void onItemSwipeEnded( ListSwipeItem item, ListSwipeItem.SwipeDirection swipedDirection ){ + // 操作完了でリフレッシュ許可 + // mRefreshLayout.setEnabled( USE_SWIPE_REFRESH ); + + // 左にスワイプした(右端に青が見えた) なら要素を削除する + if( swipedDirection == ListSwipeItem.SwipeDirection.LEFT ){ + Object o = item.getTag(); + if( o instanceof MyItem){ + MyItem adapterItem = ( MyItem ) o; + MutedWord.delete( adapterItem.name ); + listAdapter.removeItem( listAdapter.getPositionForItem( adapterItem ) ); + } + } + } + } ); + } + + private void loadData(){ + + ArrayList< MyItem > tmp_list = new ArrayList<>(); + try{ + Cursor cursor = MutedWord.createCursor(); + if( cursor != null ){ + try{ + int idx_name = cursor.getColumnIndex( MutedWord.COL_NAME ); + while( cursor.moveToNext() ){ + String name = cursor.getString( idx_name); + MyItem item = new MyItem( name ); + tmp_list.add( item ); + } + + }finally{ + cursor.close(); + } + } + }catch( Throwable ex ){ + ex.printStackTrace(); + } + listAdapter.setItemList( tmp_list ); + } + + + // リスト要素のデータ + static class MyItem { + String name; + MyItem(String name ){ + this.name = name; + } + } + + + // リスト要素のViewHolder + static class MyViewHolder extends DragItemAdapter.ViewHolder { + + final TextView tvName; + + MyViewHolder( final View viewRoot ){ + super( viewRoot + , R.id.ivDragHandle // View ID。 ここを押すとドラッグ操作をすぐに開始する + , false // 長押しでドラッグ開始するなら真 + ); + + tvName = (TextView) viewRoot.findViewById( R.id.tvName ); + + // リスト要素のビューが ListSwipeItem だった場合、Swipe操作を制御できる + if( viewRoot instanceof ListSwipeItem ){ + ListSwipeItem lsi = (ListSwipeItem) viewRoot; + lsi.setSwipeInStyle( ListSwipeItem.SwipeInStyle.SLIDE ); + lsi.setSupportedSwipeDirection( ListSwipeItem.SwipeDirection.LEFT ); + } + + } + + void bind( MyItem item ){ + itemView.setTag( item ); // itemView は親クラスのメンバ変数 + tvName.setText( item.name ); + } + +// @Override +// public boolean onItemLongClicked( View view ){ +// return false; +// } + +// @Override +// public void onItemClicked( View view ){ +// } + } + + // ドラッグ操作中のデータ + 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.item_layout).setBackgroundColor( + Styler.getAttributeColor( ActMutedWord.this, R.attr.list_item_bg_pressed_dragged) + ); + } + } + + private class MyListAdapter extends DragItemAdapter< MyItem, MyViewHolder > { + + MyListAdapter(){ + super(); + setHasStableIds( true ); + setItemList( new ArrayList< MyItem >() ); + } + + @Override + public MyViewHolder onCreateViewHolder( ViewGroup parent, int viewType ){ + View view = getLayoutInflater().inflate( R.layout.lv_mute_app, 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 getItemId( int position ){ + MyItem item = mItemList.get( position ); // mItemList は親クラスのメンバ変数 + return item.name.hashCode(); + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPost.java b/app/src/main/java/jp/juggler/subwaytooter/ActPost.java index 96d5ad5f..681a9f7c 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPost.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPost.java @@ -48,6 +48,8 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import jp.juggler.subwaytooter.api.TootApiClient; import jp.juggler.subwaytooter.api.TootApiResult; @@ -158,7 +160,7 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener, break; case R.id.btnPost: - performPost( false ); + performPost( false,false ); break; case R.id.btnRemoveReply: @@ -867,7 +869,7 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener, // 設定からリサイズ指定を読む int resize_to = list_resize_max[ pref.getInt( Pref.KEY_RESIZE_IMAGE, 4 ) ]; - Bitmap bitmap = Utils.createResizedBitmap( log, this, uri, true,resize_to ); + Bitmap bitmap = Utils.createResizedBitmap( log, this, uri, true, resize_to ); if( bitmap != null ){ try{ File cache_dir = getExternalCacheDir(); @@ -1251,7 +1253,18 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener, /////////////////////////////////////////////////////////////////////////////////////// // post - private void performPost( boolean bConfirm ){ + // [:word:] 単語構成文字 (Letter | Mark | Decimal_Number | Connector_Punctuation) + // [:alpha:] 英字 (Letter | Mark) + + static final String word = "[_\\p{L}\\p{M}\\p{Nd}\\p{Pc}]"; + static final String alpha = "[_\\p{L}\\p{M}]"; + + static final Pattern reTag = Pattern.compile( + "(?:^|[^/)\\w])#(" + word + "*" + alpha + word + "*)" + , Pattern.CASE_INSENSITIVE + ); + + private void performPost( final boolean bConfirmTag, final boolean bConfirmAccount ){ final String content = etContent.getText().toString().trim(); if( TextUtils.isEmpty( content ) ){ Utils.showToast( this, true, R.string.post_error_contents_empty ); @@ -1269,7 +1282,7 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener, } } - if( ! bConfirm ){ + if( ! bConfirmAccount ){ DlgConfirm.open( this , getString( R.string.confirm_post_from, AcctColor.getNickname( account.acct ) ) , new DlgConfirm.Callback() { @@ -1283,12 +1296,29 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener, } @Override public void onOK(){ - performPost( true ); + performPost( bConfirmTag, true ); } } ); return; } + if( ! bConfirmTag ){ + Matcher m = reTag.matcher( content ); + if( m.find() && ! TootStatus.VISIBILITY_PUBLIC.equals( visibility ) ){ + new AlertDialog.Builder( this ) + .setCancelable( true ) + .setMessage( R.string.hashtag_and_visibility_not_match ) + .setNegativeButton( R.string.cancel, null ) + .setPositiveButton( R.string.ok, new DialogInterface.OnClickListener() { + @Override public void onClick( DialogInterface dialog, int which ){ + performPost( true, bConfirmAccount ); + } + } ) + .show(); + return; + } + } + final StringBuilder sb = new StringBuilder(); sb.append( "status=" ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActText.java b/app/src/main/java/jp/juggler/subwaytooter/ActText.java new file mode 100644 index 00000000..8bd12122 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/ActText.java @@ -0,0 +1,180 @@ +package jp.juggler.subwaytooter; + +import android.app.SearchManager; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.app.AppCompatActivity; +import android.text.TextUtils; +import android.view.View; +import android.widget.EditText; + +import jp.juggler.subwaytooter.api.entity.TootStatus; +import jp.juggler.subwaytooter.table.MutedWord; +import jp.juggler.subwaytooter.table.SavedAccount; +import jp.juggler.subwaytooter.util.HTMLDecoder; +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class ActText extends AppCompatActivity implements View.OnClickListener { + + static final LogCategory log = new LogCategory( "ActText" ); + + static String encodeStatus( Context context, SavedAccount access_info, TootStatus status ){ + StringBuilder sb = new StringBuilder(); + sb.append( context.getString( R.string.send_header_url ) ); + sb.append( ": " ); + sb.append( status.url ); + sb.append( "\n" ); + sb.append( context.getString( R.string.send_header_date ) ); + sb.append( ": " ); + sb.append( TootStatus.formatTime( status.time_created_at ) ); + sb.append( "\n" ); + sb.append( context.getString( R.string.send_header_from_acct ) ); + sb.append( ": " ); + sb.append( access_info.getFullAcct( status.account ) ); + sb.append( "\n" ); + sb.append( context.getString( R.string.send_header_from_name ) ); + sb.append( ": " ); + sb.append( status.account.display_name ); + sb.append( "\n" ); + if( ! TextUtils.isEmpty( status.spoiler_text ) ){ + sb.append( context.getString( R.string.send_header_content_warning ) ); + sb.append( ": " ); + sb.append( HTMLDecoder.decodeHTMLForClipboard( access_info, status.spoiler_text ) ); + sb.append( "\n" ); + } + sb.append( "\n" ); + sb.append( HTMLDecoder.decodeHTMLForClipboard( access_info, status.content ) ); + return sb.toString(); + } + + static final String EXTRA_TEXT = "text"; + + public static void open( ActMain activity, SavedAccount access_info, TootStatus status ){ + String sv = encodeStatus( activity, access_info, status ); + Intent intent = new Intent( activity, ActText.class ); + intent.putExtra( EXTRA_TEXT, sv ); + activity.startActivity( intent ); + } + + @Override protected void onCreate( @Nullable Bundle savedInstanceState ){ + super.onCreate( savedInstanceState ); + initUI(); + + + if( savedInstanceState == null ){ + Intent intent = getIntent(); + String sv = intent.getStringExtra( EXTRA_TEXT ); + etText.setText(sv); + etText.setSelection( 0,sv.length() ); + } + } + + + EditText etText; + + void initUI(){ + setContentView( R.layout.act_text ); + etText = (EditText) findViewById( R.id.etText ); + + findViewById( R.id.btnCopy ).setOnClickListener( this ); + findViewById( R.id.btnSearch ).setOnClickListener( this ); + findViewById( R.id.btnSend ).setOnClickListener( this ); + findViewById( R.id.btnMuteWord ).setOnClickListener( this ); + + } + + @Override public void onClick( View v ){ + switch( v.getId() ){ + case R.id.btnCopy: + copy(); + break; + case R.id.btnSearch: + search(); + break; + case R.id.btnSend: + send(); + + break; + case R.id.btnMuteWord: + muteWord(); + break; + + } + } + + private String getSelection(){ + int s = etText.getSelectionStart(); + int e = etText.getSelectionEnd(); + String text = etText.getText().toString(); + if( s == e ){ + return text; + }else{ + return text.substring( s, e ); + } + } + + private void copy(){ + try{ + // Gets a handle to the clipboard service. + ClipboardManager clipboard = (ClipboardManager) + getSystemService( Context.CLIPBOARD_SERVICE ); + // Creates a new text clip to put on the clipboard + ClipData clip = ClipData.newPlainText( "text", getSelection() ); + // Set the clipboard's primary clip. + clipboard.setPrimaryClip( clip ); + + Utils.showToast( this,false,R.string.copy_complete ); + }catch( Throwable ex ){ + ex.printStackTrace(); + Utils.showToast( this, ex, "copy failed." ); + } + } + + private void search(){ + try{ + Intent intent = new Intent(Intent.ACTION_WEB_SEARCH); + intent.putExtra( SearchManager.QUERY, getSelection() ); + if( intent.resolveActivity(getPackageManager()) != null ) { + startActivity(intent); + } + }catch( Throwable ex ){ + ex.printStackTrace(); + Utils.showToast( this, ex, "search failed." ); + } + + } + + private void send(){ + try{ + + Intent intent = new Intent(); + intent.setAction( Intent.ACTION_SEND ); + intent.setType( "text/plain" ); + intent.putExtra( Intent.EXTRA_TEXT, getSelection() ); + startActivity( intent ); + + }catch( Throwable ex ){ + ex.printStackTrace(); + Utils.showToast( this, ex, "send failed." ); + } + } + + private void muteWord(){ + try{ + MutedWord.save( getSelection() ); + for( Column column : App1.getAppState( this ).column_list ){ + column.removeMuteApp(); + } + Utils.showToast( this, false, R.string.word_was_muted ); + }catch( Throwable ex ){ + ex.printStackTrace(); + Utils.showToast( this, ex, "muteWord failed." ); + } + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/AlarmService.java b/app/src/main/java/jp/juggler/subwaytooter/AlarmService.java index 663fd0c0..012c19bc 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/AlarmService.java +++ b/app/src/main/java/jp/juggler/subwaytooter/AlarmService.java @@ -31,6 +31,7 @@ import jp.juggler.subwaytooter.api.entity.TootApplication; import jp.juggler.subwaytooter.api.entity.TootNotification; import jp.juggler.subwaytooter.api.entity.TootStatus; import jp.juggler.subwaytooter.table.MutedApp; +import jp.juggler.subwaytooter.table.MutedWord; import jp.juggler.subwaytooter.table.NotificationTracking; import jp.juggler.subwaytooter.table.SavedAccount; import jp.juggler.subwaytooter.util.LogCategory; @@ -129,13 +130,15 @@ public class AlarmService extends IntentService { }else if( ACTION_NOTIFICATION_CLICK.equals( action ) ){ log.d( "Notification clicked!" ); long db_id = received_intent.getLongExtra( EXTRA_DB_ID, 0L ); - NotificationTracking.updateRead( db_id ); - notification_manager.cancel( Long.toString( db_id ), NOTIFICATION_ID ); - // + // 画面を開く intent = new Intent( this, ActCallback.class ); intent.setData( Uri.parse( "subwaytooter://notification_click?db_id=" + db_id ) ); intent.addFlags( Intent.FLAG_ACTIVITY_NEW_TASK ); startActivity( intent ); + // 通知をキャンセル + notification_manager.cancel( Long.toString( db_id ), NOTIFICATION_ID ); + // DB更新処理 + NotificationTracking.updateRead( db_id ); return; } @@ -156,7 +159,8 @@ public class AlarmService extends IntentService { boolean bAlarmRequired = false; HashSet< String > muted_app = MutedApp.getNameSet(); - + HashSet< String > muted_word = MutedWord.getNameSet(); + for( SavedAccount account : account_list ){ try{ if( account.notification_mention @@ -168,7 +172,7 @@ public class AlarmService extends IntentService { ArrayList< Data > data_list = new ArrayList<>(); - checkAccount( client, data_list, account, muted_app ); + checkAccount( client, data_list, account, muted_app ,muted_word); showNotification( account.db_id, data_list ); @@ -200,7 +204,7 @@ public class AlarmService extends IntentService { private static final String PATH_NOTIFICATIONS = "/api/v1/notifications"; - private void checkAccount( TootApiClient client, ArrayList< Data > data_list, SavedAccount account, HashSet< String > muted_app ){ + private void checkAccount( TootApiClient client, ArrayList< Data > data_list, SavedAccount account, HashSet< String > muted_app, HashSet< String > muted_word ){ log.d( "checkAccount account_db_id=%s", account.db_id ); NotificationTracking nr = NotificationTracking.load( account.db_id ); @@ -213,7 +217,7 @@ public class AlarmService extends IntentService { JSONArray array = new JSONArray( nr.last_data ); for( int i = array.length() - 1 ; i >= 0 ; -- i ){ JSONObject src = array.optJSONObject( i ); - update_sub( src, nr, account, dst_array, data_list, duplicate_check, muted_app ); + update_sub( src, nr, account, dst_array, data_list, duplicate_check, muted_app ,muted_word); } }catch( JSONException ex ){ ex.printStackTrace(); @@ -237,7 +241,7 @@ public class AlarmService extends IntentService { JSONArray array = result.array; for( int i = array.length() - 1 ; i >= 0 ; -- i ){ JSONObject src = array.optJSONObject( i ); - update_sub( src, nr, account, dst_array, data_list, duplicate_check, muted_app ); + update_sub( src, nr, account, dst_array, data_list, duplicate_check, muted_app, muted_word ); } }catch( JSONException ex ){ ex.printStackTrace(); @@ -277,7 +281,10 @@ public class AlarmService extends IntentService { , ArrayList< Data > data_list , HashSet< Long > duplicate_check , HashSet< String > muted_app - ) throws JSONException{ + , HashSet< String > muted_word + ) + throws JSONException + { long id = src.optLong( "id" ); @@ -306,18 +313,11 @@ public class AlarmService extends IntentService { return; } - // app mute { TootStatus status = notification.status; if( status != null ){ - TootApplication application = status.application; - if( application != null ){ - String name = application.name; - if( name != null ){ - if( muted_app.contains( name ) ){ - return; - } - } + if( status.checkMuted( muted_app,muted_word )){ + return; } } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/App1.java b/app/src/main/java/jp/juggler/subwaytooter/App1.java index 8890443d..34966b12 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/App1.java +++ b/app/src/main/java/jp/juggler/subwaytooter/App1.java @@ -35,6 +35,7 @@ import jp.juggler.subwaytooter.table.ClientInfo; import jp.juggler.subwaytooter.table.ContentWarning; import jp.juggler.subwaytooter.table.LogData; import jp.juggler.subwaytooter.table.MediaShown; +import jp.juggler.subwaytooter.table.MutedWord; import jp.juggler.subwaytooter.table.NotificationTracking; import jp.juggler.subwaytooter.table.SavedAccount; import jp.juggler.subwaytooter.table.UserRelation; @@ -50,7 +51,7 @@ public class App1 extends Application { static final LogCategory log = new LogCategory( "App1" ); static final String DB_NAME = "app_db"; - static final int DB_VERSION = 10; + static final int DB_VERSION = 11; // 2017/4/25 v10 1=>2 SavedAccount に通知設定を追加 // 2017/4/25 v10 1=>2 NotificationTracking テーブルを追加 // 2017/4/29 v20 2=>5 MediaShown,ContentWarningのインデクスが間違っていたので貼り直す @@ -59,6 +60,7 @@ public class App1 extends Application { // 2017/5/02 v32 7=>8 (この変更は取り消された) // 2017/5/02 v32 8=>9 AcctColor テーブルの追加 // 2017/5/04 v33 9=>10 SavedAccountに項目追加 + // 2017/5/08 v41 10=>11 MutedWord テーブルの追加 static DBOpenHelper db_open_helper; @@ -101,6 +103,7 @@ public class App1 extends Application { UserRelation.onDBCreate( db ); AcctSet.onDBCreate( db ); AcctColor.onDBCreate( db ); + MutedWord.onDBCreate( db ); } @Override @@ -116,6 +119,7 @@ public class App1 extends Application { UserRelation.onDBUpgrade( db, oldVersion, newVersion ); AcctSet.onDBUpgrade( db, oldVersion, newVersion ); AcctColor.onDBUpgrade( db, oldVersion, newVersion ); + MutedWord.onDBUpgrade( db, oldVersion, newVersion ); } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/Column.java b/app/src/main/java/jp/juggler/subwaytooter/Column.java index 7925bc46..3b5955a2 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/Column.java +++ b/app/src/main/java/jp/juggler/subwaytooter/Column.java @@ -22,7 +22,6 @@ import java.util.regex.Pattern; import jp.juggler.subwaytooter.api.TootApiClient; import jp.juggler.subwaytooter.api.TootApiResult; import jp.juggler.subwaytooter.api.entity.TootAccount; -import jp.juggler.subwaytooter.api.entity.TootApplication; import jp.juggler.subwaytooter.api.entity.TootAttachment; import jp.juggler.subwaytooter.api.entity.TootContext; import jp.juggler.subwaytooter.api.entity.TootGap; @@ -34,6 +33,7 @@ import jp.juggler.subwaytooter.api.entity.TootStatus; import jp.juggler.subwaytooter.table.AcctColor; import jp.juggler.subwaytooter.table.AcctSet; import jp.juggler.subwaytooter.table.MutedApp; +import jp.juggler.subwaytooter.table.MutedWord; import jp.juggler.subwaytooter.table.SavedAccount; import jp.juggler.subwaytooter.table.UserRelation; import jp.juggler.subwaytooter.util.LogCategory; @@ -93,11 +93,11 @@ class Column { private static final String KEY_DONT_SHOW_REPLY = "dont_show_reply"; private static final String KEY_REGEX_TEXT = "regex_text"; - static final String KEY_HEADER_BACKGROUND_COLOR = "header_background_color"; - static final String KEY_HEADER_TEXT_COLOR = "header_text_color"; - static final String KEY_COLUMN_BACKGROUND_COLOR = "column_background_color"; - static final String KEY_COLUMN_BACKGROUND_IMAGE = "column_background_image"; - static final String KEY_COLUMN_BACKGROUND_IMAGE_ALPHA = "column_background_image_alpha"; + private static final String KEY_HEADER_BACKGROUND_COLOR = "header_background_color"; + private static final String KEY_HEADER_TEXT_COLOR = "header_text_color"; + private static final String KEY_COLUMN_BACKGROUND_COLOR = "column_background_color"; + private static final String KEY_COLUMN_BACKGROUND_IMAGE = "column_background_image"; + private static final String KEY_COLUMN_BACKGROUND_IMAGE_ALPHA = "column_background_image_alpha"; private static final String KEY_PROFILE_ID = "profile_id"; private static final String KEY_PROFILE_TAB = "tab"; @@ -726,17 +726,14 @@ class Column { ArrayList< Object > tmp_list = new ArrayList<>( list_data.size() ); HashSet< String > muted_app = MutedApp.getNameSet(); + HashSet< String > muted_word = MutedWord.getNameSet(); for( Object o : list_data ){ if( o instanceof TootStatus ){ TootStatus item = (TootStatus) o; - TootApplication application = item.application; - if( application != null ){ - String name = application.name; - if( name != null && muted_app.contains( name ) ){ - log.d( "removeMuteApp: mute app %s", name ); - continue; - } + if( item.checkMuted( muted_app,muted_word )){ + continue; + } } if( o instanceof TootNotification ){ @@ -744,9 +741,8 @@ class Column { TootStatus status = item.status; if( status != null ){ - if( status.application != null ){ - String sv = status.application.name; - if( sv != null && muted_app.contains( sv ) ) continue; + if( status.checkMuted( muted_app,muted_word )){ + continue; } } } @@ -772,7 +768,8 @@ class Column { } HashSet< String > muted_app = MutedApp.getNameSet(); - + HashSet< String > muted_word = MutedWord.getNameSet(); + for( TootStatus status : src ){ if( with_attachment ){ if( ! hasMedia( status ) && ! hasMedia( status.reblog ) ) continue; @@ -799,12 +796,8 @@ class Column { } } - if( status.application != null ){ - String sv = status.application.name; - if( sv != null && muted_app.contains( sv ) ){ - log.d( "addWithFilter: mute app %s", sv ); - continue; - } + if( status.checkMuted( muted_app,muted_word )){ + continue; } dst.add( status ); @@ -815,19 +808,18 @@ class Column { private void addWithFilter( ArrayList< Object > dst, TootNotification.List src ){ HashSet< String > muted_app = MutedApp.getNameSet(); + HashSet< String > muted_word = MutedWord.getNameSet(); for( TootNotification item : src ){ TootStatus status = item.status; if( status != null ){ - if( status.application != null ){ - String sv = status.application.name; - if( sv != null && muted_app.contains( sv ) ){ - log.d( "addWithFilter: mute app %s", sv ); - continue; - } + if( status.checkMuted( muted_app,muted_word ) ){ + log.d( "addWithFilter: status muted."); + continue; } + } dst.add( item ); @@ -2243,6 +2235,7 @@ class Column { fireShowContent(); if( holder != null ){ + //noinspection StatementWithEmptyBody if(restore_idx >= 0 ){ setItemTop( restore_idx + added - 1, restore_y ); }else{ diff --git a/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.java b/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.java index c4da3e44..a88e6458 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.java +++ b/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.java @@ -61,7 +61,7 @@ class DlgContextMenu implements View.OnClickListener { View llStatus = viewRoot.findViewById( R.id.llStatus ); View btnStatusWebPage = viewRoot.findViewById( R.id.btnStatusWebPage ); - View btnStatusSendApp = viewRoot.findViewById( R.id.btnStatusSendApp ); + View btnText = viewRoot.findViewById( R.id.btnText ); View btnFavouriteAnotherAccount = viewRoot.findViewById( R.id.btnFavouriteAnotherAccount ); View btnBoostAnotherAccount = viewRoot.findViewById( R.id.btnBoostAnotherAccount ); View btnDelete = viewRoot.findViewById( R.id.btnDelete ); @@ -98,7 +98,7 @@ class DlgContextMenu implements View.OnClickListener { boolean status_by_me = access_info.isMe( status.account ); btnStatusWebPage.setOnClickListener( this ); - btnStatusSendApp.setOnClickListener( this ); + btnText.setOnClickListener( this ); if( account_list_non_pseudo_same_instance.isEmpty() ){ btnFavouriteAnotherAccount.setVisibility( View.GONE ); @@ -237,9 +237,9 @@ class DlgContextMenu implements View.OnClickListener { } break; - case R.id.btnStatusSendApp: + case R.id.btnText: if( status != null ){ - activity.sendStatus( access_info, status ); + ActText.open( activity,access_info,status); } break; diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.java index fb202e38..d502ca9d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.java +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.java @@ -97,7 +97,7 @@ public class TootAccount { if( TextUtils.isEmpty( sv ) ){ dst.display_name = dst.username; }else{ - dst.display_name = Emojione.decodeEmoji( HTMLDecoder.decodeEntity(sv ) ); + dst.display_name = filterDisplayName(sv); } dst.locked = src.optBoolean( "locked" ); @@ -123,6 +123,7 @@ public class TootAccount { } } + public static TootAccount parse( LogCategory log, LinkClickContext account,JSONObject src ){ return parse( log, account, src, new TootAccount() ); } @@ -142,4 +143,16 @@ public class TootAccount { return result; } + private static CharSequence filterDisplayName( String sv ){ + + // decode HTML entity + sv = HTMLDecoder.decodeEntity(sv ); + + // sanitize LRO,RLO + sv = Utils.sanitizeBDI( sv); + + // decode emoji code + return Emojione.decodeEmoji( sv ) ; + } + } 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 3ac93f5c..94677694 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 @@ -11,6 +11,7 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; +import java.util.HashSet; import java.util.Locale; import java.util.TimeZone; import java.util.regex.Matcher; @@ -24,7 +25,6 @@ import jp.juggler.subwaytooter.util.Utils; public class TootStatus extends TootId { - public static class List extends ArrayList< TootStatus > { public List(){ @@ -230,4 +230,38 @@ public class TootStatus extends TootId { } } + + public boolean checkMuted( HashSet< String > muted_app, HashSet< String > muted_word ){ + + // app mute + if( application != null ){ + String name = application.name; + if( name != null ){ + if( muted_app.contains( name ) ){ + return true; + } + } + } + + // word mute + for( String word: muted_word ){ + if( decoded_content != null && decoded_content.toString().contains( word ) ){ + return true; + } + if( decoded_spoiler_text != null && decoded_spoiler_text.toString().contains( word ) ){ + return true; + } + } + + // reblog + //noinspection RedundantIfStatement + if( reblog != null && reblog.checkMuted( muted_app,muted_word ) ){ + return true; + } + + return false; + + } + + } diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/AcctColor.java b/app/src/main/java/jp/juggler/subwaytooter/table/AcctColor.java index 2bb5e264..fe848c6b 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/AcctColor.java +++ b/app/src/main/java/jp/juggler/subwaytooter/table/AcctColor.java @@ -6,6 +6,7 @@ import android.database.sqlite.SQLiteDatabase; import jp.juggler.subwaytooter.App1; import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -142,7 +143,7 @@ public class AcctColor { @NonNull public static String getNickname( @NonNull String acct ){ AcctColor ac = load( acct ); - return ac != null && ! TextUtils.isEmpty( ac.nickname ) ? ac.nickname : acct; + return ac != null && ! TextUtils.isEmpty( ac.nickname ) ? Utils.sanitizeBDI( ac.nickname ) : acct; } public static boolean hasNickname( @Nullable AcctColor ac ){ diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/MutedWord.java b/app/src/main/java/jp/juggler/subwaytooter/table/MutedWord.java new file mode 100644 index 00000000..e637486c --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/table/MutedWord.java @@ -0,0 +1,114 @@ +package jp.juggler.subwaytooter.table; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import java.util.HashSet; + +import jp.juggler.subwaytooter.App1; +import jp.juggler.subwaytooter.util.LogCategory; + +public class MutedWord { + + private static final LogCategory log = new LogCategory( "MutedWord" ); + + private static final String table = "word_mute"; + public static final String COL_NAME = "name"; + private static final String COL_TIME_SAVE = "time_save"; + + 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" + + ")" + ); + 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 < 11 && newVersion >= 11){ + onDBCreate( db ); + } + } + + public static void save( String word ){ + if( word == null ) return; + try{ + long now = System.currentTimeMillis(); + + ContentValues cv = new ContentValues(); + cv.put( COL_NAME, word ); + cv.put( COL_TIME_SAVE, now ); + App1.getDB().replace( table, null, cv ); + + }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 static void delete( String name ){ + try{ + App1.getDB().delete( table, COL_NAME+"=?",new String[]{ name }); + }catch( Throwable ex ){ + log.e( ex, "delete failed." ); + } + } + + public static HashSet getNameSet(){ + HashSet dst = new HashSet<>(); + try{ + Cursor cursor = App1.getDB().query( table, null,null,null, null, null, null); + if( cursor != null ){ + try{ + int idx_name = cursor.getColumnIndex( COL_NAME ); + while(cursor.moveToNext()){ + String s = cursor.getString(idx_name); + dst.add( s); + } + }finally{ + cursor.close(); + } + } + }catch(Throwable ex){ + ex.printStackTrace( ); + } + return dst; + } + +// private static final String[] isMuted_projection = new String[]{COL_NAME}; +// private static final String isMuted_where = COL_NAME+"=?"; +// private static final ThreadLocal isMuted_where_arg = new ThreadLocal() { +// @Override protected String[] initialValue() { +// return new String[1]; +// } +// }; +// public static boolean isMuted( String app_name ){ +// if( app_name == null ) return false; +// try{ +// String[] where_arg = isMuted_where_arg.get(); +// where_arg[0] = app_name; +// Cursor cursor = App1.getDB().query( table, isMuted_projection,isMuted_where , where_arg, null, null, null ); +// try{ +// if( cursor.moveToFirst() ){ +// return true; +// } +// }finally{ +// cursor.close(); +// } +// }catch( Throwable ex ){ +// log.e( ex, "load failed." ); +// } +// return false; +// } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/Emojione.java b/app/src/main/java/jp/juggler/subwaytooter/util/Emojione.java index 38ddffae..b7ed4cd4 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/Emojione.java +++ b/app/src/main/java/jp/juggler/subwaytooter/util/Emojione.java @@ -74,8 +74,6 @@ public abstract class Emojione public static CharSequence decodeEmoji( String s ){ - - DecodeEnv decode_env = new DecodeEnv(); Matcher matcher = SHORTNAME_PATTERN.matcher(s); int last_end = 0; @@ -101,6 +99,7 @@ public abstract class Emojione } // close span decode_env.closeSpan(); + return decode_env.sb; } } 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 98396661..2f34a66f 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/Utils.java +++ b/app/src/main/java/jp/juggler/subwaytooter/util/Utils.java @@ -16,6 +16,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Locale; @@ -1040,4 +1041,51 @@ public class Utils { return null; } + + private static final char ALM = (char)0x061c; // Arabic letter mark (ALM) + private static final char LRM = (char)0x200E; // Left-to-right mark (LRM) + private static final char RLM = (char)0x200F; // Right-to-left mark (RLM) + private static final char LRE = (char)0x202A; // Left-to-right embedding (LRE) + private static final char RLE = (char)0x202B; // Right-to-left embedding (RLE) + private static final char PDF = (char)0x202C; // Pop directional formatting (PDF) + private static final char LRO = (char)0x202D; // Left-to-right override (LRO) + private static final char RLO = (char)0x202E; // Right-to-left override (RLO) + + private static final String CHARS_MUST_PDF = String.valueOf( LRE ) + RLE + LRO + RLO ; + + private static final char LRI = (char)0x2066; // Left-to-right isolate (LRI) + private static final char RLI = (char)0x2067; // Right-to-left isolate (RLI) + private static final char FSI = (char)0x2068; // First strong isolate (FSI) + private static final char PDI = (char)0x2069; // Pop directional isolate (PDI) + + private static final String CHARS_MUST_PDI = String.valueOf( LRI ) + RLI + FSI ; + + public static String sanitizeBDI(String src){ + + LinkedList< Character > stack = null; + + for( int i = 0, ie = src.length() ; i < ie ; ++ i ){ + char c = src.charAt( i ); + + if( - 1 != CHARS_MUST_PDF.indexOf( c ) ){ + if( stack == null ) stack = new LinkedList<>(); + stack.add( PDF ); + + }else if( - 1 != CHARS_MUST_PDI.indexOf( c ) ){ + if( stack == null ) stack = new LinkedList<>(); + stack.add( PDI ); + }else if( stack != null && ! stack.isEmpty() && stack.getLast() == c ){ + stack.removeLast(); + } + } + + if( stack == null || stack.isEmpty() ) return src; + + StringBuilder sb = new StringBuilder(); + sb.append( src ); + while( ! stack.isEmpty() ){ + sb.append( (char) stack.removeLast() ); + } + return sb.toString(); + } } diff --git a/app/src/main/res/layout/act_text.xml b/app/src/main/res/layout/act_text.xml new file mode 100644 index 00000000..1ad6da62 --- /dev/null +++ b/app/src/main/res/layout/act_text.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + +