From ea8a38c86550d1cc1c8ec22c7bfffe3d8dcc683a Mon Sep 17 00:00:00 2001 From: tateisu Date: Sun, 14 May 2017 21:28:11 +0900 Subject: [PATCH] =?UTF-8?q?=E7=B4=B0=E3=81=8B=E3=81=84=E6=A9=9F=E8=83=BD?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=81=A8=E3=83=90=E3=82=B0=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/jp/juggler/subwaytooter/ActMain.java | 269 +++++++++++++----- .../jp/juggler/subwaytooter/AlarmService.java | 76 +++-- .../java/jp/juggler/subwaytooter/Column.java | 108 ++++--- .../subwaytooter/ColumnViewHolder.java | 11 +- .../juggler/subwaytooter/DlgContextMenu.java | 44 +-- .../juggler/subwaytooter/StatusButtons.java | 9 +- .../subwaytooter/api/TootApiResult.java | 2 +- .../subwaytooter/api/entity/TootStatus.java | 38 ++- .../jp/juggler/subwaytooter/util/Utils.java | 4 + app/src/main/res/layout/dlg_context_menu.xml | 15 +- app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values-ja/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 13 files changed, 381 insertions(+), 201 deletions(-) diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMain.java b/app/src/main/java/jp/juggler/subwaytooter/ActMain.java index 2fa13b8a..55c3fb49 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMain.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMain.java @@ -11,11 +11,11 @@ import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; 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.v4.view.ViewParentCompat; import android.support.v7.app.AlertDialog; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -131,10 +131,10 @@ public class ActMain extends AppCompatActivity @Override protected void onSaveInstanceState( Bundle outState ){ super.onSaveInstanceState( outState ); - if(pager_adapter != null){ + if( pager_adapter != null ){ outState.putInt( STATE_CURRENT_PAGE, pager.getCurrentItem() ); }else{ - int ve =tablet_layout_manager.findLastVisibleItemPosition(); + int ve = tablet_layout_manager.findLastVisibleItemPosition(); if( ve != RecyclerView.NO_POSITION ){ outState.putInt( STATE_CURRENT_PAGE, ve ); } @@ -143,12 +143,12 @@ public class ActMain extends AppCompatActivity @Override protected void onRestoreInstanceState( Bundle savedInstanceState ){ super.onRestoreInstanceState( savedInstanceState ); - int pos = savedInstanceState.getInt( STATE_CURRENT_PAGE); + int pos = savedInstanceState.getInt( STATE_CURRENT_PAGE ); if( pos > 0 && pos < app_state.column_list.size() ){ - if(pager_adapter != null){ - pager.setCurrentItem(pos); + if( pager_adapter != null ){ + pager.setCurrentItem( pos ); }else{ - tablet_layout_manager.smoothScrollToPosition( tablet_pager,null,pos ); + tablet_layout_manager.smoothScrollToPosition( tablet_pager, null, pos ); } } } @@ -440,7 +440,7 @@ public class ActMain extends AppCompatActivity if( vs == ve && vs != RecyclerView.NO_POSITION ){ column = app_state.column_list.get( vs ); }else{ - Utils.showToast( this,false,getString(R.string.cant_close_column_by_back_button_when_multiple_column_shown) ); + Utils.showToast( this, false, getString( R.string.cant_close_column_by_back_button_when_multiple_column_shown ) ); } } if( column != null ){ @@ -626,43 +626,41 @@ public class ActMain extends AppCompatActivity float density = dm.density; int media_thumb_height = 64; - sv = pref.getString(Pref.KEY_MEDIA_THUMB_HEIGHT,""); - if( !TextUtils.isEmpty( sv )){ + sv = pref.getString( Pref.KEY_MEDIA_THUMB_HEIGHT, "" ); + if( ! TextUtils.isEmpty( sv ) ){ try{ int iv = Integer.parseInt( sv ); if( iv >= 32 ){ media_thumb_height = iv; } - }catch(Throwable ex){ - ex.printStackTrace( ); + }catch( Throwable ex ){ + ex.printStackTrace(); } } - app_state.media_thumb_height = (int)(0.5f + media_thumb_height *density); + app_state.media_thumb_height = (int) ( 0.5f + media_thumb_height * density ); int column_w_min_dp = COLUMN_WIDTH_MIN_DP; - sv = pref.getString(Pref.KEY_COLUMN_WIDTH,""); - if( !TextUtils.isEmpty( sv )){ + sv = pref.getString( Pref.KEY_COLUMN_WIDTH, "" ); + if( ! TextUtils.isEmpty( sv ) ){ try{ int iv = Integer.parseInt( sv ); if( iv >= 100 ){ column_w_min_dp = iv; } - }catch(Throwable ex){ - ex.printStackTrace( ); + }catch( Throwable ex ){ + ex.printStackTrace(); } } int column_w_min = (int) ( 0.5f + column_w_min_dp * density ); - + int sw = dm.widthPixels; - - pager = (ViewPager) findViewById( R.id.viewPager ); tablet_pager = (RecyclerView) findViewById( R.id.rvPager ); - if( pref.getBoolean(Pref.KEY_DISABLE_TABLET_MODE,false) || sw < column_w_min * 2 ){ + if( pref.getBoolean( Pref.KEY_DISABLE_TABLET_MODE, false ) || sw < column_w_min * 2 ){ tablet_pager.setVisibility( View.GONE ); - + // SmartPhone mode pager_adapter = new ColumnPagerAdapter( this ); pager.setAdapter( pager_adapter ); @@ -964,31 +962,39 @@ public class ActMain extends AppCompatActivity final String host = m.group( 1 ); final String user = Uri.decode( m.group( 2 ) ); - ArrayList< SavedAccount > account_list_same_host = new ArrayList<>(); - for( SavedAccount a : SavedAccount.loadAccountList( log ) ){ + // 1: 検索APIは疑似アカウントでは開けない => startFindAccountが使えない + // 2: かりに検索できたとしてもユーザページは疑似アカウントでは開けない + + ArrayList< SavedAccount > account_list = SavedAccount.loadAccountList( log ); + ArrayList< SavedAccount > account_list_filtered = new ArrayList<>(); + for( SavedAccount a : account_list ){ + if( a.isPseudo() ) continue; if( host.equalsIgnoreCase( a.host ) ){ - account_list_same_host.add( a ); + account_list_filtered.add( a ); } } - // ソートする - Collections.sort( account_list_same_host, new Comparator< SavedAccount >() { - @Override public int compare( SavedAccount a, SavedAccount b ){ - return String.CASE_INSENSITIVE_ORDER.compare( AcctColor.getNickname( a.acct ), AcctColor.getNickname( b.acct ) ); + if( account_list_filtered.isEmpty() ){ + + for( SavedAccount a : account_list ){ + if( a.isPseudo() ) continue; + account_list_filtered.add( a ); + } + + if( account_list_filtered.isEmpty() ){ + // 認証されたアカウントが全くないので、ブラウザで開くしかない + openChromeTab( getDefaultInsertPosition(), null, uri.toString(), true ); + return; } - } ); - - if( account_list_same_host.isEmpty() ){ - account_list_same_host.add( addPseudoAccount( host ) ); } - AccountPicker.pick( this, true, true + AccountPicker.pick( this, false, true , getString( R.string.account_picker_open_user_who, user + "@" + host ) - , account_list_same_host + , account_list_filtered , new AccountPicker.AccountPickerCallback() { @Override public void onAccountPicked( @NonNull final SavedAccount ai ){ - startGetAccount( ai, host, user, new GetAccountCallback() { - @Override public void onGetAccount( TootAccount who ){ + startFindAccount( ai, host, user, new FindAccountCallback() { + @Override public void onFindAccount( TootAccount who ){ if( who != null ){ performOpenUser( getDefaultInsertPosition(), ai, who ); return; @@ -1015,8 +1021,7 @@ public class ActMain extends AppCompatActivity long db_id = Long.parseLong( sv, 10 ); SavedAccount account = SavedAccount.loadAccount( log, db_id ); if( account != null ){ - Column column = addColumn( getDefaultInsertPosition(), account, Column.TYPE_NOTIFICATIONS ); - + addColumn( getDefaultInsertPosition(), account, Column.TYPE_NOTIFICATIONS ); } }catch( Throwable ex ){ ex.printStackTrace(); @@ -1227,8 +1232,8 @@ public class ActMain extends AppCompatActivity }else if( page_delete > 0 && page_showing == page_delete ){ int idx = page_delete - 1; scrollToColumn( idx ); - Column c = app_state.column_list.get(idx); - if( ! c.bInitialLoading ){ + Column c = app_state.column_list.get( idx ); + if( ! c.bFirstInitialized ){ c.startLoading(); } } @@ -1241,8 +1246,8 @@ public class ActMain extends AppCompatActivity }else if( page_delete > 0 ){ int idx = page_delete - 1; scrollToColumn( idx ); - Column c = app_state.column_list.get(idx); - if( ! c.bInitialLoading ){ + Column c = app_state.column_list.get( idx ); + if( ! c.bFirstInitialized ){ c.startLoading(); } } @@ -1268,7 +1273,7 @@ public class ActMain extends AppCompatActivity Column col = new Column( app_state, ai, this, type, params ); index = addColumn( col, index ); scrollToColumn( index ); - if( ! col.bInitialLoading ){ + if( ! col.bFirstInitialized ){ col.startLoading(); } return col; @@ -1321,13 +1326,13 @@ public class ActMain extends AppCompatActivity ////////////////////////////////////////////////////////////// - interface GetAccountCallback { + interface FindAccountCallback { // return account information // if failed, account is null. - void onGetAccount( TootAccount account ); + void onFindAccount( TootAccount account ); } - void startGetAccount( final SavedAccount access_info, final String host, final String user, final GetAccountCallback callback ){ + void startFindAccount( final SavedAccount access_info, final String host, final String user, final FindAccountCallback callback ){ final ProgressDialog progress = new ProgressDialog( this ); final AsyncTask< Void, Void, TootAccount > task = new AsyncTask< Void, Void, TootAccount >() { @@ -1380,7 +1385,7 @@ public class ActMain extends AppCompatActivity @Override protected void onPostExecute( TootAccount result ){ progress.dismiss(); - callback.onGetAccount( result ); + callback.onFindAccount( result ); } }; @@ -1396,15 +1401,15 @@ public class ActMain extends AppCompatActivity task.executeOnExecutor( App1.task_executor ); } - static final Pattern reHashTag = Pattern.compile( "\\Ahttps://([^/]+)/tags/([^?#]+)\\z" ); - static final Pattern reUserPage = Pattern.compile( "\\Ahttps://([^/]+)/@([^?#/]+)\\z" ); - static final Pattern reStatusPage = Pattern.compile( "\\Ahttps://([^/]+)/@([^?#/]+)/(\\d+)\\z" ); + static final Pattern reHashTag = Pattern.compile( "\\Ahttps://([^/]+)/tags/([^?#]+)(?:\\z|\\?)" ); + static final Pattern reUserPage = Pattern.compile( "\\Ahttps://([^/]+)/@([^?#/]+)(?:\\z|\\?)" ); + static final Pattern reStatusPage = Pattern.compile( "\\Ahttps://([^/]+)/@([^?#/]+)/(\\d+)(?:\\z|\\?)" ); - public void openChromeTab( final int pos, final SavedAccount access_info, final String url, boolean noIntercept ){ + public void openChromeTab( final int pos, @Nullable final SavedAccount access_info, final String url, boolean noIntercept ){ try{ log.d( "openChromeTab url=%s", url ); - if( ! noIntercept ){ + if( ! noIntercept && access_info != null ){ // ハッシュタグをアプリ内で開く Matcher m = reHashTag.matcher( url ); if( m.find() ){ @@ -1446,18 +1451,64 @@ public class ActMain extends AppCompatActivity // https://mastodon.juggler.jp/@SubwayTooter final String host = m.group( 1 ); final String user = Uri.decode( m.group( 2 ) ); - startGetAccount( access_info, host, user, new GetAccountCallback() { - @Override public void onGetAccount( TootAccount who ){ - if( who != null ){ - performOpenUser( pos, access_info, who ); - return; + + if( ! access_info.isPseudo() ){ + startFindAccount( access_info, host, user, new FindAccountCallback() { + @Override public void onFindAccount( TootAccount who ){ + if( who != null ){ + performOpenUser( pos, access_info, who ); + return; + } + openChromeTab( pos, access_info, url, true ); } - openChromeTab( pos, access_info, url, true ); + } ); + return; + } + // 1: 検索APIは疑似アカウントでは開けない => startFindAccountが使えない + // 2: かりに検索できたとしてもユーザページは疑似アカウントでは開けない + + ArrayList< SavedAccount > account_list = SavedAccount.loadAccountList( log ); + ArrayList< SavedAccount > account_list_filtered = new ArrayList<>(); + for( SavedAccount a : account_list ){ + if( a.isPseudo() ) continue; + if( host.equalsIgnoreCase( a.host ) ){ + account_list_filtered.add( a ); } - } ); + } + + if( account_list_filtered.isEmpty() ){ + + for( SavedAccount a : account_list ){ + if( a.isPseudo() ) continue; + account_list_filtered.add( a ); + } + + if( account_list_filtered.isEmpty() ){ + // 認証されたアカウントが全くないので、ブラウザで開くしかない + openChromeTab( getDefaultInsertPosition(), null, url, true ); + return; + } + } + + AccountPicker.pick( this, false, true + , getString( R.string.account_picker_open_user_who, user + "@" + host ) + , account_list_filtered + , new AccountPicker.AccountPickerCallback() { + @Override public void onAccountPicked( @NonNull final SavedAccount ai ){ + startFindAccount( ai, host, user, new FindAccountCallback() { + @Override public void onFindAccount( TootAccount who ){ + if( who != null ){ + performOpenUser( getDefaultInsertPosition(), ai, who ); + return; + } + openChromeTab( getDefaultInsertPosition(), ai, url, true ); + } + } ); + } + } ); return; + } - } try{ @@ -1641,10 +1692,6 @@ public class ActMain extends AppCompatActivity } ); } - public void performReply( SavedAccount account, TootStatus status ){ - ActPost.open( this, REQUEST_CODE_POST, account.db_id, status ); - } - public void performMention( SavedAccount account, TootAccount who ){ ActPost.open( this, REQUEST_CODE_POST, account.db_id, "@" + account.getFullAcct( who ) + " " ); } @@ -1969,6 +2016,66 @@ public class ActMain extends AppCompatActivity showColumnMatchAccount( access_info ); } + public void performReply( + final SavedAccount access_info + , final TootStatus arg_status + , final boolean bRemote + ){ + if( ! bRemote ){ + ActPost.open( this, REQUEST_CODE_POST, access_info.db_id, arg_status ); + return; + } + + new AsyncTask< Void, Void, TootApiResult >() { + TootStatus target_status; + + @Override protected TootApiResult doInBackground( Void... params ){ + TootApiClient client = new TootApiClient( ActMain.this, new TootApiClient.Callback() { + @Override public boolean isApiCancelled(){ + return isCancelled(); + } + + @Override public void publishApiProgress( final String s ){ + } + } ); + client.setAccount( access_info ); + + // 検索APIに他タンスのステータスのURLを投げると、自タンスのステータスを得られる + String path = String.format( Locale.JAPAN, Column.PATH_SEARCH, Uri.encode( arg_status.url ) ); + path = path + "&resolve=1"; + + TootApiResult result = client.request( path ); + if( result != null && result.object != null ){ + TootResults tmp = TootResults.parse( log, access_info, result.object ); + if( tmp != null && tmp.statuses != null && ! tmp.statuses.isEmpty() ){ + target_status = tmp.statuses.get( 0 ); + log.d( "status id conversion %s => %s", arg_status.id, target_status.id ); + } + if( target_status == null ){ + return new TootApiResult( getString( R.string.status_id_conversion_failed ) ); + } + } + return result; + } + + @Override + protected void onCancelled( TootApiResult result ){ + super.onPostExecute( result ); + } + + @Override + protected void onPostExecute( TootApiResult result ){ + if( result == null ){ + // cancelled. + }else if( target_status != null ){ + ActPost.open( ActMain.this, REQUEST_CODE_POST, access_info.db_id, target_status ); + }else{ + Utils.showToast( ActMain.this, true, result.error ); + } + } + }.executeOnExecutor( App1.task_executor ); + } + //////////////////////////////////////// private void performAccountSetting(){ @@ -2838,6 +2945,21 @@ public class ActMain extends AppCompatActivity } ); } + void openReplyFromAnotherAccount( @NonNull final SavedAccount access_info, final TootStatus status ){ + if( status == null ) return; + AccountPicker.pick( this, false, false + , getString( R.string.account_picker_reply ) + , makeAccountListNonPseudo( log ), new AccountPicker.AccountPickerCallback() { + @Override public void onAccountPicked( @NonNull SavedAccount ai ){ + performReply( + ai + , status + , ! ai.host.equalsIgnoreCase( access_info.host ) + ); + } + } ); + } + void openFollowFromAnotherAccount( @NonNull SavedAccount access_info, TootStatus status ){ if( status == null ) return; openFollowFromAnotherAccount( access_info, status.account ); @@ -2855,6 +2977,9 @@ public class ActMain extends AppCompatActivity } ); } + ///////////////////////////////////////////////////////////////////////// + // タブレット対応で必要になった関数など + private boolean closeColumnSetting(){ if( pager_adapter != null ){ ColumnViewHolder vh = pager_adapter.getColumnViewHolder( pager.getCurrentItem() ); @@ -2966,22 +3091,22 @@ public class ActMain extends AppCompatActivity private void resizeColumnWidth(){ int column_w_min_dp = COLUMN_WIDTH_MIN_DP; - String sv = pref.getString(Pref.KEY_COLUMN_WIDTH,""); - if( !TextUtils.isEmpty( sv )){ + String sv = pref.getString( Pref.KEY_COLUMN_WIDTH, "" ); + if( ! TextUtils.isEmpty( sv ) ){ try{ int iv = Integer.parseInt( sv ); if( iv >= 100 ){ column_w_min_dp = iv; } - }catch(Throwable ex){ - ex.printStackTrace( ); + }catch( Throwable ex ){ + ex.printStackTrace(); } } DisplayMetrics dm = getResources().getDisplayMetrics(); - + final int sw = dm.widthPixels; - + float density = dm.density; int column_w_min = (int) ( 0.5f + column_w_min_dp * density ); @@ -2989,7 +3114,7 @@ public class ActMain extends AppCompatActivity tablet_pager_adapter.setColumnWidth( sw ); }else{ nScreenColumn = sw / column_w_min; - + // 2つは表示できるが3つは表示できないかもしれない int column_w_max = (int) ( 0.5f + column_w_min * 1.5f ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/AlarmService.java b/app/src/main/java/jp/juggler/subwaytooter/AlarmService.java index 012c19bc..cea603f8 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/AlarmService.java +++ b/app/src/main/java/jp/juggler/subwaytooter/AlarmService.java @@ -23,11 +23,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; +import java.util.LinkedList; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; import jp.juggler.subwaytooter.api.TootApiClient; import jp.juggler.subwaytooter.api.TootApiResult; -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; @@ -146,44 +147,57 @@ public class AlarmService extends IntentService { } } - TootApiClient client = new TootApiClient( this, new TootApiClient.Callback() { - @Override public boolean isApiCancelled(){ - return false; - } - - @Override public void publishApiProgress( String s ){ - - } - } ); - boolean bAlarmRequired = false; - HashSet< String > muted_app = MutedApp.getNameSet(); - HashSet< String > muted_word = MutedWord.getNameSet(); + final AtomicBoolean bAlarmRequired = new AtomicBoolean( false ); + final HashSet< String > muted_app = MutedApp.getNameSet(); + final HashSet< String > muted_word = MutedWord.getNameSet(); + + LinkedList thread_list = new LinkedList<>( ); + for( SavedAccount _a : account_list ){ + final SavedAccount account = _a; + Thread t = new Thread( new Runnable() { + @Override public void run(){ - for( SavedAccount account : account_list ){ - try{ - if( account.notification_mention - || account.notification_boost - || account.notification_favourite - || account.notification_follow - ){ - bAlarmRequired = true; - - ArrayList< Data > data_list = new ArrayList<>(); - - checkAccount( client, data_list, account, muted_app ,muted_word); - - showNotification( account.db_id, data_list ); - + try{ + if( account.notification_mention + || account.notification_boost + || account.notification_favourite + || account.notification_follow + ){ + bAlarmRequired.set(true); + + TootApiClient client = new TootApiClient( AlarmService.this, new TootApiClient.Callback() { + @Override public boolean isApiCancelled(){ + return false; + } + @Override public void publishApiProgress( String s ){ + } + } ); + + ArrayList< Data > data_list = new ArrayList<>(); + checkAccount( client, data_list, account, muted_app ,muted_word); + showNotification( account.db_id, data_list ); + } + }catch( Throwable ex ){ + ex.printStackTrace(); + } } - }catch( Throwable ex ){ - ex.printStackTrace(); + } ); + thread_list.add( t); + t.start(); + } + + for( Thread t : thread_list ){ + try{ + t.join(); + }catch(Throwable ex){ + ex.printStackTrace( ); } } alarm_manager.cancel( pi_next ); - if( bAlarmRequired ){ + if( bAlarmRequired.get() ){ long now = SystemClock.elapsedRealtime(); alarm_manager.setWindow( AlarmManager.ELAPSED_REALTIME_WAKEUP diff --git a/app/src/main/java/jp/juggler/subwaytooter/Column.java b/app/src/main/java/jp/juggler/subwaytooter/Column.java index 0ff1446f..1d0ab7e4 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/Column.java +++ b/app/src/main/java/jp/juggler/subwaytooter/Column.java @@ -48,7 +48,6 @@ import jp.juggler.subwaytooter.util.Utils; class Column implements StreamReader.Callback { private static final LogCategory log = new LogCategory( "Column" ); - interface Callback { boolean isActivityResume(); } @@ -236,7 +235,7 @@ class Column implements StreamReader.Callback { item.put( KEY_DONT_STREAMING, dont_streaming ); item.put( KEY_DONT_AUTO_REFRESH, dont_auto_refresh ); item.put( KEY_HIDE_MEDIA_DEFAULT, hide_media_default ); - + item.put( KEY_REGEX_TEXT, regex_text ); item.put( KEY_HEADER_BACKGROUND_COLOR, header_bg_color ); @@ -374,6 +373,13 @@ class Column implements StreamReader.Callback { void dispose(){ is_dispose.set( true ); stopStreaming(); + + if( _holder != null ){ + try{ + _holder.getListView().setAdapter( null ); + }catch( Throwable ignored ){ + } + } } String getColumnName( boolean bLong ){ @@ -698,41 +704,43 @@ class Column implements StreamReader.Callback { AlarmService.dataRemoved( context, access_info.db_id ); } - private ColumnViewHolder holder; + ////////////////////////////////////////////////////////////////////////////////////// - void setColumnViewHolder( ColumnViewHolder holder ){ - this.holder = holder; + private ColumnViewHolder _holder; + + void setColumnViewHolder( ColumnViewHolder new_holder ){ + this._holder = new_holder; } - private final Runnable proc_showContent = new Runnable() { - @Override public void run(){ - if( holder != null ) holder.showContent(); - } - }; - private final Runnable proc_showColumnHeader = new Runnable() { - @Override public void run(){ - if( holder != null ) holder.showColumnHeader(); - } - }; - private final Runnable proc_showColumnColor = new Runnable() { - @Override public void run(){ - if( holder != null ) holder.showColumnColor(); - } - }; + private ColumnViewHolder getViewHolder(){ + return is_dispose.get() ? null : _holder; + } void fireShowContent(){ - Utils.runOnMainThread( proc_showContent ); + if( ! Utils.isMainThread() ){ + throw new RuntimeException( "fireShowColumnHeader: not on main thread." ); + } + ColumnViewHolder holder = getViewHolder(); + if( holder != null ) holder.showContent(); } - // カラムヘッダ部分だけ更新する void fireShowColumnHeader(){ - Utils.runOnMainThread( proc_showColumnHeader ); + if( ! Utils.isMainThread() ){ + throw new RuntimeException( "fireShowColumnHeader: not on main thread." ); + } + ColumnViewHolder holder = getViewHolder(); + if( holder != null ) holder.showColumnHeader(); } void fireColumnColor(){ - Utils.runOnMainThread( proc_showColumnColor ); + if( ! Utils.isMainThread() ){ + throw new RuntimeException( "fireShowColumnHeader: not on main thread." ); + } + ColumnViewHolder holder = getViewHolder(); + if( holder != null ) holder.showColumnColor(); } + ////////////////////////////////////////////////////////////////////////////////////// private AsyncTask< Void, Void, TootApiResult > last_task; @@ -1166,6 +1174,7 @@ class Column implements StreamReader.Callback { @Override protected void onPostExecute( TootApiResult result ){ + if( is_dispose.get() ) return; if( isCancelled() || result == null ){ return; @@ -1186,11 +1195,12 @@ class Column implements StreamReader.Callback { resumeStreaming( false ); } fireShowContent(); - + // 初期ロードの直後は先頭に移動する try{ - holder.getListView().setSelection( 0 ); - }catch(Throwable ignored){ + ColumnViewHolder holder = getViewHolder(); + if( holder != null ) holder.getListView().setSelection( 0 ); + }catch( Throwable ignored ){ } } }; @@ -1362,14 +1372,15 @@ class Column implements StreamReader.Callback { case TYPE_FEDERATE: startRefresh( true, false, status_id, refresh_after_toot ); break; + case TYPE_PROFILE: if( profile_tab == TAB_STATUS && profile_id == access_info.id ){ startRefresh( true, false, status_id, refresh_after_toot ); } break; + case TYPE_CONVERSATION: startLoading(); - break; } } @@ -1381,22 +1392,26 @@ class Column implements StreamReader.Callback { if( last_task != null ){ if( ! bSilent ){ Utils.showToast( context, true, R.string.column_is_busy ); + ColumnViewHolder holder = getViewHolder(); if( holder != null ) holder.getRefreshLayout().setRefreshing( false ); } return; }else if( bBottom && max_id == null ){ if( ! bSilent ){ Utils.showToast( context, true, R.string.end_of_list ); + ColumnViewHolder holder = getViewHolder(); if( holder != null ) holder.getRefreshLayout().setRefreshing( false ); } return; }else if( ! bBottom && since_id == null ){ + ColumnViewHolder holder = getViewHolder(); if( holder != null ) holder.getRefreshLayout().setRefreshing( false ); startLoading(); return; } if( bSilent ){ + ColumnViewHolder holder = getViewHolder(); if( holder != null ){ holder.getRefreshLayout().setRefreshing( true ); } @@ -1820,6 +1835,8 @@ class Column implements StreamReader.Callback { @Override protected void onPostExecute( TootApiResult result ){ + if( is_dispose.get() ) return; + if( isCancelled() || result == null ){ return; } @@ -1846,6 +1863,7 @@ class Column implements StreamReader.Callback { // 事前にスクロール位置を覚えておく ScrollPosition sp = null; + ColumnViewHolder holder = getViewHolder(); if( holder != null ){ sp = holder.getScrollPosition(); } @@ -1911,6 +1929,7 @@ class Column implements StreamReader.Callback { return; } + ColumnViewHolder holder = getViewHolder(); if( holder != null ){ holder.getRefreshLayout().setRefreshing( true ); } @@ -2223,10 +2242,12 @@ class Column implements StreamReader.Callback { @Override protected void onPostExecute( TootApiResult result ){ + if( is_dispose.get() ) return; if( isCancelled() || result == null ){ return; } + last_task = null; bRefreshLoading = false; @@ -2243,16 +2264,18 @@ class Column implements StreamReader.Callback { ArrayList< Object > list_new = duplicate_map.filterDuplicate( list_tmp ); + ColumnViewHolder holder = getViewHolder(); + // idx番目の要素がListViewのtopから何ピクセル下にあるか int restore_idx = position + 1; int restore_y = 0; if( holder != null ){ try{ - restore_y = getItemTop( restore_idx ); + restore_y = getItemTop( holder, restore_idx ); }catch( IndexOutOfBoundsException ex ){ restore_idx = position; try{ - restore_y = getItemTop( restore_idx ); + restore_y = getItemTop( holder, restore_idx ); }catch( IndexOutOfBoundsException ex2 ){ restore_idx = - 1; } @@ -2267,7 +2290,7 @@ class Column implements StreamReader.Callback { if( holder != null ){ //noinspection StatementWithEmptyBody if( restore_idx >= 0 ){ - setItemTop( restore_idx + added - 1, restore_y ); + setItemTop( holder, restore_idx + added - 1, restore_y ); }else{ // ギャップが画面内にない場合、何もしない } @@ -2293,7 +2316,8 @@ class Column implements StreamReader.Callback { } // 特定の要素が特定の位置に来るようにスクロール位置を調整する - private void setItemTop( int idx, int y ){ + private void setItemTop( @NonNull ColumnViewHolder holder, int idx, int y ){ + MyListView listView = holder.getListView(); boolean hasHeader = holder.hasHeaderView(); if( hasHeader ){ @@ -2309,7 +2333,8 @@ class Column implements StreamReader.Callback { listView.setSelectionFromTop( idx, y ); } - private int getItemTop( int idx ){ + private int getItemTop( @NonNull ColumnViewHolder holder, int idx ){ + MyListView listView = holder.getListView(); boolean hasHeader = holder.hasHeaderView(); @@ -2382,6 +2407,7 @@ class Column implements StreamReader.Callback { log.e( ex, "getId() failed. o=", list_new.get( 0 ) ); } } + ColumnViewHolder holder = getViewHolder(); // 事前にスクロール位置を覚えておく ScrollPosition sp = null; @@ -2396,7 +2422,7 @@ class Column implements StreamReader.Callback { if( list_data.size() > 0 ){ try{ restore_idx = holder.getListView().getFirstVisiblePosition(); - restore_y = getItemTop( restore_idx ); + restore_y = getItemTop( holder, restore_idx ); }catch( IndexOutOfBoundsException ex ){ restore_idx = - 1; restore_y = 0; @@ -2426,11 +2452,11 @@ class Column implements StreamReader.Callback { if( holder != null ){ //noinspection StatementWithEmptyBody - if( sp == null || ( sp.pos == 0 && sp.top == 0 ) ){ + if( sp.pos == 0 && sp.top == 0 ){ // スクロール位置が先頭なら先頭のまま }else if( restore_idx >= 0 ){ // - setItemTop( restore_idx + added, restore_y ); + setItemTop( holder, restore_idx + added, restore_y ); }else{ // ギャップが画面内にない場合、何もしない } @@ -2446,6 +2472,7 @@ class Column implements StreamReader.Callback { }; @Override public void onStreamingMessage( String event_type, Object o ){ + if( is_dispose.get() ) return; if( o instanceof Long ){ removeStatus( access_info, (Long) o ); @@ -2510,7 +2537,7 @@ class Column implements StreamReader.Callback { log.d( "onResume: column was disposed." ); return; } - + // 未初期化なら何もしない if( ! bFirstInitialized ){ log.d( "onResume: column is not initialized." ); @@ -2529,9 +2556,9 @@ class Column implements StreamReader.Callback { log.d( "onResume: bRefreshingTop is true." ); }else if( ! bRefreshLoading - && canAutoRefresh() - && ! App1.getAppState( context ).pref.getBoolean( Pref.KEY_DONT_REFRESH_ON_RESUME, false ) - && ! dont_auto_refresh + && canAutoRefresh() + && ! App1.getAppState( context ).pref.getBoolean( Pref.KEY_DONT_REFRESH_ON_RESUME, false ) + && ! dont_auto_refresh ){ // リフレッシュしてからストリーミング開始 @@ -2589,7 +2616,6 @@ class Column implements StreamReader.Callback { private boolean bPutGap; - private void resumeStreaming( boolean bPutGap ){ if( ! canStreaming() ){ diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java index fa360da3..34660b84 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java @@ -57,9 +57,11 @@ class ColumnViewHolder is_destroyed.set( true ); saveScrollPosition(); - + + listView.setAdapter( null ); + column.setColumnViewHolder( null ); - + closeBitmaps(); activity.closeListItemPopup(); @@ -118,13 +120,14 @@ class ColumnViewHolder tvLoading = (TextView) root.findViewById( R.id.tvLoading ); listView = (MyListView) root.findViewById( R.id.listView ); listView.setAdapter( null ); + if( column.column_type == Column.TYPE_PROFILE ){ vh_header = new HeaderViewHolder( activity, column, listView ); status_adapter.header = vh_header; }else{ status_adapter.header = null; } - listView.setAdapter( status_adapter ); + this.swipyRefreshLayout = (SwipyRefreshLayout) root.findViewById( R.id.swipyRefreshLayout ); swipyRefreshLayout.setOnRefreshListener( this ); @@ -291,7 +294,7 @@ class ColumnViewHolder } // - + listView.setAdapter( status_adapter ); column.setColumnViewHolder( this ); showColumnColor(); diff --git a/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.java b/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.java index 167614c7..bda7bd6b 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.java +++ b/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.java @@ -66,6 +66,7 @@ class DlgContextMenu implements View.OnClickListener, View.OnLongClickListener { View btnText = viewRoot.findViewById( R.id.btnText ); View btnFavouriteAnotherAccount = viewRoot.findViewById( R.id.btnFavouriteAnotherAccount ); View btnBoostAnotherAccount = viewRoot.findViewById( R.id.btnBoostAnotherAccount ); + View btnReplyAnotherAccount = viewRoot.findViewById( R.id.btnReplyAnotherAccount ); View btnDelete = viewRoot.findViewById( R.id.btnDelete ); View btnReport = viewRoot.findViewById( R.id.btnReport ); Button btnMuteApp = (Button) viewRoot.findViewById( R.id.btnMuteApp ); @@ -105,9 +106,11 @@ class DlgContextMenu implements View.OnClickListener, View.OnLongClickListener { if( account_list_non_pseudo_same_instance.isEmpty() ){ btnFavouriteAnotherAccount.setVisibility( View.GONE ); btnBoostAnotherAccount.setVisibility( View.GONE ); + btnReplyAnotherAccount.setVisibility( View.GONE ); }else{ btnFavouriteAnotherAccount.setOnClickListener( this ); btnBoostAnotherAccount.setOnClickListener( this ); + btnReplyAnotherAccount.setOnClickListener( this ); } if( access_info.isPseudo() ){ btnDelete.setVisibility( View.GONE ); @@ -254,44 +257,15 @@ class DlgContextMenu implements View.OnClickListener, View.OnLongClickListener { break; case R.id.btnFavouriteAnotherAccount: - if( status != null ){ - AccountPicker.pick( activity, false, false - , activity.getString( R.string.account_picker_favourite ) - // , account_list_non_pseudo_same_instance - , account_list_non_pseudo - , new AccountPicker.AccountPickerCallback() { - @Override public void onAccountPicked( @NonNull SavedAccount ai ){ - activity.performFavourite( - ai - , ! ai.host.equalsIgnoreCase( access_info.host ) - , true - , status - , activity.favourite_complete_callback - ); - } - } ); - } + activity.openFavouriteFromAnotherAccount( access_info,status ); break; case R.id.btnBoostAnotherAccount: - if( status != null ){ - AccountPicker.pick( activity, false, false - , activity.getString( R.string.account_picker_boost ) - // , account_list_non_pseudo_same_instance - , account_list_non_pseudo - , new AccountPicker.AccountPickerCallback() { - @Override public void onAccountPicked( @NonNull SavedAccount ai ){ - activity.performBoost( - ai - , ! ai.host.equalsIgnoreCase( access_info.host ) - , true - , status - , false - , activity.boost_complete_callback - ); - } - } ); - } + activity.openBoostFromAnotherAccount( access_info,status ); + break; + + case R.id.btnReplyAnotherAccount: + activity.openReplyFromAnotherAccount( access_info,status ); break; case R.id.btnDelete: diff --git a/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.java b/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.java index 071142c2..28d0a8f5 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.java +++ b/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.java @@ -57,6 +57,7 @@ class StatusButtons implements View.OnClickListener, View.OnLongClickListener { // v = viewRoot.findViewById( R.id.btnReply ); v.setOnClickListener( this ); + v.setOnLongClickListener( this ); } @@ -117,9 +118,9 @@ class StatusButtons implements View.OnClickListener, View.OnLongClickListener { break; case R.id.btnReply: if( access_info.isPseudo() ){ - Utils.showToast( activity, false, R.string.not_available_for_pseudo_account ); + activity.openReplyFromAnotherAccount( access_info, status ); }else{ - activity.performReply( access_info, status ); + activity.performReply( access_info, status ,false); } break; case R.id.btnBoost: @@ -184,6 +185,10 @@ class StatusButtons implements View.OnClickListener, View.OnLongClickListener { activity.openFollowFromAnotherAccount( access_info, status ); break; + case R.id.btnReply: + activity.openReplyFromAnotherAccount( access_info, status ); + break; + } return true; } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResult.java b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResult.java index 22f1e9db..cff389fe 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResult.java +++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResult.java @@ -67,7 +67,7 @@ public class TootApiResult { while( m.find()){ String url = m.group(1); String rel = m.group(2); - log.d("Link %s,%s",rel,url); + // log.d("Link %s,%s",rel,url); if( "next".equals( rel )) link_older = url; if( "prev".equals( rel )) link_newer = url; } 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 94677694..c5f5f7bd 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 @@ -10,7 +10,9 @@ import org.json.JSONObject; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Calendar; import java.util.Date; +import java.util.GregorianCalendar; import java.util.HashSet; import java.util.Locale; import java.util.TimeZone; @@ -138,7 +140,7 @@ public class TootStatus extends TootId { status.media_attachments = TootAttachment.parseList( log, src.optJSONArray( "media_attachments" ) ); status.mentions = TootMention.parseList( log, src.optJSONArray( "mentions" ) ); status.tags = TootTag.parseList( log, src.optJSONArray( "tags" ) ); - status.application = TootApplication.parse( log,src.optJSONObject( "application" )); // null + status.application = TootApplication.parse( log, src.optJSONObject( "application" ) ); // null status.time_created_at = parseTime( log, status.created_at ); status.decoded_content = HTMLDecoder.decodeHTML( account, status.content ); @@ -172,22 +174,32 @@ public class TootStatus extends TootId { return result; } - private static final Pattern reTime = Pattern.compile("\\A(\\d+\\D+\\d+\\D+\\d+\\D+\\d+\\D+\\d+\\D+\\d+\\D+\\d+)"); - private static final SimpleDateFormat date_format_utc = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.getDefault() ); + private static final Pattern reTime = Pattern.compile( "\\A(\\d+)\\D+(\\d+)\\D+(\\d+)\\D+(\\d+)\\D+(\\d+)\\D+(\\d+)\\D+(\\d+)" ); + + private static final TimeZone tz_utc = TimeZone.getTimeZone( "UTC" ); static long parseTime( LogCategory log, String strTime ){ if( ! TextUtils.isEmpty( strTime ) ){ try{ Matcher m = reTime.matcher( strTime ); - if(!m.find() ){ - log.d("!!invalid time format: %s",strTime); + if( ! m.find() ){ + log.d( "!!invalid time format: %s", strTime ); }else{ - date_format_utc.setTimeZone( TimeZone.getTimeZone( "GMT" ) ); - return date_format_utc.parse( m.group( 1 ) ).getTime(); + GregorianCalendar g = new GregorianCalendar( tz_utc ); + g.set( + Utils.parse_int( m.group( 1 ), 1 ), + Utils.parse_int( m.group( 2 ), 1 ) - 1, + Utils.parse_int( m.group( 3 ), 1 ), + Utils.parse_int( m.group( 4 ), 0 ), + Utils.parse_int( m.group( 5 ), 0 ), + Utils.parse_int( m.group( 6 ), 0 ) + ); + g.set( Calendar.MILLISECOND, Utils.parse_int( m.group( 7 ), 0 ) ); + return g.getTimeInMillis(); } - }catch( Throwable ex ){// ParseException, ArrayIndexOutOfBoundsException + }catch( Throwable ex ){// ParseException, ArrayIndexOutOfBoundsException ex.printStackTrace(); - log.e( ex, "TootStatus.parseTime failed. src=%s",strTime ); + log.e( ex, "TootStatus.parseTime failed. src=%s", strTime ); } } return 0L; @@ -230,7 +242,6 @@ public class TootStatus extends TootId { } } - public boolean checkMuted( HashSet< String > muted_app, HashSet< String > muted_word ){ // app mute @@ -244,7 +255,7 @@ public class TootStatus extends TootId { } // word mute - for( String word: muted_word ){ + for( String word : muted_word ){ if( decoded_content != null && decoded_content.toString().contains( word ) ){ return true; } @@ -255,13 +266,12 @@ public class TootStatus extends TootId { // reblog //noinspection RedundantIfStatement - if( reblog != null && reblog.checkMuted( muted_app,muted_word ) ){ + if( reblog != null && reblog.checkMuted( muted_app, muted_word ) ){ return true; } return false; - + } - } 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 2f34a66f..39a5a752 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/Utils.java +++ b/app/src/main/java/jp/juggler/subwaytooter/util/Utils.java @@ -886,6 +886,10 @@ public class Utils { return resources.getString( string_id, args ) + String.format( " :%s %s", ex.getClass().getSimpleName(), ex.getMessage() ); } + public static boolean isMainThread( ){ + return Looper.getMainLooper().getThread() == Thread.currentThread(); + } + public static void runOnMainThread( @NonNull Runnable proc ){ if( Looper.getMainLooper().getThread() == Thread.currentThread() ){ proc.run(); diff --git a/app/src/main/res/layout/dlg_context_menu.xml b/app/src/main/res/layout/dlg_context_menu.xml index 1f8d94b8..dfec359b 100644 --- a/app/src/main/res/layout/dlg_context_menu.xml +++ b/app/src/main/res/layout/dlg_context_menu.xml @@ -133,7 +133,20 @@ android:text="@string/boost_from_another_account" android:textAllCaps="false" /> - +