細かい機能追加とバグ修正

This commit is contained in:
tateisu 2017-05-14 21:28:11 +09:00
parent 02bb8f5af0
commit ea8a38c865
13 changed files with 381 additions and 201 deletions

View File

@ -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;
// つは表示できるが3つは表示できないかもしれない
int column_w_max = (int) ( 0.5f + column_w_min * 1.5f );

View File

@ -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> 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

View File

@ -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() ){

View File

@ -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();

View File

@ -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:

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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();

View File

@ -133,7 +133,20 @@
android:text="@string/boost_from_another_account"
android:textAllCaps="false"
/>
<Button
android:id="@+id/btnReplyAnotherAccount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/btn_bg_transparent"
android:gravity="start|center_vertical"
android:minHeight="32dp"
android:paddingBottom="4dp"
android:paddingEnd="8dp"
android:paddingStart="8dp"
android:paddingTop="4dp"
android:text="@string/reply_from_another_account"
android:textAllCaps="false"
/>
<Button
android:id="@+id/btnDelete"
android:layout_width="match_parent"

View File

@ -313,5 +313,7 @@
<string name="disable_tablet_mode">Désactiver le mode tablette (redémarrage nécessaire)</string>
<string name="media_thumbnail_height">Hauteur des miniatures (défaut=64(dp), redémarrage nécessaire)</string>
<string name="minimum_column_width">Largeur minimum des colonnes (défaut=300(dp), redémarrage nécessaire)</string>
<string name="account_picker_reply">Which account do you reply from?</string>
<string name="reply_from_another_account">Reply from another account</string>
</resources>

View File

@ -599,4 +599,6 @@
<string name="disable_tablet_mode">タブレットモードを使わない(アプリ再起動が必要)</string>
<string name="media_thumbnail_height">添付メディアのサムネイルの高さ(デフォルト=64(dp)、アプリ再起動が必要)</string>
<string name="minimum_column_width">カラム最小幅(デフォルト=300(dp)、アプリ再起動が必要)</string>
<string name="account_picker_reply">どのアカウントから返信しますか?</string>
<string name="reply_from_another_account">別アカウントから返信</string>
</resources>

View File

@ -309,4 +309,6 @@
<string name="disable_tablet_mode">Disable tablet mode (app restart required)</string>
<string name="minimum_column_width">Minimum column width (default=300(dp),app restart required)</string>
<string name="media_thumbnail_height">Media Thumbnail height (default=64(dp), app restart required)</string>
<string name="account_picker_reply">Which account do you reply from?</string>
<string name="reply_from_another_account">Reply from another account</string>
</resources>