ストリーミングAPI対応。画面復帰時に自動リフレッシュ。
This commit is contained in:
parent
5343c6e44c
commit
c32415842c
|
@ -9,8 +9,8 @@ android {
|
|||
applicationId "jp.juggler.subwaytooter"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 25
|
||||
versionCode 43
|
||||
versionName "0.4.3"
|
||||
versionCode 44
|
||||
versionName "0.4.4"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
|
|
|
@ -50,6 +50,9 @@ public class ActAppSetting extends AppCompatActivity
|
|||
Switch swExitAppWhenCloseProtectedColumn;
|
||||
Switch swShowFollowButtonInButtonBar;
|
||||
Switch swDontRound;
|
||||
Switch swDontUseStreaming;
|
||||
Switch swDontRefreshOnResume;
|
||||
|
||||
|
||||
Spinner spBackButtonAction;
|
||||
Spinner spUITheme;
|
||||
|
@ -99,6 +102,12 @@ public class ActAppSetting extends AppCompatActivity
|
|||
swDontRound = (Switch) findViewById( R.id.swDontRound );
|
||||
swDontRound.setOnCheckedChangeListener( this );
|
||||
|
||||
swDontUseStreaming = (Switch) findViewById( R.id.swDontUseStreaming );
|
||||
swDontUseStreaming.setOnCheckedChangeListener( this );
|
||||
|
||||
swDontRefreshOnResume = (Switch) findViewById( R.id.swDontRefreshOnResume );
|
||||
swDontRefreshOnResume.setOnCheckedChangeListener( this );
|
||||
|
||||
cbNotificationSound = (CheckBox) findViewById( R.id.cbNotificationSound );
|
||||
cbNotificationVibration = (CheckBox) findViewById( R.id.cbNotificationVibration );
|
||||
cbNotificationLED = (CheckBox) findViewById( R.id.cbNotificationLED );
|
||||
|
@ -190,6 +199,8 @@ public class ActAppSetting extends AppCompatActivity
|
|||
swExitAppWhenCloseProtectedColumn.setChecked( pref.getBoolean( Pref.KEY_EXIT_APP_WHEN_CLOSE_PROTECTED_COLUMN, false ) );
|
||||
swShowFollowButtonInButtonBar.setChecked( pref.getBoolean( Pref.KEY_SHOW_FOLLOW_BUTTON_IN_BUTTON_BAR, false ) );
|
||||
swDontRound.setChecked( pref.getBoolean( Pref.KEY_DONT_ROUND, false ) );
|
||||
swDontUseStreaming.setChecked( pref.getBoolean( Pref.KEY_DONT_USE_STREAMING, false ) );
|
||||
swDontRefreshOnResume.setChecked( pref.getBoolean( Pref.KEY_DONT_REFRESH_ON_RESUME , false ) );
|
||||
|
||||
cbNotificationSound.setChecked( pref.getBoolean( Pref.KEY_NOTIFICATION_SOUND, true ) );
|
||||
cbNotificationVibration.setChecked( pref.getBoolean( Pref.KEY_NOTIFICATION_VIBRATION, true ) );
|
||||
|
@ -220,6 +231,8 @@ public class ActAppSetting extends AppCompatActivity
|
|||
.putBoolean( Pref.KEY_EXIT_APP_WHEN_CLOSE_PROTECTED_COLUMN, swExitAppWhenCloseProtectedColumn.isChecked() )
|
||||
.putBoolean( Pref.KEY_SHOW_FOLLOW_BUTTON_IN_BUTTON_BAR, swShowFollowButtonInButtonBar.isChecked() )
|
||||
.putBoolean( Pref.KEY_DONT_ROUND, swDontRound.isChecked() )
|
||||
.putBoolean( Pref.KEY_DONT_USE_STREAMING, swDontUseStreaming.isChecked() )
|
||||
.putBoolean( Pref.KEY_DONT_REFRESH_ON_RESUME, swDontRefreshOnResume.isChecked() )
|
||||
|
||||
.putBoolean( Pref.KEY_NOTIFICATION_SOUND, cbNotificationSound.isChecked() )
|
||||
.putBoolean( Pref.KEY_NOTIFICATION_VIBRATION, cbNotificationVibration.isChecked() )
|
||||
|
|
|
@ -31,8 +31,6 @@ import android.widget.ImageButton;
|
|||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import com.yasesprox.android.transcommusdk.TransCommuActivity;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
|
@ -65,7 +63,7 @@ import okhttp3.Request;
|
|||
import okhttp3.RequestBody;
|
||||
|
||||
public class ActMain extends AppCompatActivity
|
||||
implements NavigationView.OnNavigationItemSelectedListener, View.OnClickListener, ViewPager.OnPageChangeListener
|
||||
implements NavigationView.OnNavigationItemSelectedListener, View.OnClickListener, ViewPager.OnPageChangeListener, Column.Callback
|
||||
{
|
||||
public static final LogCategory log = new LogCategory( "ActMain" );
|
||||
|
||||
|
@ -115,7 +113,14 @@ public class ActMain extends AppCompatActivity
|
|||
super.onDestroy();
|
||||
}
|
||||
|
||||
|
||||
boolean bResume;
|
||||
@Override public boolean isActivityResume(){
|
||||
return bResume;
|
||||
}
|
||||
|
||||
@Override protected void onResume(){
|
||||
bResume = true;
|
||||
log.d( "onResume" );
|
||||
super.onResume();
|
||||
HTMLDecoder.link_callback = link_click_listener;
|
||||
|
@ -156,10 +161,6 @@ public class ActMain extends AppCompatActivity
|
|||
posted_acct = null;
|
||||
}
|
||||
|
||||
if( pager_adapter.getCount() == 0 ){
|
||||
llEmpty.setVisibility( View.VISIBLE );
|
||||
}
|
||||
|
||||
Uri uri = ActCallback.last_uri.getAndSet( null );
|
||||
if( uri != null ){
|
||||
handleIntentUri( uri );
|
||||
|
@ -169,6 +170,15 @@ public class ActMain extends AppCompatActivity
|
|||
if( intent != null ){
|
||||
handleSentIntent( intent );
|
||||
}
|
||||
|
||||
|
||||
if( pager_adapter.getCount() == 0 ){
|
||||
llEmpty.setVisibility( View.VISIBLE );
|
||||
}else{
|
||||
for(Column column : app_state.column_list ){
|
||||
column.onResume(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSentIntent( final Intent sent_intent ){
|
||||
|
@ -192,8 +202,12 @@ public class ActMain extends AppCompatActivity
|
|||
}
|
||||
|
||||
@Override protected void onPause(){
|
||||
bResume = false;
|
||||
|
||||
closeListItemPopup();
|
||||
|
||||
app_state.stream_reader.onPause();
|
||||
|
||||
HTMLDecoder.link_callback = null;
|
||||
super.onPause();
|
||||
}
|
||||
|
@ -268,7 +282,7 @@ public class ActMain extends AppCompatActivity
|
|||
llEmpty.setVisibility( View.VISIBLE );
|
||||
}else{
|
||||
int select = data.getIntExtra( ActColumnList.EXTRA_SELECTION, - 1 );
|
||||
if( select != - 1 ){
|
||||
if( 0 <= select && select < pager_adapter.getCount() ){
|
||||
pager.setCurrentItem( select, true );
|
||||
scrollColumnStrip( select );
|
||||
}
|
||||
|
@ -347,7 +361,7 @@ public class ActMain extends AppCompatActivity
|
|||
ActionsDialog dialog = new ActionsDialog();
|
||||
dialog.addAction( getString( R.string.close_column ), new Runnable() {
|
||||
@Override public void run(){
|
||||
performColumnClose( true, pager_adapter.getColumn( pager.getCurrentItem() ) );
|
||||
closeColumn( true, pager_adapter.getColumn( pager.getCurrentItem() ) );
|
||||
}
|
||||
} );
|
||||
dialog.addAction( getString( R.string.open_column_list ), new Runnable() {
|
||||
|
@ -373,7 +387,7 @@ public class ActMain extends AppCompatActivity
|
|||
ActMain.this.finish();
|
||||
return;
|
||||
}
|
||||
performColumnClose( false, column );
|
||||
closeColumn( false, column );
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -1042,7 +1056,7 @@ public class ActMain extends AppCompatActivity
|
|||
}
|
||||
}
|
||||
|
||||
public void performColumnClose( boolean bConfirm, final Column column ){
|
||||
public void closeColumn( boolean bConfirm, final Column column ){
|
||||
if( column.dont_close ){
|
||||
Utils.showToast( this, false, R.string.column_has_dont_close_option );
|
||||
return;
|
||||
|
@ -1055,7 +1069,7 @@ public class ActMain extends AppCompatActivity
|
|||
.setPositiveButton( R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick( DialogInterface dialog, int which ){
|
||||
performColumnClose( true, column );
|
||||
closeColumn( true, column );
|
||||
}
|
||||
} )
|
||||
.show();
|
||||
|
@ -1088,7 +1102,7 @@ public class ActMain extends AppCompatActivity
|
|||
//
|
||||
llEmpty.setVisibility( View.GONE );
|
||||
//
|
||||
Column col = new Column( app_state, ai, type, params );
|
||||
Column col = new Column( app_state, ai, this,type, params );
|
||||
int idx = pager_adapter.addColumn( pager, col );
|
||||
app_state.saveColumnList();
|
||||
updateColumnStrip();
|
||||
|
@ -1140,7 +1154,7 @@ public class ActMain extends AppCompatActivity
|
|||
}
|
||||
Utils.showToast( ActMain.this, false, R.string.app_was_muted );
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
interface GetAccountCallback {
|
||||
|
|
|
@ -2,6 +2,7 @@ package jp.juggler.subwaytooter;
|
|||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.json.JSONArray;
|
||||
|
@ -26,12 +27,16 @@ class AppState {
|
|||
final Context context;
|
||||
final float density;
|
||||
final SharedPreferences pref;
|
||||
final Handler handler;
|
||||
|
||||
final StreamReader stream_reader;
|
||||
|
||||
AppState( Context applicationContext ,SharedPreferences pref){
|
||||
this.context = applicationContext;
|
||||
this.pref = pref;
|
||||
this.density = context.getResources().getDisplayMetrics().density;
|
||||
this.stream_reader = new StreamReader(applicationContext,pref);
|
||||
this.handler = new Handler();
|
||||
|
||||
if( ! isLoaded ){
|
||||
isLoaded = true;
|
||||
|
|
|
@ -9,11 +9,15 @@ import android.text.TextUtils;
|
|||
import android.view.View;
|
||||
import android.widget.ListView;
|
||||
|
||||
import com.omadahealth.github.swipyrefreshlayout.library.SwipyRefreshLayoutDirection;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.regex.Matcher;
|
||||
|
@ -37,6 +41,7 @@ import jp.juggler.subwaytooter.table.MutedWord;
|
|||
import jp.juggler.subwaytooter.table.SavedAccount;
|
||||
import jp.juggler.subwaytooter.table.UserRelation;
|
||||
import jp.juggler.subwaytooter.util.BucketList;
|
||||
import jp.juggler.subwaytooter.util.DuplicateMap;
|
||||
import jp.juggler.subwaytooter.util.LogCategory;
|
||||
import jp.juggler.subwaytooter.util.MyListView;
|
||||
import jp.juggler.subwaytooter.util.ScrollPosition;
|
||||
|
@ -45,6 +50,26 @@ import jp.juggler.subwaytooter.util.Utils;
|
|||
class Column {
|
||||
private static final LogCategory log = new LogCategory( "Column" );
|
||||
|
||||
|
||||
interface Callback {
|
||||
boolean isActivityResume();
|
||||
}
|
||||
|
||||
private WeakReference< Callback > callback_ref;
|
||||
|
||||
private boolean isResume(){
|
||||
if( callback_ref == null ){
|
||||
log.d("isResume: callback_ref is not set");
|
||||
return false;
|
||||
}
|
||||
Callback cb = callback_ref.get();
|
||||
if( cb == null ){
|
||||
log.d("isResume: callback was lost.");
|
||||
return false;
|
||||
}
|
||||
return cb.isActivityResume();
|
||||
}
|
||||
|
||||
private static Object getParamAt( Object[] params, int idx ){
|
||||
if( params == null || idx >= params.length ){
|
||||
throw new IndexOutOfBoundsException( "getParamAt idx=" + idx );
|
||||
|
@ -164,11 +189,12 @@ class Column {
|
|||
|
||||
ScrollPosition scroll_save;
|
||||
|
||||
Column( @NonNull AppState app_state, @NonNull SavedAccount access_info, int type, Object... params ){
|
||||
Column( @NonNull AppState app_state, @NonNull SavedAccount access_info, @NonNull Callback callback,int type, Object... params ){
|
||||
this.app_state = app_state;
|
||||
this.context = app_state.context;
|
||||
this.access_info = access_info;
|
||||
this.column_type = type;
|
||||
this.callback_ref = new WeakReference< Callback >( callback );
|
||||
switch( type ){
|
||||
|
||||
case TYPE_CONVERSATION:
|
||||
|
@ -203,13 +229,12 @@ class Column {
|
|||
item.put( KEY_DONT_SHOW_REPLY, dont_show_reply );
|
||||
item.put( KEY_REGEX_TEXT, regex_text );
|
||||
|
||||
|
||||
item.put( KEY_HEADER_BACKGROUND_COLOR, header_bg_color );
|
||||
item.put( KEY_HEADER_TEXT_COLOR, header_fg_color );
|
||||
item.put( KEY_COLUMN_BACKGROUND_COLOR, column_bg_color );
|
||||
item.put( KEY_COLUMN_BACKGROUND_IMAGE, column_bg_image );
|
||||
item.put( KEY_COLUMN_BACKGROUND_IMAGE_ALPHA, (double) column_bg_image_alpha );
|
||||
|
||||
|
||||
switch( column_type ){
|
||||
case TYPE_CONVERSATION:
|
||||
case TYPE_BOOSTED_BY:
|
||||
|
@ -238,7 +263,7 @@ class Column {
|
|||
item.put( KEY_OLD_INDEX, old_index );
|
||||
}
|
||||
|
||||
Column( @NonNull AppState app_state,JSONObject src ){
|
||||
Column( @NonNull AppState app_state, JSONObject src ){
|
||||
this.app_state = app_state;
|
||||
this.context = app_state.context;
|
||||
|
||||
|
@ -256,8 +281,8 @@ class Column {
|
|||
this.header_fg_color = src.optInt( KEY_HEADER_TEXT_COLOR );
|
||||
this.column_bg_color = src.optInt( KEY_COLUMN_BACKGROUND_COLOR );
|
||||
this.column_bg_image = Utils.optStringX( src, KEY_COLUMN_BACKGROUND_IMAGE );
|
||||
this.column_bg_image_alpha = (float)src.optDouble( KEY_COLUMN_BACKGROUND_IMAGE_ALPHA ,1.0f);
|
||||
|
||||
this.column_bg_image_alpha = (float) src.optDouble( KEY_COLUMN_BACKGROUND_IMAGE_ALPHA, 1.0f );
|
||||
|
||||
switch( column_type ){
|
||||
|
||||
case TYPE_CONVERSATION:
|
||||
|
@ -334,6 +359,7 @@ class Column {
|
|||
|
||||
void dispose(){
|
||||
is_dispose.set( true );
|
||||
stopStreaming();
|
||||
}
|
||||
|
||||
String getColumnName( boolean bLong ){
|
||||
|
@ -471,7 +497,6 @@ class Column {
|
|||
|
||||
boolean bFirstInitialized = false;
|
||||
|
||||
|
||||
private void init(){
|
||||
bSimpleList = ( column_type != Column.TYPE_CONVERSATION && app_state.pref.getBoolean( Pref.KEY_SIMPLE_LIST, false ) );
|
||||
}
|
||||
|
@ -708,6 +733,7 @@ class Column {
|
|||
String task_progress;
|
||||
|
||||
final BucketList< Object > list_data = new BucketList<>();
|
||||
private final DuplicateMap duplicate_map = new DuplicateMap();
|
||||
|
||||
private static boolean hasMedia( TootStatus status ){
|
||||
if( status == null ) return false;
|
||||
|
@ -732,7 +758,7 @@ class Column {
|
|||
for( Object o : list_data ){
|
||||
if( o instanceof TootStatus ){
|
||||
TootStatus item = (TootStatus) o;
|
||||
if( item.checkMuted( muted_app,muted_word )){
|
||||
if( item.checkMuted( muted_app, muted_word ) ){
|
||||
continue;
|
||||
|
||||
}
|
||||
|
@ -742,7 +768,7 @@ class Column {
|
|||
TootStatus status = item.status;
|
||||
|
||||
if( status != null ){
|
||||
if( status.checkMuted( muted_app,muted_word )){
|
||||
if( status.checkMuted( muted_app, muted_word ) ){
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
@ -756,10 +782,12 @@ class Column {
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private void addWithFilter( ArrayList< Object > dst, TootStatus.List src ){
|
||||
|
||||
Pattern column_regex_filter = null;
|
||||
private Pattern column_regex_filter;
|
||||
private HashSet< String > muted_app;
|
||||
private HashSet< String > muted_word;
|
||||
|
||||
private void initFilter(){
|
||||
column_regex_filter = null;
|
||||
if( ! TextUtils.isEmpty( regex_text ) ){
|
||||
try{
|
||||
column_regex_filter = Pattern.compile( regex_text );
|
||||
|
@ -768,70 +796,84 @@ 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;
|
||||
}
|
||||
|
||||
if( dont_show_boost ){
|
||||
if( status.reblog != null ) continue;
|
||||
}
|
||||
|
||||
if( dont_show_reply ){
|
||||
if( status.in_reply_to_id != null
|
||||
|| ( status.reblog != null && status.reblog.in_reply_to_id != null )
|
||||
) continue;
|
||||
}
|
||||
|
||||
if( column_regex_filter != null ){
|
||||
if( status.reblog != null ){
|
||||
if( column_regex_filter.matcher( status.reblog.decoded_content.toString() ).find() )
|
||||
continue;
|
||||
}else{
|
||||
if( column_regex_filter.matcher( status.decoded_content.toString() ).find() )
|
||||
continue;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if( status.checkMuted( muted_app,muted_word )){
|
||||
continue;
|
||||
}
|
||||
|
||||
dst.add( status );
|
||||
muted_app = MutedApp.getNameSet();
|
||||
muted_word = MutedWord.getNameSet();
|
||||
}
|
||||
|
||||
private boolean isFiltered( TootStatus status ){
|
||||
if( with_attachment ){
|
||||
if( ! hasMedia( status ) && ! hasMedia( status.reblog ) ) return true;
|
||||
}
|
||||
|
||||
if( dont_show_boost ){
|
||||
if( status.reblog != null ) return true;
|
||||
}
|
||||
|
||||
if( dont_show_reply ){
|
||||
if( status.in_reply_to_id != null
|
||||
|| ( status.reblog != null && status.reblog.in_reply_to_id != null )
|
||||
) return true;
|
||||
}
|
||||
|
||||
if( column_regex_filter != null ){
|
||||
if( status.reblog != null ){
|
||||
if( column_regex_filter.matcher( status.reblog.decoded_content.toString() ).find() )
|
||||
return true;
|
||||
}else{
|
||||
if( column_regex_filter.matcher( status.decoded_content.toString() ).find() )
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
//noinspection RedundantIfStatement
|
||||
if( status.checkMuted( muted_app, muted_word ) ){
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private void addWithFilter( ArrayList< Object > dst, TootStatus.List src ){
|
||||
for( TootStatus status : src ){
|
||||
if( ! isFiltered( status ) ){
|
||||
dst.add( status );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFiltered( TootNotification item ){
|
||||
|
||||
TootStatus status = item.status;
|
||||
if( status != null ){
|
||||
if( status.checkMuted( muted_app, muted_word ) ){
|
||||
log.d( "addWithFilter: status muted." );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
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.checkMuted( muted_app,muted_word ) ){
|
||||
log.d( "addWithFilter: status muted.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if( ! isFiltered( item ) ){
|
||||
dst.add( item );
|
||||
}
|
||||
|
||||
dst.add( item );
|
||||
}
|
||||
}
|
||||
|
||||
void startLoading(){
|
||||
cancelLastTask();
|
||||
|
||||
stopStreaming();
|
||||
|
||||
initFilter();
|
||||
|
||||
bFirstInitialized = true;
|
||||
list_data.clear();
|
||||
duplicate_map.clear();
|
||||
mRefreshLoadingError = null;
|
||||
bRefreshLoading = false;
|
||||
mInitialLoadingError = null;
|
||||
|
@ -1110,15 +1152,18 @@ class Column {
|
|||
Column.this.mInitialLoadingError = result.error;
|
||||
}else{
|
||||
if( list_tmp != null ){
|
||||
ArrayList< Object > list_new = duplicate_map.filterDuplicate( list_tmp );
|
||||
list_data.clear();
|
||||
list_data.addAll( list_tmp );
|
||||
list_data.addAll( list_new );
|
||||
}
|
||||
|
||||
resumeStreaming( false );
|
||||
}
|
||||
fireShowContent();
|
||||
}
|
||||
};
|
||||
|
||||
task.executeOnExecutor(App1.task_executor);
|
||||
task.executeOnExecutor( App1.task_executor );
|
||||
}
|
||||
|
||||
private static final Pattern reMaxId = Pattern.compile( "&max_id=(\\d+)" ); // より古いデータの取得に使う
|
||||
|
@ -1297,6 +1342,8 @@ class Column {
|
|||
}
|
||||
}
|
||||
|
||||
boolean bRefreshingTop;
|
||||
|
||||
void startRefresh( final boolean bSilent, final boolean bBottom, final long status_id, final int refresh_after_toot ){
|
||||
|
||||
if( last_task != null ){
|
||||
|
@ -1323,6 +1370,11 @@ class Column {
|
|||
}
|
||||
}
|
||||
|
||||
if(!bBottom){
|
||||
bRefreshingTop = true;
|
||||
stopStreaming();
|
||||
}
|
||||
|
||||
bRefreshLoading = true;
|
||||
mRefreshLoadingError = null;
|
||||
|
||||
|
@ -1736,114 +1788,91 @@ class Column {
|
|||
|
||||
@Override
|
||||
protected void onPostExecute( TootApiResult result ){
|
||||
|
||||
if( isCancelled() || result == null ){
|
||||
return;
|
||||
}
|
||||
last_task = null;
|
||||
bRefreshLoading = false;
|
||||
|
||||
if( result.error != null ){
|
||||
Column.this.mRefreshLoadingError = result.error;
|
||||
fireShowContent();
|
||||
return;
|
||||
}
|
||||
if( list_tmp == null || list_tmp.isEmpty() ){
|
||||
fireShowContent();
|
||||
return;
|
||||
}
|
||||
|
||||
// 古いリストにある要素のIDの集合集合
|
||||
HashSet< Long > set_status_id = new HashSet<>();
|
||||
HashSet< Long > set_notification_id = new HashSet<>();
|
||||
HashSet< Long > set_report_id = new HashSet<>();
|
||||
HashSet< Long > set_account_id = new HashSet<>();
|
||||
for( Object o : list_data ){
|
||||
if( o instanceof TootStatus ){
|
||||
set_status_id.add( ( (TootStatus) o ).id );
|
||||
}else if( o instanceof TootNotification ){
|
||||
set_notification_id.add( ( (TootNotification) o ).id );
|
||||
}else if( o instanceof TootReport ){
|
||||
set_report_id.add( ( (TootReport) o ).id );
|
||||
}else if( o instanceof TootAccount ){
|
||||
set_account_id.add( ( (TootAccount) o ).id );
|
||||
}
|
||||
}
|
||||
ArrayList< Object > list_new = new ArrayList<>();
|
||||
for( Object o : list_tmp ){
|
||||
if( o instanceof TootStatus ){
|
||||
if( set_status_id.contains( ( (TootStatus) o ).id ) ) continue;
|
||||
}else if( o instanceof TootNotification ){
|
||||
if( set_notification_id.contains( ( (TootNotification) o ).id ) )
|
||||
continue;
|
||||
}else if( o instanceof TootReport ){
|
||||
if( set_report_id.contains( ( (TootReport) o ).id ) ) continue;
|
||||
}else if( o instanceof TootAccount ){
|
||||
if( set_account_id.contains( ( (TootAccount) o ).id ) ) continue;
|
||||
}
|
||||
list_new.add( o );
|
||||
}
|
||||
if( list_new.isEmpty() ){
|
||||
fireShowContent();
|
||||
return;
|
||||
}
|
||||
|
||||
// 事前にスクロール位置を覚えておく
|
||||
ScrollPosition sp = null;
|
||||
if( holder != null ){
|
||||
sp = holder.getScrollPosition();
|
||||
}
|
||||
|
||||
if( bBottom ){
|
||||
list_data.addAll( list_new );
|
||||
fireShowContent();
|
||||
try{
|
||||
last_task = null;
|
||||
bRefreshLoading = false;
|
||||
|
||||
if( sp != null ){
|
||||
holder.setScrollPosition( sp, 20f );
|
||||
if( result.error != null ){
|
||||
Column.this.mRefreshLoadingError = result.error;
|
||||
fireShowContent();
|
||||
return;
|
||||
}
|
||||
if( list_tmp == null || list_tmp.isEmpty() ){
|
||||
fireShowContent();
|
||||
return;
|
||||
}
|
||||
}else{
|
||||
|
||||
int status_index = - 1;
|
||||
for( int i = 0, ie = list_new.size() ; i < ie ; ++ i ){
|
||||
Object o = list_new.get( i );
|
||||
if( o instanceof TootStatus ){
|
||||
TootStatus status = (TootStatus) o;
|
||||
if( status.id == status_id ){
|
||||
status_index = i;
|
||||
break;
|
||||
ArrayList< Object > list_new = duplicate_map.filterDuplicate( list_tmp );
|
||||
|
||||
if( list_new.isEmpty() ){
|
||||
fireShowContent();
|
||||
return;
|
||||
}
|
||||
|
||||
// 事前にスクロール位置を覚えておく
|
||||
ScrollPosition sp = null;
|
||||
if( holder != null ){
|
||||
sp = holder.getScrollPosition();
|
||||
}
|
||||
|
||||
if( bBottom ){
|
||||
list_data.addAll( list_new );
|
||||
fireShowContent();
|
||||
|
||||
if( sp != null ){
|
||||
holder.setScrollPosition( sp, 20f );
|
||||
}
|
||||
}else{
|
||||
|
||||
int status_index = - 1;
|
||||
for( int i = 0, ie = list_new.size() ; i < ie ; ++ i ){
|
||||
Object o = list_new.get( i );
|
||||
if( o instanceof TootStatus ){
|
||||
TootStatus status = (TootStatus) o;
|
||||
if( status.id == status_id ){
|
||||
status_index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int added = list_new.size();
|
||||
list_data.addAll( 0, list_new );
|
||||
fireShowContent();
|
||||
|
||||
if( status_index >= 0 && refresh_after_toot == Pref.RAT_REFRESH_SCROLL ){
|
||||
if( holder != null ){
|
||||
holder.setScrollPosition( new ScrollPosition( status_index, 0 ), 0f );
|
||||
}else{
|
||||
scroll_save = new ScrollPosition( status_index, 0 );
|
||||
}
|
||||
}else{
|
||||
float delta = bSilent ? 0f : - 20f;
|
||||
if( sp != null ){
|
||||
sp.pos += added;
|
||||
holder.setScrollPosition( sp, delta );
|
||||
}else if( scroll_save != null ){
|
||||
scroll_save.pos += added;
|
||||
}else{
|
||||
scroll_save = new ScrollPosition( added, 0 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int added = list_new.size();
|
||||
list_data.addAll( 0, list_new );
|
||||
fireShowContent();
|
||||
|
||||
if( status_index >= 0 && refresh_after_toot == Pref.RAT_REFRESH_SCROLL ){
|
||||
if( holder != null ){
|
||||
holder.setScrollPosition( new ScrollPosition( status_index, 0 ), 0f );
|
||||
}else{
|
||||
scroll_save = new ScrollPosition( status_index, 0 );
|
||||
}
|
||||
}else{
|
||||
float delta = bSilent ? 0f : - 20f;
|
||||
if( sp != null ){
|
||||
sp.pos += added;
|
||||
holder.setScrollPosition( sp, delta );
|
||||
}else if( scroll_save != null ){
|
||||
scroll_save.pos += added;
|
||||
}else{
|
||||
scroll_save = new ScrollPosition( added, 0 );
|
||||
}
|
||||
}finally{
|
||||
if(!bBottom){
|
||||
bRefreshingTop = false;
|
||||
resumeStreaming( false );
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
task.executeOnExecutor(App1.task_executor);
|
||||
|
||||
task.executeOnExecutor( App1.task_executor );
|
||||
}
|
||||
|
||||
|
||||
void startGap( final TootGap gap, final int position ){
|
||||
if( last_task != null ){
|
||||
Utils.showToast( context, true, R.string.column_is_busy );
|
||||
|
@ -2178,39 +2207,9 @@ class Column {
|
|||
fireShowContent();
|
||||
return;
|
||||
}
|
||||
// 0個でもギャップを消すために以下の処理を続ける
|
||||
|
||||
// 古いリストにある要素のIDの集合
|
||||
HashSet< Long > set_status_id = new HashSet<>();
|
||||
HashSet< Long > set_notification_id = new HashSet<>();
|
||||
HashSet< Long > set_report_id = new HashSet<>();
|
||||
HashSet< Long > set_account_id = new HashSet<>();
|
||||
for( Object o : list_data ){
|
||||
if( o instanceof TootStatus ){
|
||||
set_status_id.add( ( (TootStatus) o ).id );
|
||||
}else if( o instanceof TootNotification ){
|
||||
set_notification_id.add( ( (TootNotification) o ).id );
|
||||
}else if( o instanceof TootReport ){
|
||||
set_report_id.add( ( (TootReport) o ).id );
|
||||
}else if( o instanceof TootAccount ){
|
||||
set_account_id.add( ( (TootAccount) o ).id );
|
||||
}
|
||||
}
|
||||
// list_tmp をフィルタしてlist_newを作成
|
||||
ArrayList< Object > list_new = new ArrayList<>();
|
||||
for( Object o : list_tmp ){
|
||||
if( o instanceof TootStatus ){
|
||||
if( set_status_id.contains( ( (TootStatus) o ).id ) ) continue;
|
||||
}else if( o instanceof TootNotification ){
|
||||
if( set_notification_id.contains( ( (TootNotification) o ).id ) )
|
||||
continue;
|
||||
}else if( o instanceof TootReport ){
|
||||
if( set_report_id.contains( ( (TootReport) o ).id ) ) continue;
|
||||
}else if( o instanceof TootAccount ){
|
||||
if( set_account_id.contains( ( (TootAccount) o ).id ) )
|
||||
continue;
|
||||
}
|
||||
list_new.add( o );
|
||||
}
|
||||
ArrayList< Object > list_new = duplicate_map.filterDuplicate( list_tmp );
|
||||
|
||||
// idx番目の要素がListViewのtopから何ピクセル下にあるか
|
||||
int restore_idx = position + 1;
|
||||
|
@ -2223,7 +2222,7 @@ class Column {
|
|||
try{
|
||||
restore_y = getItemTop( restore_idx );
|
||||
}catch( IndexOutOfBoundsException ex2 ){
|
||||
restore_idx = -1;
|
||||
restore_idx = - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2235,7 +2234,7 @@ class Column {
|
|||
|
||||
if( holder != null ){
|
||||
//noinspection StatementWithEmptyBody
|
||||
if(restore_idx >= 0 ){
|
||||
if( restore_idx >= 0 ){
|
||||
setItemTop( restore_idx + added - 1, restore_y );
|
||||
}else{
|
||||
// ギャップが画面内にない場合、何もしない
|
||||
|
@ -2248,7 +2247,7 @@ class Column {
|
|||
}
|
||||
};
|
||||
|
||||
task.executeOnExecutor(App1.task_executor);
|
||||
task.executeOnExecutor( App1.task_executor );
|
||||
}
|
||||
|
||||
private static final int heightSpec = View.MeasureSpec.makeMeasureSpec( 0, View.MeasureSpec.UNSPECIFIED );
|
||||
|
@ -2296,4 +2295,296 @@ class Column {
|
|||
return listView.getChildAt( child_idx ).getTop();
|
||||
}
|
||||
|
||||
final StreamReader.Callback stream_callback = new StreamReader.Callback() {
|
||||
@Override public void onEvent( String event_type, Object o ){
|
||||
|
||||
if( o instanceof Long ){
|
||||
removeStatus( access_info, (Long) o );
|
||||
return;
|
||||
}
|
||||
|
||||
if( o instanceof TootNotification ){
|
||||
TootNotification notification = (TootNotification) o;
|
||||
if( column_type != TYPE_NOTIFICATIONS ) return;
|
||||
if( isFiltered( notification ) ) return;
|
||||
}else if( o instanceof TootStatus ){
|
||||
TootStatus status = (TootStatus) o;
|
||||
if( column_type == TYPE_NOTIFICATIONS ) return;
|
||||
if( column_type == TYPE_LOCAL && status.account.acct.indexOf( '@' ) != - 1 ) return;
|
||||
if( isFiltered( status ) ) return;
|
||||
}
|
||||
|
||||
stream_data_queue.addFirst( o );
|
||||
proc_stream_data.run();
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
private final LinkedList< Object > stream_data_queue = new LinkedList<>();
|
||||
|
||||
// ListViewの表示更新が追いつかないとスクロール位置が崩れるので
|
||||
// 一定時間より短期間にはデータ更新しないようにする
|
||||
private long last_show_stream_data;
|
||||
|
||||
final Runnable proc_stream_data = new Runnable() {
|
||||
@Override public void run(){
|
||||
App1.getAppState( context ).handler.removeCallbacks( proc_stream_data );
|
||||
long now = SystemClock.elapsedRealtime();
|
||||
long remain = last_show_stream_data + 333L - now;
|
||||
if( remain > 0 ){
|
||||
App1.getAppState( context ).handler.postDelayed( proc_stream_data, 333L );
|
||||
return;
|
||||
}
|
||||
last_show_stream_data = now;
|
||||
|
||||
ArrayList< Object > list_new = duplicate_map.filterDuplicate( stream_data_queue );
|
||||
stream_data_queue.clear();
|
||||
|
||||
if( list_new.isEmpty() ){
|
||||
return;
|
||||
}else{
|
||||
if( column_type == TYPE_NOTIFICATIONS ){
|
||||
TootNotification.List list = new TootNotification.List( );
|
||||
for( Object o : list_new ){
|
||||
if( o instanceof TootNotification){
|
||||
list.add( (TootNotification) o);
|
||||
}
|
||||
}
|
||||
if( !list.isEmpty() ){
|
||||
AlarmService.injectData( context, access_info.db_id, list );
|
||||
}
|
||||
}
|
||||
|
||||
try{
|
||||
since_id = Long.toString( getId( list_new.get( 0 ) ) );
|
||||
}catch(Throwable ex){
|
||||
// ストリームに来るのは通知かステータスだから、多分ここは通らない
|
||||
log.e(ex,"getId() failed. o=",list_new.get( 0 ));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 事前にスクロール位置を覚えておく
|
||||
ScrollPosition sp = null;
|
||||
if( holder != null ){
|
||||
sp = holder.getScrollPosition();
|
||||
}
|
||||
|
||||
// idx番目の要素がListViewのtopから何ピクセル下にあるか
|
||||
int restore_idx = - 1;
|
||||
int restore_y = 0;
|
||||
if( holder != null ){
|
||||
if( list_data.size() > 0 ){
|
||||
try{
|
||||
restore_idx = holder.getListView().getFirstVisiblePosition();
|
||||
restore_y = getItemTop( restore_idx );
|
||||
}catch( IndexOutOfBoundsException ex ){
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( bPutGap ){
|
||||
bPutGap = false;
|
||||
try{
|
||||
if( list_new.size() > 0 && list_data.size() > 0 ){
|
||||
long max = getId( list_new.get( list_new.size() - 1 ) );
|
||||
long since = getId( list_data.get( 0 ) );
|
||||
if( max > since ){
|
||||
TootGap gap = new TootGap( max,since );
|
||||
list_new.add( gap );
|
||||
}
|
||||
}
|
||||
}catch( Throwable ex ){
|
||||
log.e( ex, "can't put gap." );
|
||||
}
|
||||
}
|
||||
|
||||
list_data.addAll( 0, list_new );
|
||||
fireShowContent();
|
||||
int added = list_new.size();
|
||||
|
||||
|
||||
if( holder != null ){
|
||||
//noinspection StatementWithEmptyBody
|
||||
if( sp == null || ( sp.pos == 0 && sp.top == 0 ) ){
|
||||
// スクロール位置が先頭なら先頭のまま
|
||||
}else if( restore_idx >= 0 ){
|
||||
//
|
||||
setItemTop( restore_idx + added, restore_y );
|
||||
}else{
|
||||
// ギャップが画面内にない場合、何もしない
|
||||
}
|
||||
}else{
|
||||
if( scroll_save == null || ( scroll_save.pos == 0 || scroll_save.top == 0 ) ){
|
||||
// スクロール位置が先頭なら先頭のまま
|
||||
}else{
|
||||
// 現在の要素が表示され続けるようにしたい
|
||||
scroll_save.pos += added;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private long getId( Object o ){
|
||||
if( o instanceof TootNotification ){
|
||||
return ( (TootNotification) o ).id;
|
||||
}else if( o instanceof TootStatus ){
|
||||
return ( (TootStatus) o ).id;
|
||||
}else if( o instanceof TootAccount){
|
||||
return ( (TootAccount) o ).id;
|
||||
}
|
||||
throw new RuntimeException( "getId: object is not status,notification" );
|
||||
}
|
||||
|
||||
// onPauseの時はまとめて止められるが
|
||||
// カラム破棄やリロード開始時は個別にストリーミングを止める必要がある
|
||||
private void stopStreaming(){
|
||||
|
||||
switch( column_type ){
|
||||
case TYPE_HOME:
|
||||
case TYPE_NOTIFICATIONS:
|
||||
|
||||
app_state.stream_reader.unregister(
|
||||
access_info
|
||||
, StreamReader.EP_USER
|
||||
, stream_callback
|
||||
);
|
||||
break;
|
||||
|
||||
case TYPE_LOCAL:
|
||||
case TYPE_FEDERATE:
|
||||
app_state.stream_reader.unregister(
|
||||
access_info
|
||||
, StreamReader.EP_PUBLIC
|
||||
, stream_callback
|
||||
);
|
||||
break;
|
||||
|
||||
case TYPE_HASHTAG:
|
||||
app_state.stream_reader.unregister(
|
||||
access_info
|
||||
, StreamReader.EP_HASHTAG + "?tag=" + Uri.encode( hashtag )
|
||||
, stream_callback
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void onResume( Callback callback ){
|
||||
this.callback_ref = new WeakReference<>( callback );
|
||||
|
||||
// 破棄されたカラムなら何もしない
|
||||
if( is_dispose.get() ){
|
||||
log.d("onResume: column was disposed.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 未初期化なら何もしない
|
||||
if( ! bFirstInitialized ){
|
||||
log.d("onResume: column is not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 初期ロード中なら何もしない
|
||||
if( bInitialLoading ){
|
||||
log.d("onResume: column is in initial loading.");
|
||||
return;
|
||||
}
|
||||
|
||||
if( bRefreshingTop ){
|
||||
log.d("onResume: bRefreshingTop is true.");
|
||||
// リフレッシュ終了時に自動でストリーミング開始するはず
|
||||
}else if( !bRefreshLoading
|
||||
&& ! App1.getAppState( context ).pref.getBoolean(Pref.KEY_DONT_REFRESH_ON_RESUME,false)
|
||||
){
|
||||
log.d("onResume: start auto refresh.");
|
||||
// リフレッシュしてからストリーミング開始
|
||||
startRefresh( true, false, - 1L, - 1 );
|
||||
}else{
|
||||
log.d("onResume: start streaming with gap.");
|
||||
// ギャップつきでストリーミング開始
|
||||
resumeStreaming( true );
|
||||
}
|
||||
}
|
||||
|
||||
boolean bPutGap;
|
||||
|
||||
private void resumeStreaming( boolean bPutGap ){
|
||||
|
||||
if( ! isResume() ){
|
||||
log.d("resumeStreaming: not resumed.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 破棄されたカラムなら何もしない
|
||||
if( is_dispose.get() ){
|
||||
log.d("resumeStreaming: column was disposed.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 未初期化なら何もしない
|
||||
if( ! bFirstInitialized ){
|
||||
log.d("resumeStreaming: column is not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 初期ロード中なら何もしない
|
||||
if( bInitialLoading ){
|
||||
log.d("resumeStreaming: is in initial loading.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.bPutGap = bPutGap;
|
||||
|
||||
stream_data_queue.clear();
|
||||
|
||||
switch( column_type ){
|
||||
case TYPE_HOME:
|
||||
case TYPE_NOTIFICATIONS:
|
||||
|
||||
if( access_info.isPseudo() ) return;
|
||||
|
||||
app_state.stream_reader.register(
|
||||
access_info
|
||||
, StreamReader.EP_USER
|
||||
, stream_callback
|
||||
);
|
||||
break;
|
||||
|
||||
case TYPE_LOCAL:
|
||||
|
||||
// 認証がないと読めないらしい
|
||||
if( access_info.isPseudo() ) return;
|
||||
|
||||
app_state.stream_reader.register(
|
||||
access_info
|
||||
, StreamReader.EP_PUBLIC_LOCAL
|
||||
, stream_callback
|
||||
);
|
||||
break;
|
||||
|
||||
case TYPE_FEDERATE:
|
||||
|
||||
// 認証がないと読めないらしい
|
||||
if( access_info.isPseudo() ) return;
|
||||
|
||||
app_state.stream_reader.register(
|
||||
access_info
|
||||
, StreamReader.EP_PUBLIC
|
||||
, stream_callback
|
||||
);
|
||||
break;
|
||||
|
||||
case TYPE_HASHTAG:
|
||||
app_state.stream_reader.register(
|
||||
access_info
|
||||
, StreamReader.EP_HASHTAG + "&tag=" + Uri.encode( hashtag )
|
||||
, stream_callback
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -47,7 +47,6 @@ class ColumnPagerAdapter extends PagerAdapter {
|
|||
pager.setAdapter( null );
|
||||
column_list.add( index, column );
|
||||
pager.setAdapter( this );
|
||||
notifyDataSetChanged();
|
||||
return index;
|
||||
}
|
||||
|
||||
|
@ -55,9 +54,8 @@ class ColumnPagerAdapter extends PagerAdapter {
|
|||
int idx_column = column_list.indexOf( column );
|
||||
if( idx_column == - 1 ) return;
|
||||
pager.setAdapter( null );
|
||||
column_list.remove( idx_column );
|
||||
column_list.remove( idx_column ).dispose();
|
||||
pager.setAdapter( this );
|
||||
column.dispose();
|
||||
}
|
||||
|
||||
void setOrder( ViewPager pager, ArrayList< Integer > order ){
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package jp.juggler.subwaytooter;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
|
@ -430,7 +428,7 @@ class ColumnViewHolder
|
|||
public void onClick( View v ){
|
||||
switch( v.getId() ){
|
||||
case R.id.btnColumnClose:
|
||||
activity.performColumnClose( false, column );
|
||||
activity.closeColumn( false, column );
|
||||
break;
|
||||
|
||||
case R.id.btnColumnReload:
|
||||
|
|
|
@ -37,6 +37,9 @@ public class Pref {
|
|||
static final String KEY_FOOTER_TAB_BG_COLOR = "footer_tab_bg_color";
|
||||
static final String KEY_FOOTER_TAB_DIVIDER_COLOR = "footer_tab_divider_color";
|
||||
|
||||
static final String KEY_DONT_USE_STREAMING = "dont_use_streaming";
|
||||
static final String KEY_DONT_REFRESH_ON_RESUME = "dont_refresh_on_resume";
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,282 @@
|
|||
package jp.juggler.subwaytooter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import jp.juggler.subwaytooter.api.TootApiClient;
|
||||
import jp.juggler.subwaytooter.api.TootApiResult;
|
||||
import jp.juggler.subwaytooter.api.entity.TootNotification;
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus;
|
||||
import jp.juggler.subwaytooter.table.SavedAccount;
|
||||
import jp.juggler.subwaytooter.util.LogCategory;
|
||||
import jp.juggler.subwaytooter.util.Utils;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.WebSocket;
|
||||
import okhttp3.WebSocketListener;
|
||||
|
||||
class StreamReader {
|
||||
static final LogCategory log = new LogCategory( "StreamReader" );
|
||||
|
||||
static final String EP_USER = "/api/v1/streaming/?stream=user";
|
||||
static final String EP_PUBLIC = "/api/v1/streaming/?stream=public";
|
||||
static final String EP_PUBLIC_LOCAL = "/api/v1/streaming/?stream=public:local";
|
||||
static final String EP_HASHTAG = "/api/v1/streaming/?stream=hashtag"; // + &tag=hashtag (先頭の#を含まない)
|
||||
|
||||
interface Callback {
|
||||
void onEvent( String event_type, Object o );
|
||||
}
|
||||
|
||||
private class Reader extends WebSocketListener {
|
||||
final SavedAccount access_info;
|
||||
final String end_point;
|
||||
final LinkedList< Callback > callback_list = new LinkedList<>();
|
||||
|
||||
Reader( SavedAccount access_info, String end_point ){
|
||||
this.access_info = access_info;
|
||||
this.end_point = end_point;
|
||||
}
|
||||
|
||||
synchronized void addCallback( @NonNull Callback stream_callback ){
|
||||
for( Callback c : callback_list ){
|
||||
if( c == stream_callback ) return;
|
||||
}
|
||||
callback_list.add( stream_callback );
|
||||
}
|
||||
|
||||
synchronized void removeCallback( Callback stream_callback ){
|
||||
Iterator< Callback > it = callback_list.iterator();
|
||||
while( it.hasNext() ){
|
||||
Callback c = it.next();
|
||||
if( c == stream_callback ) it.remove();
|
||||
}
|
||||
}
|
||||
|
||||
final AtomicBoolean bDisposed = new AtomicBoolean();
|
||||
final AtomicBoolean bListening = new AtomicBoolean();
|
||||
final AtomicReference< WebSocket > socket = new AtomicReference<>( null );
|
||||
|
||||
void dispose(){
|
||||
bDisposed.set( true );
|
||||
WebSocket ws = socket.get();
|
||||
if( ws != null ){
|
||||
ws.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a web socket has been accepted by the remote peer and may begin transmitting
|
||||
* messages.
|
||||
*/
|
||||
@Override
|
||||
public void onOpen( WebSocket webSocket, Response response ){
|
||||
log.d( "WebSocket onOpen. url=%s", webSocket.request().url() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a text (type {@code 0x1}) message has been received.
|
||||
*/
|
||||
@Override
|
||||
public void onMessage( WebSocket webSocket, String text ){
|
||||
// log.d( "WebSocket onMessage. url=%s, message=%s", webSocket.request().url(), text );
|
||||
try{
|
||||
final JSONObject obj = new JSONObject( text );
|
||||
final String event = obj.optString( "event" );
|
||||
final Object payload = parsePayload( event, obj );
|
||||
if( payload != null ){
|
||||
Utils.runOnMainThread( new Runnable() {
|
||||
@Override public void run(){
|
||||
if( bDisposed.get() ) return;
|
||||
synchronized( this ){
|
||||
for( Callback callback : callback_list ){
|
||||
try{
|
||||
callback.onEvent( event, payload );
|
||||
}catch( Throwable ex ){
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
}catch( Throwable ex ){
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private Object parsePayload( String event, JSONObject obj ){
|
||||
try{
|
||||
if( "update".equals( event ) ){
|
||||
return TootStatus.parse( log, access_info, new JSONObject( obj.optString( "payload" ) ) );
|
||||
}else if( "notification".equals( event ) ){
|
||||
return TootNotification.parse( log, access_info, new JSONObject( obj.optString( "payload" ) ) );
|
||||
}else if( "delete".equals( event ) ){
|
||||
return obj.optLong( "payload", - 1L );
|
||||
}
|
||||
}catch( Throwable ex ){
|
||||
ex.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the peer has indicated that no more incoming messages will be transmitted.
|
||||
*/
|
||||
@Override
|
||||
public void onClosing( WebSocket webSocket, int code, String reason ){
|
||||
log.d( "WebSocket onClosing. code=%s,reason=%s,url=%s", code, reason, webSocket.request().url() );
|
||||
webSocket.cancel();
|
||||
bListening.set( false );
|
||||
handler.removeCallbacks( proc_reconnect );
|
||||
handler.postDelayed( proc_reconnect, 10000L );
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when both peers have indicated that no more messages will be transmitted and the
|
||||
* connection has been successfully released. No further calls to this listener will be made.
|
||||
*/
|
||||
@Override
|
||||
public void onClosed( WebSocket webSocket, int code, String reason ){
|
||||
log.d( "WebSocket onClosed. code=%s,reason=%s,url=%s", code, reason, webSocket.request().url() );
|
||||
bListening.set( false );
|
||||
handler.removeCallbacks( proc_reconnect );
|
||||
handler.postDelayed( proc_reconnect, 10000L );
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a web socket has been closed due to an error reading from or writing to the
|
||||
* network. Both outgoing and incoming messages may have been lost. No further calls to this
|
||||
* listener will be made.
|
||||
*/
|
||||
@Override
|
||||
public void onFailure( WebSocket webSocket, Throwable ex, Response response ){
|
||||
log.e( ex, "WebSocket onFailure. url=%s", webSocket.request().url() );
|
||||
bListening.set( false );
|
||||
handler.removeCallbacks( proc_reconnect );
|
||||
handler.postDelayed( proc_reconnect, 10000L );
|
||||
}
|
||||
|
||||
final Runnable proc_reconnect = new Runnable() {
|
||||
@Override public void run(){
|
||||
if( bDisposed.get() ) return;
|
||||
startRead();
|
||||
}
|
||||
};
|
||||
|
||||
void startRead(){
|
||||
if( bDisposed.get() ){
|
||||
log.d( "startRead: this reader is disposed." );
|
||||
return;
|
||||
}else if( bListening.get() ){
|
||||
log.d( "startRead: this reader is already listening." );
|
||||
return;
|
||||
}
|
||||
|
||||
bListening.set( true );
|
||||
new AsyncTask< Void, Void, TootApiResult >() {
|
||||
@Override protected TootApiResult doInBackground( Void... params ){
|
||||
TootApiClient client = new TootApiClient( context, new TootApiClient.Callback() {
|
||||
@Override public boolean isApiCancelled(){
|
||||
return isCancelled();
|
||||
}
|
||||
|
||||
@Override public void publishApiProgress( String s ){
|
||||
}
|
||||
} );
|
||||
|
||||
client.setAccount( access_info );
|
||||
|
||||
TootApiResult result = client.webSocket( end_point, new Request.Builder(), Reader.this );
|
||||
if( result == null ){
|
||||
log.d( "startRead: cancelled." );
|
||||
bListening.set( false );
|
||||
}else{
|
||||
socket.set( result.socket );
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}.executeOnExecutor( App1.task_executor );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final LinkedList< Reader > reader_list = new LinkedList<>();
|
||||
|
||||
final Context context;
|
||||
final SharedPreferences pref;
|
||||
private final Handler handler;
|
||||
|
||||
StreamReader( Context context, SharedPreferences pref ){
|
||||
this.context = context;
|
||||
this.pref = pref;
|
||||
this.handler = new Handler();
|
||||
}
|
||||
|
||||
private Reader prepareReader( @NonNull SavedAccount access_info, @NonNull String end_point ){
|
||||
synchronized( reader_list ){
|
||||
for( Reader reader : reader_list ){
|
||||
if( reader.access_info.db_id == access_info.db_id
|
||||
&& reader.end_point.equals( end_point )
|
||||
){
|
||||
return reader;
|
||||
}
|
||||
}
|
||||
Reader reader = new Reader( access_info, end_point );
|
||||
reader_list.add( reader );
|
||||
return reader;
|
||||
}
|
||||
}
|
||||
|
||||
// onPauseのタイミングで全てのStreaming接続を破棄する
|
||||
void onPause(){
|
||||
synchronized( reader_list ){
|
||||
for( Reader reader : reader_list ){
|
||||
reader.dispose();
|
||||
}
|
||||
reader_list.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// カラム破棄やリロードのタイミングで呼ばれる
|
||||
void unregister( SavedAccount access_info, String end_point, Callback stream_callback ){
|
||||
synchronized( reader_list ){
|
||||
Iterator< Reader > it = reader_list.iterator();
|
||||
while( it.hasNext() ){
|
||||
Reader reader = it.next();
|
||||
if( reader.access_info.db_id == access_info.db_id
|
||||
&& reader.end_point.equals( end_point )
|
||||
){
|
||||
reader.removeCallback( stream_callback );
|
||||
if( reader.callback_list.isEmpty() ){
|
||||
reader.dispose();
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// onResume や ロード完了ののタイミングで登録される
|
||||
void register( @NonNull SavedAccount access_info, @NonNull String end_point, @NonNull Callback stream_callback ){
|
||||
|
||||
if( pref.getBoolean( Pref.KEY_DONT_USE_STREAMING ,false) ) return;
|
||||
|
||||
final Reader reader = prepareReader( access_info, end_point );
|
||||
reader.addCallback( stream_callback );
|
||||
|
||||
if( ! reader.bListening.get() ){
|
||||
reader.startRead();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -20,6 +20,8 @@ import okhttp3.OkHttpClient;
|
|||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.WebSocket;
|
||||
import okhttp3.WebSocketListener;
|
||||
|
||||
public class TootApiClient {
|
||||
private static final LogCategory log = new LogCategory( "TootApiClient" );
|
||||
|
@ -141,6 +143,46 @@ public class TootApiClient {
|
|||
}
|
||||
|
||||
|
||||
public TootApiResult webSocket( String path, Request.Builder request_builder , WebSocketListener ws_listener ){
|
||||
|
||||
if( callback.isApiCancelled() ) return null;
|
||||
|
||||
// アクセストークンを使ってAPIを呼び出す
|
||||
callback.publishApiProgress( context.getString( R.string.request_api, path ) );
|
||||
|
||||
|
||||
if( account == null ){
|
||||
return new TootApiResult( "account is null" );
|
||||
}
|
||||
|
||||
String url = "wss://" + instance + path;
|
||||
|
||||
JSONObject token_info = account.token_info;
|
||||
String access_token = Utils.optStringX( token_info, "access_token" );
|
||||
if( !TextUtils.isEmpty( access_token ) ){
|
||||
char delm = (-1!= url.indexOf( '?' ) ? '&':'?');
|
||||
url = url + delm + "access_token="+ access_token;
|
||||
}
|
||||
|
||||
request_builder.url( url );
|
||||
|
||||
try{
|
||||
WebSocket ws = ok_http_client.newWebSocket( request_builder.build() ,ws_listener );
|
||||
if( callback.isApiCancelled() ){
|
||||
ws.cancel();
|
||||
return null;
|
||||
}
|
||||
return new TootApiResult( ws );
|
||||
}catch( Throwable ex ){
|
||||
ex.printStackTrace( );
|
||||
return new TootApiResult(
|
||||
Utils.formatError( ex, context.getResources(), R.string.network_error )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 疑似アカウントの追加時に、インスタンスの検証を行う
|
||||
public TootApiResult checkInstance(){
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import java.util.regex.Pattern;
|
|||
|
||||
import jp.juggler.subwaytooter.util.LogCategory;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.WebSocket;
|
||||
|
||||
public class TootApiResult {
|
||||
public String error;
|
||||
|
@ -18,19 +19,21 @@ public class TootApiResult {
|
|||
public String json;
|
||||
public JSONObject token_info;
|
||||
public Response response;
|
||||
public WebSocket socket;
|
||||
|
||||
|
||||
public TootApiResult( String error ){
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public TootApiResult( Response response,JSONObject token_info,String json,JSONObject object ){
|
||||
TootApiResult( Response response, JSONObject token_info, String json, JSONObject object ){
|
||||
this.token_info = token_info;
|
||||
this.json = json;
|
||||
this.object = object;
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
public TootApiResult( LogCategory log, Response response, JSONObject token_info
|
||||
TootApiResult( LogCategory log, Response response, JSONObject token_info
|
||||
, String json, JSONArray array ){
|
||||
this.token_info = token_info;
|
||||
this.json = json;
|
||||
|
@ -39,6 +42,10 @@ public class TootApiResult {
|
|||
parseLinkHeader(log,response,array);
|
||||
}
|
||||
|
||||
TootApiResult( WebSocket socket ){
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
public String link_older; // より古いデータへのリンク
|
||||
public String link_newer; // より新しいデータへの
|
||||
|
||||
|
|
|
@ -11,4 +11,8 @@ public class TootGap {
|
|||
this.max_id = Long.toString(max_id);
|
||||
this.since_id = since_id;
|
||||
}
|
||||
public TootGap( long max_id, long since_id ){
|
||||
this.max_id = Long.toString(max_id);
|
||||
this.since_id = Long.toString(since_id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,6 @@ public class BucketList < E >
|
|||
this( DEFAULT_BUCKET_CAPACITY );
|
||||
}
|
||||
|
||||
|
||||
private static class Bucket < E > extends ArrayList< E > {
|
||||
int total_start;
|
||||
int total_end;
|
||||
|
@ -67,7 +66,7 @@ public class BucketList < E >
|
|||
return new BucketPos();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
private BucketPos findPos( BucketPos dst, int total_index ){
|
||||
|
||||
if( total_index < 0 || total_index >= mSize ){
|
||||
|
@ -139,7 +138,7 @@ public class BucketList < E >
|
|||
|
||||
BucketPos pos = findPos( pos_internal.get(), index );
|
||||
Bucket< E > bucket = groups.get( pos.group_index );
|
||||
|
||||
|
||||
// 挿入位置がバケツの先頭ではないか、バケツのサイズに問題がないなら
|
||||
if( pos.bucket_index > 0 || bucket.size() + c_size <= mStep ){
|
||||
// バケツの中に挿入する
|
||||
|
@ -150,22 +149,22 @@ public class BucketList < E >
|
|||
bucket.addAll( c );
|
||||
groups.add( pos.group_index, bucket );
|
||||
}
|
||||
|
||||
|
||||
updateIndex();
|
||||
return true;
|
||||
}
|
||||
|
||||
public E remove( int index ){
|
||||
BucketPos pos = findPos( pos_internal.get(), index );
|
||||
|
||||
|
||||
Bucket< E > bucket = groups.get( pos.group_index );
|
||||
|
||||
|
||||
E data = bucket.remove( pos.bucket_index );
|
||||
|
||||
|
||||
if( bucket.isEmpty() ){
|
||||
groups.remove( pos.group_index );
|
||||
}
|
||||
|
||||
|
||||
updateIndex();
|
||||
return data;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package jp.juggler.subwaytooter.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import jp.juggler.subwaytooter.api.entity.TootAccount;
|
||||
import jp.juggler.subwaytooter.api.entity.TootNotification;
|
||||
import jp.juggler.subwaytooter.api.entity.TootReport;
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus;
|
||||
|
||||
/**
|
||||
* Created by tateisu on 2017/05/10.
|
||||
*/
|
||||
|
||||
public class DuplicateMap {
|
||||
|
||||
HashSet< Long > set_status_id = new HashSet<>();
|
||||
HashSet< Long > set_notification_id = new HashSet<>();
|
||||
HashSet< Long > set_report_id = new HashSet<>();
|
||||
HashSet< Long > set_account_id = new HashSet<>();
|
||||
|
||||
public void clear(){
|
||||
set_status_id.clear();
|
||||
set_notification_id.clear();
|
||||
set_report_id.clear();
|
||||
set_account_id.clear();
|
||||
}
|
||||
|
||||
boolean isDuplicate(Object o){
|
||||
|
||||
if( o instanceof TootStatus ){
|
||||
if( set_status_id.contains( ( (TootStatus) o ).id ) ) return true;
|
||||
set_status_id.add( ( (TootStatus) o ).id );
|
||||
|
||||
}else if( o instanceof TootNotification ){
|
||||
if( set_notification_id.contains( ( (TootNotification) o ).id ) ) return true;
|
||||
set_notification_id.add( ( (TootNotification) o ).id );
|
||||
|
||||
}else if( o instanceof TootReport ){
|
||||
if( set_report_id.contains( ( (TootReport) o ).id ) ) return true;
|
||||
set_report_id.add( ( (TootReport) o ).id );
|
||||
|
||||
}else if( o instanceof TootAccount ){
|
||||
if( set_account_id.contains( ( (TootAccount) o ).id ) ) return true;
|
||||
set_account_id.add( ( (TootAccount) o ).id );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public ArrayList<Object> filterDuplicate( Collection< Object > src ){
|
||||
ArrayList< Object > list_new = new ArrayList<>();
|
||||
for( Object o : src ){
|
||||
if( isDuplicate( o )) continue;
|
||||
list_new.add( o );
|
||||
}
|
||||
|
||||
return list_new;
|
||||
}
|
||||
|
||||
}
|
|
@ -26,6 +26,40 @@
|
|||
|
||||
<View style="@style/setting_divider"/>
|
||||
|
||||
|
||||
<TextView
|
||||
style="@style/setting_row_label"
|
||||
android:text="@string/dont_use_streaming_api"
|
||||
/>
|
||||
|
||||
<LinearLayout style="@style/setting_row_form">
|
||||
|
||||
<Switch
|
||||
android:id="@+id/swDontUseStreaming"
|
||||
style="@style/setting_horizontal_stretch"
|
||||
android:gravity="center"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
<View style="@style/setting_divider"/>
|
||||
|
||||
|
||||
<TextView
|
||||
style="@style/setting_row_label"
|
||||
android:text="@string/dont_refresh_on_activity_resume"
|
||||
/>
|
||||
|
||||
<LinearLayout style="@style/setting_row_form">
|
||||
|
||||
<Switch
|
||||
android:id="@+id/swDontRefreshOnResume"
|
||||
style="@style/setting_horizontal_stretch"
|
||||
android:gravity="center"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
<View style="@style/setting_divider"/>
|
||||
|
||||
<TextView
|
||||
style="@style/setting_row_label"
|
||||
android:text="@string/dont_confirm_before_close_column"
|
||||
|
|
|
@ -302,5 +302,7 @@
|
|||
<string name="tab_divider_color">Tab divider color</string>
|
||||
<string name="help_translation">Help translation</string>
|
||||
<string name="open_status_from">Open status from</string>
|
||||
<string name="dont_use_streaming_api">Don\'t use streaming API</string>
|
||||
<string name="dont_refresh_on_activity_resume">Don\'t auto refresh when activity resumed</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -588,4 +588,6 @@
|
|||
<!--ステータスURLをアプリ内で開く時に、どのアカウントから閲覧するかで取得結果が異なる。ログインなしの擬似アカウントも選択肢に含まれる。-->
|
||||
<string name="open_status_from">ステータスを次のアカウントから開く</string>
|
||||
<string name="help_translation">翻訳に協力する</string>
|
||||
<string name="dont_use_streaming_api">ストリーミングAPIを使わない</string>
|
||||
<string name="dont_refresh_on_activity_resume">画面復帰時に自動ロードしない</string>
|
||||
</resources>
|
|
@ -298,4 +298,6 @@
|
|||
<string name="tab_divider_color">Tab divider color</string>
|
||||
<string name="open_status_from">Open status from</string>
|
||||
<string name="help_translation">Help translation</string>
|
||||
<string name="dont_use_streaming_api">Don\'t use streaming API</string>
|
||||
<string name="dont_refresh_on_activity_resume">Don\'t auto refresh when activity resumed</string>
|
||||
</resources>
|
||||
|
|
|
@ -6,7 +6,7 @@ buildscript {
|
|||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:2.3.0'
|
||||
classpath 'com.android.tools.build:gradle:2.3.1'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
|
Loading…
Reference in New Issue