SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/Column.java

3754 lines
117 KiB
Java

package jp.juggler.subwaytooter;
import android.annotation.SuppressLint;
import android.content.Context;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.view.View;
import android.widget.ListView;
import org.json.JSONException;
import org.json.JSONObject;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jp.juggler.subwaytooter.api.TootApiClient;
import jp.juggler.subwaytooter.api.TootApiResult;
import jp.juggler.subwaytooter.api.entity.TootAccount;
import jp.juggler.subwaytooter.api.entity.TootCard;
import jp.juggler.subwaytooter.api.entity.TootContext;
import jp.juggler.subwaytooter.api.entity.TootDomainBlock;
import jp.juggler.subwaytooter.api.entity.TootGap;
import jp.juggler.subwaytooter.api.entity.TootInstance;
import jp.juggler.subwaytooter.api.entity.TootList;
import jp.juggler.subwaytooter.api.entity.TootNotification;
import jp.juggler.subwaytooter.api.entity.TootRelationShip;
import jp.juggler.subwaytooter.api.entity.TootReport;
import jp.juggler.subwaytooter.api.entity.TootResults;
import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.api.TootParser;
import jp.juggler.subwaytooter.api.entity.TootStatusLike;
import jp.juggler.subwaytooter.api.entity.TootTag;
import jp.juggler.subwaytooter.api_msp.MSPClient;
import jp.juggler.subwaytooter.api_msp.entity.MSPToot;
import jp.juggler.subwaytooter.api_tootsearch.TSClient;
import jp.juggler.subwaytooter.api_tootsearch.entity.TSToot;
import jp.juggler.subwaytooter.table.AcctColor;
import jp.juggler.subwaytooter.table.AcctSet;
import jp.juggler.subwaytooter.table.HighlightWord;
import jp.juggler.subwaytooter.table.MutedApp;
import jp.juggler.subwaytooter.table.MutedWord;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.table.TagSet;
import jp.juggler.subwaytooter.table.UserRelation;
import jp.juggler.subwaytooter.util.BucketList;
import jp.juggler.subwaytooter.api.DuplicateMap;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.VersionString;
import jp.juggler.subwaytooter.util.WordTrieTree;
import jp.juggler.subwaytooter.view.MyListView;
import jp.juggler.subwaytooter.util.ScrollPosition;
import jp.juggler.subwaytooter.util.Utils;
@SuppressWarnings("WeakerAccess") public class Column implements StreamReader.Callback {
private static final LogCategory log = new LogCategory( "Column" );
interface Callback {
boolean isActivityStart();
}
private WeakReference< Callback > callback_ref;
private boolean isActivityStart(){
if( callback_ref == null ){
log.d( "isActivityStart: callback_ref is not set" );
return false;
}
Callback cb = callback_ref.get();
if( cb == null ){
log.d( "isActivityStart: callback was lost." );
return false;
}
return cb.isActivityStart();
}
private static Object getParamAt( Object[] params, int idx ){
if( params == null || idx >= params.length ){
throw new IndexOutOfBoundsException( "getParamAt idx=" + idx );
}
return params[ idx ];
}
public static final int READ_LIMIT = 80; // API側の上限が80です。ただし指定しても40しか返ってこないことが多い
private static final long LOOP_TIMEOUT = 10000L;
private static final int LOOP_READ_ENOUGH = 30; // フィルタ後のデータ数がコレ以上ならループを諦めます
private static final int RELATIONSHIP_LOAD_STEP = 40;
private static final int ACCT_DB_STEP = 100;
// ステータスのリストを返すAPI
private static final String PATH_HOME = "/api/v1/timelines/home?limit=" + READ_LIMIT;
private static final String PATH_LOCAL = "/api/v1/timelines/public?limit=" + READ_LIMIT + "&local=1";
private static final String PATH_FEDERATE = "/api/v1/timelines/public?limit=" + READ_LIMIT;
private static final String PATH_FAVOURITES = "/api/v1/favourites?limit=" + READ_LIMIT;
private static final String PATH_ACCOUNT_STATUSES = "/api/v1/accounts/%d/statuses?limit=" + READ_LIMIT; // 1:account_id
private static final String PATH_HASHTAG = "/api/v1/timelines/tag/%s?limit=" + READ_LIMIT; // 1: hashtag(url encoded)
private static final String PATH_LIST_TL = "/api/v1/timelines/list/%s?limit=" + READ_LIMIT;
// アカウントのリストを返すAPI
private static final String PATH_ACCOUNT_FOLLOWING = "/api/v1/accounts/%d/following?limit=" + READ_LIMIT; // 1:account_id
private static final String PATH_ACCOUNT_FOLLOWERS = "/api/v1/accounts/%d/followers?limit=" + READ_LIMIT; // 1:account_id
private static final String PATH_MUTES = "/api/v1/mutes?limit=" + READ_LIMIT; // 1:account_id
private static final String PATH_BLOCKS = "/api/v1/blocks?limit=" + READ_LIMIT; // 1:account_id
private static final String PATH_FOLLOW_REQUESTS = "/api/v1/follow_requests?limit=" + READ_LIMIT; // 1:account_id
private static final String PATH_BOOSTED_BY = "/api/v1/statuses/%s/reblogged_by?limit=" + READ_LIMIT; // 1:status_id
private static final String PATH_FAVOURITED_BY = "/api/v1/statuses/%s/favourited_by?limit=" + READ_LIMIT; // 1:status_id
private static final String PATH_LIST_MEMBER = "/api/v1/lists/%s/accounts?limit=" + READ_LIMIT;
// 他のリストを返すAPI
private static final String PATH_REPORTS = "/api/v1/reports?limit=" + READ_LIMIT;
private static final String PATH_NOTIFICATIONS = "/api/v1/notifications?limit=" + READ_LIMIT;
private static final String PATH_DOMAIN_BLOCK = "/api/v1/domain_blocks?limit=" + READ_LIMIT;
private static final String PATH_LIST_LIST = "/api/v1/lists?limit=" + READ_LIMIT;
// リストではなくオブジェクトを返すAPI
private static final String PATH_ACCOUNT = "/api/v1/accounts/%d"; // 1:account_id
private static final String PATH_STATUSES = "/api/v1/statuses/%d"; // 1:status_id
private static final String PATH_STATUSES_CONTEXT = "/api/v1/statuses/%d/context"; // 1:status_id
public static final String PATH_SEARCH = "/api/v1/search?q=%s"; // 1: query(urlencoded) , also, append "&resolve=1" if resolve non-local accounts
private static final String PATH_INSTANCE = "/api/v1/instance";
private static final String PATH_LIST_INFO = "/api/v1/lists/%s";
@Nullable String getStreamPath(){
switch( column_type ){
default:
return null;
case TYPE_HOME:
case TYPE_NOTIFICATIONS:
return "/api/v1/streaming/?stream=user";
case TYPE_LOCAL:
return "/api/v1/streaming/?stream=public:local";
case TYPE_FEDERATE:
return "/api/v1/streaming/?stream=public";
case TYPE_HASHTAG:
return "/api/v1/streaming/?stream=hashtag&tag=" + Uri.encode( hashtag ); /* タグ先頭の#を含まない */
case TYPE_LIST_TL:
return "/api/v1/streaming/?stream=list&list=" + Long.toString( profile_id );
}
}
public boolean isPublicStream(){
switch( column_type ){
default:
return false;
case TYPE_LOCAL:
case TYPE_FEDERATE:
case TYPE_HASHTAG:
return true;
}
}
static final String KEY_ACCOUNT_ROW_ID = "account_id";
static final String KEY_TYPE = "type";
static final String KEY_DONT_CLOSE = "dont_close";
private static final String KEY_WITH_ATTACHMENT = "with_attachment";
private static final String KEY_DONT_SHOW_BOOST = "dont_show_boost";
private static final String KEY_DONT_SHOW_FAVOURITE = "dont_show_favourite";
private static final String KEY_DONT_SHOW_FOLLOW = "dont_show_follow";
private static final String KEY_DONT_SHOW_REPLY = "dont_show_reply";
private static final String KEY_DONT_STREAMING = "dont_streaming";
private static final String KEY_DONT_AUTO_REFRESH = "dont_auto_refresh";
private static final String KEY_HIDE_MEDIA_DEFAULT = "hide_media_default";
private static final String KEY_ENABLE_SPEECH = "enable_speech";
private static final String KEY_REGEX_TEXT = "regex_text";
private static final String KEY_HEADER_BACKGROUND_COLOR = "header_background_color";
private static final String KEY_HEADER_TEXT_COLOR = "header_text_color";
private static final String KEY_COLUMN_BACKGROUND_COLOR = "column_background_color";
private static final String KEY_COLUMN_ACCT_TEXT_COLOR = "column_acct_text_color";
private static final String KEY_COLUMN_CONTENT_TEXT_COLOR = "column_content_text_color";
private static final String KEY_COLUMN_BACKGROUND_IMAGE = "column_background_image";
private static final String KEY_COLUMN_BACKGROUND_IMAGE_ALPHA = "column_background_image_alpha";
private static final String KEY_PROFILE_ID = "profile_id";
private static final String KEY_PROFILE_TAB = "tab";
private static final String KEY_STATUS_ID = "status_id";
private static final String KEY_HASHTAG = "hashtag";
private static final String KEY_SEARCH_QUERY = "search_query";
private static final String KEY_SEARCH_RESOLVE = "search_resolve";
private static final String KEY_INSTANCE_URI = "instance_uri";
static final String KEY_COLUMN_ACCESS = "column_access";
static final String KEY_COLUMN_ACCESS_COLOR = "column_access_color";
static final String KEY_COLUMN_ACCESS_COLOR_BG = "column_access_color_bg";
static final String KEY_COLUMN_NAME = "column_name";
static final String KEY_OLD_INDEX = "old_index";
static final int TYPE_HOME = 1;
public static final int TYPE_LOCAL = 2;
static final int TYPE_FEDERATE = 3;
public static final int TYPE_PROFILE = 4;
static final int TYPE_FAVOURITES = 5;
static final int TYPE_REPORTS = 6;
public static final int TYPE_NOTIFICATIONS = 7;
public static final int TYPE_CONVERSATION = 8;
public static final int TYPE_HASHTAG = 9;
static final int TYPE_SEARCH = 10;
static final int TYPE_MUTES = 11;
static final int TYPE_BLOCKS = 12;
static final int TYPE_FOLLOW_REQUESTS = 13;
static final int TYPE_BOOSTED_BY = 14;
static final int TYPE_FAVOURITED_BY = 15;
static final int TYPE_DOMAIN_BLOCKS = 16;
static final int TYPE_SEARCH_MSP = 17;
public static final int TYPE_INSTANCE_INFORMATION = 18;
static final int TYPE_LIST_LIST = 19;
static final int TYPE_LIST_TL = 20;
static final int TYPE_LIST_MEMBER = 21;
static final int TYPE_SEARCH_TS = 22;
@NonNull final Context context;
@NonNull private final AppState app_state;
@NonNull public final SavedAccount access_info;
public final int column_type;
boolean dont_close;
boolean with_attachment;
boolean dont_show_boost;
boolean dont_show_reply;
boolean dont_show_favourite; // 通知カラムのみ
boolean dont_show_follow; // 通知カラムのみ
boolean dont_streaming;
boolean dont_auto_refresh;
boolean hide_media_default;
boolean enable_speech;
String regex_text;
int header_bg_color;
int header_fg_color;
int column_bg_color;
int acct_color;
int content_color;
String column_bg_image;
float column_bg_image_alpha = 1f;
// プロフカラムではアカウントのID。リストカラムではリストのID
private long profile_id;
int profile_tab = TAB_STATUS;
static final int TAB_STATUS = 0;
static final int TAB_FOLLOWING = 1;
static final int TAB_FOLLOWERS = 2;
// プロフカラムでのアカウント情報
volatile TootAccount who_account;
// リストカラムでのリスト情報
volatile TootList list_info;
private long status_id;
private String hashtag;
String search_query;
boolean search_resolve;
String instance_uri;
// 「インスタンス情報」カラムに表示するインスタンス情報
// (SavedAccount中のインスタンス情報とは異なるので注意)
TootInstance instance_information;
ScrollPosition scroll_save;
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 );
switch( type ){
case TYPE_CONVERSATION:
case TYPE_BOOSTED_BY:
case TYPE_FAVOURITED_BY:
this.status_id = (Long) getParamAt( params, 0 );
break;
case TYPE_PROFILE:
case TYPE_LIST_TL:
case TYPE_LIST_MEMBER:
this.profile_id = (Long) getParamAt( params, 0 );
break;
case TYPE_HASHTAG:
this.hashtag = (String) getParamAt( params, 0 );
break;
case TYPE_SEARCH:
this.search_query = (String) getParamAt( params, 0 );
this.search_resolve = (Boolean) getParamAt( params, 1 );
break;
case TYPE_SEARCH_MSP:
case TYPE_SEARCH_TS:
this.search_query = (String) getParamAt( params, 0 );
break;
case TYPE_INSTANCE_INFORMATION:
this.instance_uri = (String) getParamAt( params, 0 );
break;
}
init();
}
void encodeJSON( JSONObject item, int old_index ) throws JSONException{
item.put( KEY_ACCOUNT_ROW_ID, access_info.db_id );
item.put( KEY_TYPE, column_type );
item.put( KEY_DONT_CLOSE, dont_close );
item.put( KEY_WITH_ATTACHMENT, with_attachment );
item.put( KEY_DONT_SHOW_BOOST, dont_show_boost );
item.put( KEY_DONT_SHOW_FOLLOW, dont_show_follow );
item.put( KEY_DONT_SHOW_FAVOURITE, dont_show_favourite );
item.put( KEY_DONT_SHOW_REPLY, dont_show_reply );
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_ENABLE_SPEECH, enable_speech );
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_ACCT_TEXT_COLOR, acct_color );
item.put( KEY_COLUMN_CONTENT_TEXT_COLOR, content_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:
case TYPE_FAVOURITED_BY:
item.put( KEY_STATUS_ID, status_id );
break;
case TYPE_PROFILE:
item.put( KEY_PROFILE_ID, profile_id );
item.put( KEY_PROFILE_TAB, profile_tab );
break;
case TYPE_LIST_MEMBER:
case TYPE_LIST_TL:
item.put( KEY_PROFILE_ID, profile_id );
break;
case TYPE_HASHTAG:
item.put( KEY_HASHTAG, hashtag );
break;
case TYPE_SEARCH:
item.put( KEY_SEARCH_QUERY, search_query );
item.put( KEY_SEARCH_RESOLVE, search_resolve );
break;
case TYPE_SEARCH_MSP:
case TYPE_SEARCH_TS:
item.put( KEY_SEARCH_QUERY, search_query );
break;
case TYPE_INSTANCE_INFORMATION:
item.put( KEY_INSTANCE_URI, instance_uri );
break;
}
// 以下は保存には必要ないが、カラムリスト画面で使う
AcctColor ac = AcctColor.load( access_info.acct );
item.put( KEY_COLUMN_ACCESS, AcctColor.hasNickname( ac ) ? ac.nickname : access_info.acct );
item.put( KEY_COLUMN_ACCESS_COLOR, AcctColor.hasColorForeground( ac ) ? ac.color_fg : 0 );
item.put( KEY_COLUMN_ACCESS_COLOR_BG, AcctColor.hasColorBackground( ac ) ? ac.color_bg : 0 );
item.put( KEY_COLUMN_NAME, getColumnName( true ) );
item.put( KEY_OLD_INDEX, old_index );
}
Column( @NonNull AppState app_state, JSONObject src ){
this.app_state = app_state;
this.context = app_state.context;
long account_db_id = Utils.optLongX( src, KEY_ACCOUNT_ROW_ID );
if( account_db_id >= 0 ){
SavedAccount ac = SavedAccount.loadAccount( context, account_db_id );
if( ac == null ) throw new RuntimeException( "missing account" );
this.access_info = ac;
}else{
this.access_info = SavedAccount.getNA();
}
this.column_type = src.optInt( KEY_TYPE );
this.dont_close = src.optBoolean( KEY_DONT_CLOSE );
this.with_attachment = src.optBoolean( KEY_WITH_ATTACHMENT );
this.dont_show_boost = src.optBoolean( KEY_DONT_SHOW_BOOST );
this.dont_show_follow = src.optBoolean( KEY_DONT_SHOW_FOLLOW );
this.dont_show_favourite = src.optBoolean( KEY_DONT_SHOW_FAVOURITE );
this.dont_show_reply = src.optBoolean( KEY_DONT_SHOW_REPLY );
this.dont_streaming = src.optBoolean( KEY_DONT_STREAMING );
this.dont_auto_refresh = src.optBoolean( KEY_DONT_AUTO_REFRESH );
this.hide_media_default = src.optBoolean( KEY_HIDE_MEDIA_DEFAULT );
this.enable_speech = src.optBoolean( KEY_ENABLE_SPEECH );
this.regex_text = Utils.optStringX( src, KEY_REGEX_TEXT );
this.header_bg_color = src.optInt( KEY_HEADER_BACKGROUND_COLOR );
this.header_fg_color = src.optInt( KEY_HEADER_TEXT_COLOR );
this.column_bg_color = src.optInt( KEY_COLUMN_BACKGROUND_COLOR );
this.acct_color = src.optInt( KEY_COLUMN_ACCT_TEXT_COLOR );
this.content_color = src.optInt( KEY_COLUMN_CONTENT_TEXT_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 );
switch( column_type ){
case TYPE_CONVERSATION:
case TYPE_BOOSTED_BY:
case TYPE_FAVOURITED_BY:
this.status_id = Utils.optLongX( src, KEY_STATUS_ID );
break;
case TYPE_PROFILE:
this.profile_id = Utils.optLongX( src, KEY_PROFILE_ID );
this.profile_tab = src.optInt( KEY_PROFILE_TAB );
break;
case TYPE_LIST_MEMBER:
case TYPE_LIST_TL:
this.profile_id = Utils.optLongX( src, KEY_PROFILE_ID );
break;
case TYPE_HASHTAG:
this.hashtag = src.optString( KEY_HASHTAG );
break;
case TYPE_SEARCH:
this.search_query = src.optString( KEY_SEARCH_QUERY );
this.search_resolve = src.optBoolean( KEY_SEARCH_RESOLVE, false );
break;
case TYPE_SEARCH_MSP:
case TYPE_SEARCH_TS:
this.search_query = src.optString( KEY_SEARCH_QUERY );
break;
case TYPE_INSTANCE_INFORMATION:
this.instance_uri = src.optString( KEY_INSTANCE_URI );
break;
}
init();
}
boolean isSameSpec( SavedAccount ai, int type, Object[] params ){
if( type != column_type || ! Utils.equalsNullable( ai.acct, access_info.acct ) )
return false;
switch( type ){
default:
return true;
case TYPE_PROFILE:
case TYPE_LIST_TL:
case TYPE_LIST_MEMBER:
try{
long who_id = (Long) getParamAt( params, 0 );
return who_id == this.profile_id;
}catch( Throwable ex ){
return false;
}
case TYPE_CONVERSATION:
case TYPE_BOOSTED_BY:
case TYPE_FAVOURITED_BY:
try{
long status_id = (Long) getParamAt( params, 0 );
return status_id == this.status_id;
}catch( Throwable ex ){
return false;
}
case TYPE_HASHTAG:
try{
String hashtag = (String) getParamAt( params, 0 );
return Utils.equalsNullable( hashtag, this.hashtag );
}catch( Throwable ex ){
return false;
}
case TYPE_SEARCH:
try{
String q = (String) getParamAt( params, 0 );
boolean r = (Boolean) getParamAt( params, 1 );
return Utils.equalsNullable( q, this.search_query )
&& r == this.search_resolve;
}catch( Throwable ex ){
return false;
}
case TYPE_SEARCH_MSP:
case TYPE_SEARCH_TS:
try{
String q = (String) getParamAt( params, 0 );
return Utils.equalsNullable( q, this.search_query );
}catch( Throwable ex ){
return false;
}
case TYPE_INSTANCE_INFORMATION:
try{
String q = (String) getParamAt( params, 0 );
return Utils.equalsNullable( q, this.instance_uri );
}catch( Throwable ex ){
return false;
}
}
}
final AtomicBoolean is_dispose = new AtomicBoolean();
void dispose(){
is_dispose.set( true );
stopStreaming();
for( ColumnViewHolder vh : _holder_list ){
try{
vh.getListView().setAdapter( null );
}catch( Throwable ignored ){
}
}
}
String getColumnName( boolean bLong ){
switch( column_type ){
default:
return getColumnTypeName( context, column_type );
case TYPE_PROFILE:
return context.getString( R.string.profile_of
, who_account != null ? AcctColor.getNickname( access_info.getFullAcct( who_account ) ) : Long.toString( profile_id )
);
case TYPE_LIST_MEMBER:
return context.getString( R.string.list_member_of
, list_info != null ? list_info.title : Long.toString( profile_id )
);
case TYPE_LIST_TL:
return context.getString( R.string.list_tl_of
, list_info != null ? list_info.title : Long.toString( profile_id )
);
case TYPE_CONVERSATION:
return context.getString( R.string.conversation_around, status_id );
case TYPE_HASHTAG:
return context.getString( R.string.hashtag_of, hashtag );
case TYPE_SEARCH:
if( bLong ){
return context.getString( R.string.search_of, search_query );
}else{
return getColumnTypeName( context, column_type );
}
case TYPE_SEARCH_MSP:
if( bLong ){
return context.getString( R.string.toot_search_msp_of, search_query );
}else{
return getColumnTypeName( context, column_type );
}
case TYPE_SEARCH_TS:
if( bLong ){
return context.getString( R.string.toot_search_ts_of, search_query );
}else{
return getColumnTypeName( context, column_type );
}
case TYPE_INSTANCE_INFORMATION:
if( bLong ){
return context.getString( R.string.instance_information_of, instance_uri );
}else{
return getColumnTypeName( context, column_type );
}
}
}
public static String getColumnTypeName( Context context, int type ){
switch( type ){
default:
return "?";
case TYPE_HOME:
return context.getString( R.string.home );
case TYPE_LOCAL:
return context.getString( R.string.local_timeline );
case TYPE_FEDERATE:
return context.getString( R.string.federate_timeline );
case TYPE_PROFILE:
return context.getString( R.string.profile );
case TYPE_FAVOURITES:
return context.getString( R.string.favourites );
case TYPE_REPORTS:
return context.getString( R.string.reports );
case TYPE_NOTIFICATIONS:
return context.getString( R.string.notifications );
case TYPE_CONVERSATION:
return context.getString( R.string.conversation );
case TYPE_BOOSTED_BY:
return context.getString( R.string.boosted_by );
case TYPE_FAVOURITED_BY:
return context.getString( R.string.favourited_by );
case TYPE_HASHTAG:
return context.getString( R.string.hashtag );
case TYPE_MUTES:
return context.getString( R.string.muted_users );
case TYPE_BLOCKS:
return context.getString( R.string.blocked_users );
case TYPE_DOMAIN_BLOCKS:
return context.getString( R.string.blocked_domains );
case TYPE_SEARCH:
return context.getString( R.string.search );
case TYPE_SEARCH_MSP:
return context.getString( R.string.toot_search_msp );
case TYPE_SEARCH_TS:
return context.getString( R.string.toot_search_ts );
case TYPE_INSTANCE_INFORMATION:
return context.getString( R.string.instance_information );
case TYPE_FOLLOW_REQUESTS:
return context.getString( R.string.follow_requests );
case TYPE_LIST_LIST:
return context.getString( R.string.lists );
case TYPE_LIST_MEMBER:
return context.getString( R.string.list_member );
case TYPE_LIST_TL:
return context.getString( R.string.list_timeline );
}
}
int getIconAttrId( int type ){
return getIconAttrId( access_info.acct, type );
}
static int getIconAttrId( String acct, int type ){
switch( type ){
default:
case TYPE_REPORTS:
return R.attr.ic_info;
case TYPE_HOME:
return R.attr.btn_home;
case TYPE_LOCAL:
return R.attr.btn_local_tl;
case TYPE_FEDERATE:
return R.attr.btn_federate_tl;
case TYPE_PROFILE:
return R.attr.btn_statuses;
case TYPE_FAVOURITES:
return SavedAccount.isNicoru( acct ) ? R.attr.ic_nicoru : R.attr.btn_favourite;
case TYPE_NOTIFICATIONS:
return R.attr.btn_notification;
case TYPE_CONVERSATION:
return R.attr.ic_conversation;
case TYPE_BOOSTED_BY:
return R.attr.btn_boost;
case TYPE_FAVOURITED_BY:
return SavedAccount.isNicoru( acct ) ? R.attr.ic_nicoru : R.attr.btn_favourite;
case TYPE_HASHTAG:
return R.attr.ic_hashtag;
case TYPE_MUTES:
return R.attr.ic_mute;
case TYPE_BLOCKS:
return R.attr.ic_block;
case TYPE_DOMAIN_BLOCKS:
return R.attr.ic_domain_block;
case TYPE_SEARCH:
case TYPE_SEARCH_MSP:
case TYPE_SEARCH_TS:
return R.attr.ic_search;
case TYPE_INSTANCE_INFORMATION:
return R.attr.ic_info;
case TYPE_FOLLOW_REQUESTS:
return R.attr.ic_account_add;
case TYPE_LIST_LIST:
return R.attr.ic_list_list;
case TYPE_LIST_MEMBER:
return R.attr.ic_list_member;
case TYPE_LIST_TL:
return R.attr.ic_list_tl;
}
}
boolean bFirstInitialized = false;
private void init(){
}
public interface StatusEntryCallback {
boolean onIterate( SavedAccount account, TootStatus status );
}
// ブーストやお気に入りの更新に使う。ステータスを列挙する。
public void findStatus( @NonNull String target_instance, long target_status_id, StatusEntryCallback callback ){
if( access_info.host.equalsIgnoreCase( target_instance ) ){
boolean bChanged = false;
for( Object data : list_data ){
//
if( data instanceof TootNotification ){
data = ( (TootNotification) data ).status;
}
//
if( data instanceof TootStatus ){
//
TootStatus status = (TootStatus) data;
if( target_status_id == status.id ){
if( callback.onIterate( access_info, status ) ){
bChanged = true;
}
}
//
TootStatus reblog = status.reblog;
if( reblog != null && target_status_id == reblog.id ){
if( callback.onIterate( access_info, reblog ) ){
bChanged = true;
}
}
}
}
if( bChanged ){
fireShowContent();
}
}
}
// ミュート、ブロックが成功した時に呼ばれる
// リストメンバーカラムでメンバーをリストから除去した時に呼ばれる
public void removeAccountInTimeline( SavedAccount target_account, long who_id ){
if( ! target_account.acct.equals( access_info.acct ) ) return;
ArrayList< Object > tmp_list = new ArrayList<>( list_data.size() );
for( Object o : list_data ){
if( o instanceof TootStatus ){
TootStatus item = (TootStatus) o;
if( ( item.account != null && item.account.id == who_id )
|| ( item.reblog != null && item.reblog.account != null && item.reblog.account.id == who_id )
){
continue;
}
}else if( o instanceof TootNotification ){
TootNotification item = (TootNotification) o;
if( item.account.id == who_id ) continue;
if( item.status != null ){
if( ( item.status.account != null && item.status.account.id == who_id ) )
continue;
if( item.status.reblog != null && item.status.reblog.account != null && item.status.reblog.account.id == who_id )
continue;
}
}else if( o instanceof TootAccount ){
TootAccount item = (TootAccount) o;
if( item.id == who_id ) continue;
}
tmp_list.add( o );
}
if( tmp_list.size() != list_data.size() ){
list_data.clear();
list_data.addAll( tmp_list );
fireShowContent();
}
}
// ミュート解除が成功した時に呼ばれる
public void removeFromMuteList( SavedAccount target_account, long who_id ){
if( ! target_account.acct.equals( access_info.acct ) ) return;
if( column_type != TYPE_MUTES ) return;
ArrayList< Object > tmp_list = new ArrayList<>( list_data.size() );
for( Object o : list_data ){
if( o instanceof TootAccount ){
TootAccount item = (TootAccount) o;
if( item.id == who_id ) continue;
}
tmp_list.add( o );
}
if( tmp_list.size() != list_data.size() ){
list_data.clear();
list_data.addAll( tmp_list );
fireShowContent();
}
}
// ブロック解除が成功したので、ブロックリストから削除する
public void removeFromBlockList( SavedAccount target_account, long who_id ){
if( ! target_account.acct.equals( access_info.acct ) ) return;
if( column_type != TYPE_BLOCKS ) return;
ArrayList< Object > tmp_list = new ArrayList<>( list_data.size() );
for( Object o : list_data ){
if( o instanceof TootAccount ){
TootAccount item = (TootAccount) o;
if( item.id == who_id ) continue;
}
tmp_list.add( o );
}
if( tmp_list.size() != list_data.size() ){
list_data.clear();
list_data.addAll( tmp_list );
fireShowContent();
}
}
public void removeFollowRequest( SavedAccount target_account, long who_id ){
if( ! target_account.acct.equals( access_info.acct ) ) return;
if( column_type == TYPE_FOLLOW_REQUESTS ){
ArrayList< Object > tmp_list = new ArrayList<>( list_data.size() );
for( Object o : list_data ){
if( o instanceof TootAccount ){
TootAccount item = (TootAccount) o;
if( item.id == who_id ) continue;
}
tmp_list.add( o );
}
if( tmp_list.size() != list_data.size() ){
list_data.clear();
list_data.addAll( tmp_list );
fireShowContent();
}
}else{
// 他のカラムでもフォロー状態の表示更新が必要
fireShowContent();
}
}
// 自分のステータスを削除した時に呼ばれる
public void removeStatus( SavedAccount target_account, long status_id ){
if( ! target_account.host.equals( access_info.host ) ) return;
ArrayList< Object > tmp_list = new ArrayList<>( list_data.size() );
for( Object o : list_data ){
if( o instanceof TootStatus ){
TootStatus item = (TootStatus) o;
if( item.id == status_id
|| ( item.reblog != null && item.reblog.id == status_id )
){
continue;
}
}
if( o instanceof TootNotification ){
TootNotification item = (TootNotification) o;
if( item.status != null ){
if( item.status.id == status_id ) continue;
if( item.status.reblog != null && item.status.reblog.id == status_id )
continue;
}
}
tmp_list.add( o );
}
if( tmp_list.size() != list_data.size() ){
list_data.clear();
list_data.addAll( tmp_list );
fireShowContent();
}
}
public void removeNotifications(){
cancelLastTask();
list_data.clear();
mRefreshLoadingError = null;
bRefreshLoading = false;
mInitialLoadingError = null;
bInitialLoading = false;
max_id = null;
since_id = null;
fireShowContent();
PollingWorker.queueNotificationCleared( context, access_info.db_id );
}
public void removeNotificationOne( SavedAccount target_account, TootNotification notification ){
if( column_type != TYPE_NOTIFICATIONS ) return;
if( ! access_info.acct.equals( target_account.acct ) ) return;
ArrayList< Object > tmp_list = new ArrayList<>( list_data.size() );
for( Object o : list_data ){
if( o instanceof TootNotification ){
TootNotification item = (TootNotification) o;
if( item.id == notification.id ) continue;
}
tmp_list.add( o );
}
if( tmp_list.size() != list_data.size() ){
list_data.clear();
list_data.addAll( tmp_list );
fireShowContent();
}
}
public void onMuteAppUpdated(){
ArrayList< Object > tmp_list = new ArrayList<>( list_data.size() );
HashSet< String > muted_app = MutedApp.getNameSet();
WordTrieTree muted_word = MutedWord.getNameSet();
for( Object o : list_data ){
if( o instanceof TootStatus ){
TootStatus item = (TootStatus) o;
if( item.checkMuted( muted_app, muted_word ) ){
continue;
}
}
if( o instanceof TootNotification ){
TootNotification item = (TootNotification) o;
TootStatus status = item.status;
if( status != null ){
if( status.checkMuted( muted_app, muted_word ) ){
continue;
}
}
}
tmp_list.add( o );
}
if( tmp_list.size() != list_data.size() ){
list_data.clear();
list_data.addAll( tmp_list );
fireShowContent();
}
}
public void onDomainBlockChanged( SavedAccount target_account, String domain, boolean bBlocked ){
if( ! target_account.host.equals( access_info.host ) ) return;
if( access_info.isPseudo() ) return;
if( column_type == TYPE_DOMAIN_BLOCKS ){
// ドメインブロック一覧を読み直す
startLoading();
return;
}
if( bBlocked ){
// ブロックしたのとドメイン部分が一致するアカウントからのステータスと通知をすべて除去する
Pattern reDomain = Pattern.compile( "[^@]+@\\Q" + domain + "\\E\\z", Pattern.CASE_INSENSITIVE );
ArrayList< Object > tmp_list = new ArrayList<>( list_data.size() );
for( Object o : list_data ){
if( o instanceof TootStatus ){
TootStatus item = (TootStatus) o;
if( item.account != null && reDomain.matcher( item.account.acct ).find() )
continue;
if( item.reblog != null && item.reblog.account != null && reDomain.matcher( item.reblog.account.acct ).find() )
continue;
}else if( o instanceof TootNotification ){
TootNotification item = (TootNotification) o;
if( item.account != null ){
if( reDomain.matcher( item.account.acct ).find() ) continue;
}
if( item.status != null ){
if( item.status.account != null && reDomain.matcher( item.status.account.acct ).find() )
continue;
if( item.status.reblog != null && item.status.reblog.account != null && reDomain.matcher( item.status.reblog.account.acct ).find() )
continue;
}
}
tmp_list.add( o );
}
if( tmp_list.size() != list_data.size() ){
list_data.clear();
list_data.addAll( tmp_list );
fireShowContent();
}
}
}
public void onListListUpdated( @NonNull SavedAccount account ){
if( column_type == TYPE_LIST_LIST && access_info.acct.equals( account.acct ) ){
startLoading();
ColumnViewHolder vh = getViewHolder();
if( vh != null ) vh.onListListUpdated();
}
}
public void onListMemberUpdated( SavedAccount account, long list_id, TootAccount who, boolean bAdd ){
if( column_type == TYPE_LIST_TL && access_info.acct.equals( account.acct ) && list_id == profile_id ){
if( ! bAdd ){
removeAccountInTimeline( account, who.id );
}
}else if( column_type == TYPE_LIST_MEMBER && access_info.acct.equals( account.acct ) && list_id == profile_id ){
if( ! bAdd ){
removeAccountInTimeline( account, who.id );
}
}
}
//////////////////////////////////////////////////////////////////////////////////////
// カラムを閉じた後のnotifyDataSetChangedのタイミングで、add/removeされる順序が期待通りにならないので
// 参照を1つだけ持つのではなく、リストを保持して先頭の要素を使うことにする
private final LinkedList< ColumnViewHolder > _holder_list = new LinkedList<>();
void addColumnViewHolder( @NonNull ColumnViewHolder cvh ){
// 現在のリストにあるなら削除する
removeColumnViewHolder( cvh );
// 最後に追加されたものが先頭にくるようにする
// 呼び出しの後に必ず追加されているようにする
_holder_list.addFirst( cvh );
}
void removeColumnViewHolder( @NonNull ColumnViewHolder cvh ){
for( Iterator< ColumnViewHolder > it = _holder_list.iterator() ; it.hasNext() ; ){
if( cvh == it.next() ) it.remove();
}
}
void removeColumnViewHolderByActivity( ActMain activity ){
for( Iterator< ColumnViewHolder > it = _holder_list.iterator() ; it.hasNext() ; ){
ColumnViewHolder cvh = it.next();
if( cvh != null && cvh.activity == activity ){
it.remove();
}
}
}
boolean hasMultipleViewHolder(){
return _holder_list.size() > 1;
}
ColumnViewHolder getViewHolder(){
if( is_dispose.get() ) return null;
// 複数のリスナがある場合、最も新しいものを返す
return _holder_list.isEmpty() ? null : _holder_list.getFirst();
}
void fireShowContent(){
if( ! Utils.isMainThread() ){
throw new RuntimeException( "fireShowColumnHeader: not on main thread." );
}
ColumnViewHolder holder = getViewHolder();
if( holder != null ) holder.showContent();
}
void fireShowColumnHeader(){
if( ! Utils.isMainThread() ){
throw new RuntimeException( "fireShowColumnHeader: not on main thread." );
}
ColumnViewHolder holder = getViewHolder();
if( holder != null ) holder.showColumnHeader();
}
void fireColumnColor(){
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;
private void cancelLastTask(){
if( last_task != null ){
last_task.cancel( true );
last_task = null;
//
bInitialLoading = false;
bRefreshLoading = false;
mInitialLoadingError = context.getString( R.string.cancelled );
//
}
}
boolean bInitialLoading;
boolean bRefreshLoading;
String mInitialLoadingError;
String mRefreshLoadingError;
String task_progress;
final BucketList< Object > list_data = new BucketList<>();
private final DuplicateMap duplicate_map = new DuplicateMap();
private boolean isFilterEnabled(){
return ( with_attachment
|| dont_show_boost
|| dont_show_favourite
|| dont_show_follow
|| dont_show_reply
|| ! TextUtils.isEmpty( regex_text )
);
}
private Pattern column_regex_filter;
private HashSet< String > muted_app;
private WordTrieTree muted_word;
private WordTrieTree highlight_trie;
private void initFilter(){
column_regex_filter = null;
if( ! TextUtils.isEmpty( regex_text ) ){
try{
column_regex_filter = Pattern.compile( regex_text );
}catch( Throwable ex ){
log.trace( ex );
}
}
muted_app = MutedApp.getNameSet();
muted_word = MutedWord.getNameSet();
highlight_trie = HighlightWord.getNameSet();
}
private boolean isFiltered( @NonNull TootStatus status ){
if( with_attachment ){
boolean hasMedia = status.reblog != null ? status.reblog.hasMedia() : status.hasMedia();
if( ! hasMedia ) 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;
}
private boolean isFiltered( MSPToot status ){
if( with_attachment ){
boolean hasMedia = status.hasMedia();
if( ! hasMedia ) return true;
}
if( column_regex_filter != null ){
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;
}
private boolean isFiltered( TSToot status ){
if( with_attachment ){
boolean hasMedia = status.hasMedia();
if( ! hasMedia ) return true;
}
if( column_regex_filter != null ){
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 );
}
}
}
@SuppressWarnings("ConstantConditions")
private void addWithFilter( ArrayList< Object > dst, MSPToot.List src ){
for( MSPToot status : src ){
if( ! isFiltered( status ) ){
dst.add( status );
}
}
}
@SuppressWarnings("ConstantConditions")
private void addWithFilter( ArrayList< Object > dst, TSToot.List src ){
for( TSToot status : src ){
if( ! isFiltered( status ) ){
dst.add( status );
}
}
}
private boolean isFiltered( TootNotification item ){
if( dont_show_favourite && TootNotification.TYPE_FAVOURITE.equals( item.type ) ){
log.d( "isFiltered: favourite notification filtered." );
return true;
}
if( dont_show_boost && TootNotification.TYPE_REBLOG.equals( item.type ) ){
log.d( "isFiltered: reblog notification filtered." );
return true;
}
if( dont_show_follow && TootNotification.TYPE_FOLLOW.equals( item.type ) ){
log.d( "isFiltered: follow notification filtered." );
return true;
}
TootStatus status = item.status;
if( status != null ){
if( status.checkMuted( muted_app, muted_word ) ){
log.d( "isFiltered: status muted." );
return true;
}
}
return false;
}
@SuppressWarnings("ConstantConditions")
private void addWithFilter( ArrayList< Object > dst, TootNotification.List src ){
for( TootNotification item : src ){
if( ! isFiltered( item ) ){
dst.add( item );
}
}
}
// @Nullable String parseMaxId( TootApiResult result ){
// if( result != null && result.link_older != null ){
// Matcher m = reMaxId.matcher( result.link_older );
// if( m.find() ) return m.group( 1 );
// }
// return null;
// }
void loadProfileAccount( TootApiClient client, boolean bForceReload ){
if( bForceReload || this.who_account == null ){
TootApiResult result = client.request( String.format( Locale.JAPAN, PATH_ACCOUNT, profile_id ) );
if( result != null && result.object != null ){
TootAccount data = TootAccount.parse( context, access_info, result.object );
if( data != null ){
this.who_account = data;
client.publishApiProgress( "" ); // カラムヘッダの再表示
}
}
}
}
void loadListInfo( TootApiClient client, boolean bForceReload ){
if( bForceReload || this.list_info == null ){
TootApiResult result = client.request( String.format( Locale.JAPAN, PATH_LIST_INFO, profile_id ) );
if( result != null && result.object != null ){
TootList data = TootList.parse( result.object );
if( data != null ){
this.list_info = data;
client.publishApiProgress( "" ); // カラムヘッダの再表示
}
}
}
}
@NonNull static final VersionString version_1_6 = new VersionString( "1.6" );
void startLoading(){
cancelLastTask();
stopStreaming();
initFilter();
mRefreshLoadingError = null;
mInitialLoadingError = null;
bFirstInitialized = true;
bInitialLoading = true;
bRefreshLoading = false;
max_id = null;
since_id = null;
duplicate_map.clear();
list_data.clear();
fireShowContent();
@SuppressLint("StaticFieldLeak") AsyncTask< Void, Void, TootApiResult > task = this.last_task = new AsyncTask< Void, Void, TootApiResult >() {
TootParser parser = new TootParser( context, access_info).setHighlightTrie( highlight_trie );
TootInstance instance_tmp;
TootApiResult getInstanceInformation( @NonNull TootApiClient client, @Nullable String instance_name ){
instance_tmp = null;
if( instance_name != null ) client.setInstance( instance_name );
TootApiResult result = client.request( "/api/v1/instance" );
if( result != null && result.object != null ){
instance_tmp = TootInstance.parse( result.object );
}
return result;
}
ArrayList< Object > list_pinned;
void getStatusesPinned( TootApiClient client, String path_base ){
TootApiResult result = client.request( path_base );
if( result != null && result.array != null ){
//
TootStatus.List src = new TootParser( context, access_info)
.setPinned( true )
.setHighlightTrie( highlight_trie )
.statusList( result.array );
for( TootStatus status : src ){
log.d( "pinned: %s %s", status.id, status.decoded_content );
}
list_pinned = new ArrayList<>( src.size() );
addWithFilter( list_pinned, src );
// pinned tootにはページングの概念はない
}
log.d( "getStatusesPinned: list size=%s", list_pinned == null ? - 1 : list_pinned.size() );
}
ArrayList< Object > list_tmp;
TootApiResult getStatuses( TootApiClient client, String path_base ){
long time_start = SystemClock.elapsedRealtime();
TootApiResult result = client.request( path_base );
if( result != null && result.array != null ){
saveRange( result, true, true );
//
TootStatus.List src = parser.statusList( result.array );
list_tmp = new ArrayList<>( src.size() );
addWithFilter( list_tmp, src );
//
char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' );
for( ; ; ){
if( client.isCancelled() ){
log.d( "loading-statuses: cancelled." );
break;
}
if( ! isFilterEnabled() ){
log.d( "loading-statuses: isFiltered is false." );
break;
}
if( max_id == null ){
log.d( "loading-statuses: max_id is null." );
break;
}
if( list_tmp.size() >= LOOP_READ_ENOUGH ){
log.d( "loading-statuses: read enough data." );
break;
}
if( src.isEmpty() ){
log.d( "loading-statuses: previous response is empty." );
break;
}
if( SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){
log.d( "loading-statuses: timeout." );
break;
}
String path = path_base + delimiter + "max_id=" + max_id;
TootApiResult result2 = client.request( path );
if( result2 == null || result2.array == null ){
log.d( "loading-statuses: error or cancelled." );
break;
}
src = parser.statusList( result2.array );
addWithFilter( list_tmp, src );
if( ! saveRangeEnd( result2 ) ){
log.d( "loading-statuses: missing range info." );
break;
}
}
}
return result;
}
TootApiResult parseAccountList( TootApiClient client, String path_base ){
TootApiResult result = client.request( path_base );
if( result != null ){
saveRange( result, true, true );
list_tmp = new ArrayList<>();
list_tmp.addAll( TootAccount.parseList( context, access_info, result.array ) );
}
return result;
}
TootApiResult parseDomainList( TootApiClient client, String path_base ){
TootApiResult result = client.request( path_base );
if( result != null ){
saveRange( result, true, true );
list_tmp = new ArrayList<>();
list_tmp.addAll( TootDomainBlock.parseList( result.array ) );
}
return result;
}
TootApiResult parseReports( TootApiClient client, String path_base ){
TootApiResult result = client.request( path_base );
if( result != null ){
saveRange( result, true, true );
list_tmp = new ArrayList<>();
list_tmp.addAll( TootReport.parseList( result.array ) );
}
return result;
}
TootApiResult parseListList( TootApiClient client, String path_base ){
TootApiResult result = client.request( path_base );
if( result != null ){
saveRange( result, true, true );
list_tmp = new ArrayList<>();
TootList.List l = TootList.parseList( result.array );
Collections.sort( l );
list_tmp.addAll( l );
}
return result;
}
TootApiResult parseNotifications( TootApiClient client, String path_base ){
long time_start = SystemClock.elapsedRealtime();
TootApiResult result = client.request( path_base );
if( result != null && result.array != null ){
saveRange( result, true, true );
//
TootNotification.List src = parser.notificationList( result.array );
list_tmp = new ArrayList<>( src.size() );
addWithFilter( list_tmp, src );
//
if( ! src.isEmpty() ){
PollingWorker.injectData( context, access_info.db_id, src );
}
//
char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' );
for( ; ; ){
if( client.isCancelled() ){
log.d( "loading-notifications: cancelled." );
break;
}
if( ! isFilterEnabled() ){
log.d( "loading-notifications: isFiltered is false." );
break;
}
if( max_id == null ){
log.d( "loading-notifications: max_id is null." );
break;
}
if( list_tmp.size() >= LOOP_READ_ENOUGH ){
log.d( "loading-notifications: read enough data." );
break;
}
if( src.isEmpty() ){
log.d( "loading-notifications: previous response is empty." );
break;
}
if( SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){
log.d( "loading-notifications: timeout." );
break;
}
String path = path_base + delimiter + "max_id=" + max_id;
TootApiResult result2 = client.request( path );
if( result2 == null || result2.array == null ){
log.d( "loading-notifications: error or cancelled." );
break;
}
src = parser.notificationList( result2.array );
addWithFilter( list_tmp, src );
if( ! saveRangeEnd( result2 ) ){
log.d( "loading-notifications: missing range info." );
break;
}
}
}
return result;
}
@Override protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( context, new TootApiClient.Callback() {
@Override public boolean isApiCancelled(){
return isCancelled() || is_dispose.get();
}
@Override public void publishApiProgress( @NonNull final String s ){
Utils.runOnMainThread( new Runnable() {
@Override public void run(){
if( isCancelled() ) return;
task_progress = s;
fireShowContent();
}
} );
}
} );
client.setAccount( access_info );
try{
TootApiResult result;
String q;
switch( column_type ){
default:
case TYPE_HOME:
return getStatuses( client, PATH_HOME );
case TYPE_LOCAL:
return getStatuses( client, PATH_LOCAL );
case TYPE_FEDERATE:
return getStatuses( client, PATH_FEDERATE );
case TYPE_PROFILE:
loadProfileAccount( client, true );
switch( profile_tab ){
default:
case TAB_STATUS:
TootInstance instance = access_info.getInstance();
if( access_info.isPseudo() || instance == null ){
TootApiResult r2 = getInstanceInformation( client, null );
if( instance_tmp != null ){
instance = instance_tmp;
access_info.setInstance( instance_tmp );
}
if( access_info.isPseudo() ) return r2;
}
{
String s = String.format( Locale.JAPAN, PATH_ACCOUNT_STATUSES, profile_id );
if( with_attachment ) s = s + "&only_media=1";
if( instance != null && instance.isEnoughVersion( version_1_6 ) ){
getStatusesPinned( client, s + "&pinned=1" );
}
return getStatuses( client, s );
}
case TAB_FOLLOWING:
return parseAccountList( client,
String.format( Locale.JAPAN, PATH_ACCOUNT_FOLLOWING, profile_id ) );
case TAB_FOLLOWERS:
return parseAccountList( client,
String.format( Locale.JAPAN, PATH_ACCOUNT_FOLLOWERS, profile_id ) );
}
case TYPE_MUTES:
return parseAccountList( client, PATH_MUTES );
case TYPE_BLOCKS:
return parseAccountList( client, PATH_BLOCKS );
case TYPE_DOMAIN_BLOCKS:
return parseDomainList( client, PATH_DOMAIN_BLOCK );
case TYPE_LIST_LIST:
return parseListList( client, PATH_LIST_LIST );
case TYPE_LIST_TL:
loadListInfo( client, true );
return getStatuses( client, String.format( Locale.JAPAN, PATH_LIST_TL, profile_id ) );
case TYPE_LIST_MEMBER:
loadListInfo( client, true );
return parseAccountList( client, String.format( Locale.JAPAN, PATH_LIST_MEMBER, profile_id ) );
case TYPE_FOLLOW_REQUESTS:
return parseAccountList( client, PATH_FOLLOW_REQUESTS );
case TYPE_FAVOURITES:
return getStatuses( client, PATH_FAVOURITES );
case TYPE_HASHTAG:
return getStatuses( client,
String.format( Locale.JAPAN, PATH_HASHTAG, Uri.encode( hashtag ) ) );
case TYPE_REPORTS:
return parseReports( client, PATH_REPORTS );
case TYPE_NOTIFICATIONS:
return parseNotifications( client, PATH_NOTIFICATIONS );
case TYPE_BOOSTED_BY:
return parseAccountList( client, String.format( Locale.JAPAN, PATH_BOOSTED_BY, status_id ) );
case TYPE_FAVOURITED_BY:
return parseAccountList( client, String.format( Locale.JAPAN, PATH_FAVOURITED_BY, status_id ) );
case TYPE_CONVERSATION:
// 指定された発言そのもの
result = client.request(
String.format( Locale.JAPAN, PATH_STATUSES, status_id ) );
if( result == null || result.object == null ) return result;
TootStatus target_status = parser.status( result.object );
if( target_status == null ){
return new TootApiResult( "TootStatus parse failed." );
}
target_status.conversation_main = true;
// 前後の会話
result = client.request(
String.format( Locale.JAPAN, PATH_STATUSES_CONTEXT, status_id ) );
if( result == null || result.object == null ) return result;
// 一つのリストにまとめる
TootContext conversation_context = parser.context( result.object );
if( conversation_context != null ){
list_tmp = new ArrayList<>( 1 + conversation_context.ancestors.size() + conversation_context.descendants.size() );
if( conversation_context.ancestors != null )
addWithFilter( list_tmp, conversation_context.ancestors );
list_tmp.add( target_status );
if( conversation_context.descendants != null )
addWithFilter( list_tmp, conversation_context.descendants );
}else{
Utils.showToast( context, true, "TootContext parse failed." );
list_tmp = new ArrayList<>( 1 );
list_tmp.add( target_status );
}
// カードを取得する
for( Object o : list_tmp ){
if( o instanceof TootStatus ){
TootStatus status = (TootStatus) o;
TootApiResult r2 = client.request( "/api/v1/statuses/" + status.id + "/card" );
if( r2 != null && r2.object != null ){
status.card = TootCard.parse( r2.object );
}
}
}
//
return result;
case TYPE_SEARCH:
if( access_info.isPseudo() ){
// 1.5.0rc からマストドンの検索APIは認証を要求するようになった
return new TootApiResult( context.getString( R.string.search_is_not_available_on_pseudo_account ) );
}
String path = String.format( Locale.JAPAN, PATH_SEARCH, Uri.encode( search_query ) );
if( search_resolve ) path = path + "&resolve=1";
result = client.request( path );
if( result == null || result.object == null ) return result;
TootResults tmp = parser.results( result.object );
if( tmp != null ){
list_tmp = new ArrayList<>();
list_tmp.addAll( tmp.hashtags );
list_tmp.addAll( tmp.accounts );
list_tmp.addAll( tmp.statuses );
}
return result;
case TYPE_SEARCH_MSP:
max_id = "";
q = search_query.trim();
if( q.length() <= 0 ){
list_tmp = new ArrayList<>();
result = new TootApiResult();
}else{
result = MSPClient.search( context, search_query, max_id, new MSPClient.Callback() {
@Override
public boolean isApiCancelled(){
return isCancelled() || is_dispose.get();
}
@Override
public void publishApiProgress( final String s ){
Utils.runOnMainThread( new Runnable() {
@Override
public void run(){
if( isCancelled() ) return;
task_progress = s;
fireShowContent();
}
} );
}
} );
if( result != null && result.array != null ){
// max_id の更新
max_id = MSPClient.getMaxId( result.array, max_id );
// リストデータの用意
MSPToot.List search_result = MSPToot.parseList(parser, result.array );
if( search_result != null ){
list_tmp = new ArrayList<>();
addWithFilter( list_tmp, search_result );
}
}
}
return result;
case TYPE_SEARCH_TS:
max_id = "0";
q = search_query.trim();
if( TextUtils.isEmpty( q ) ){
list_tmp = new ArrayList<>();
result = new TootApiResult();
}else{
result = TSClient.search( context, search_query, max_id, new TSClient.Callback() {
@Override public boolean isApiCancelled(){
return isCancelled() || is_dispose.get();
}
@Override public void publishApiProgress( final String s ){
Utils.runOnMainThread( new Runnable() {
@Override public void run(){
if( isCancelled() ) return;
task_progress = s;
fireShowContent();
}
} );
}
} );
if( result != null ){
if( result.object != null ){
// max_id の更新
max_id = TSClient.getMaxId( result.object, max_id );
// リストデータの用意
TSToot.List search_result = TSToot.parseList( parser, result.object );
list_tmp = new ArrayList<>();
addWithFilter( list_tmp, search_result );
if( search_result.isEmpty() ){
log.d( "search result is empty. %s", result.json );
}
}else{
log.d( "search error." );
}
}
}
return result;
case TYPE_INSTANCE_INFORMATION:{
result = getInstanceInformation( client, instance_uri );
if( instance_tmp != null ){
instance_information = instance_tmp;
}
return result;
}
}
}finally
{
try{
updateRelation( client, list_tmp, who_account );
}catch( Throwable ex ){
log.trace( ex );
}
}
}
@Override
protected void onCancelled( TootApiResult result ){
onPostExecute( null );
}
@Override
protected void onPostExecute( TootApiResult result ){
if( is_dispose.get() ) return;
if( isCancelled() || result == null ){
return;
}
bInitialLoading = false;
last_task = null;
if( result.error != null ){
Column.this.mInitialLoadingError = result.error;
}else{
list_data.clear();
if( list_tmp != null ){
if( list_pinned != null && ! list_pinned.isEmpty() ){
ArrayList< Object > list_new = duplicate_map.filterDuplicate( list_pinned );
list_data.addAll( list_new );
}
ArrayList< Object > list_new = duplicate_map.filterDuplicate( list_tmp );
list_data.addAll( list_new );
}
resumeStreaming( false );
}
fireShowContent();
// 初期ロードの直後は先頭に移動する
try{
ColumnViewHolder holder = getViewHolder();
if( holder != null ) holder.getListView().setSelection( 0 );
}catch( Throwable ignored ){
}
}
};
task.executeOnExecutor( App1.task_executor );
}
public static final Pattern reMaxId = Pattern.compile( "[&?]max_id=(\\d+)" ); // より古いデータの取得に使う
private static final Pattern reSinceId = Pattern.compile( "[&?]since_id=(\\d+)" ); // より新しいデータの取得に使う
private String max_id;
private String since_id;
// int scroll_hack;
private void saveRange( TootApiResult result, boolean bBottom, boolean bTop ){
if( result != null ){
if( bBottom ){
if( result.link_older == null ){
max_id = null;
}else{
Matcher m = reMaxId.matcher( result.link_older );
if( m.find() ) max_id = m.group( 1 );
}
}
if( bTop && result.link_newer != null ){
Matcher m = reSinceId.matcher( result.link_newer );
if( m.find() ) since_id = m.group( 1 );
}
}
}
private boolean saveRangeEnd( TootApiResult result ){
if( result != null ){
if( result.link_older == null ){
max_id = null;
}else{
Matcher m = reMaxId.matcher( result.link_older );
if( m.find() ){
max_id = m.group( 1 );
return true;
}
}
}
return false;
}
private String addRange( boolean bBottom, String path ){
char delimiter = ( - 1 != path.indexOf( '?' ) ? '&' : '?' );
if( bBottom ){
if( max_id != null ) return path + delimiter + "max_id=" + max_id;
}else{
if( since_id != null ) return path + delimiter + "since_id=" + since_id;
}
return path;
}
private class UpdateRelationEnv {
final HashSet< Long > who_set = new HashSet<>();
final HashSet< String > acct_set = new HashSet<>();
final HashSet< String > tag_set = new HashSet<>();
void add( @Nullable TootAccount a ){
if( a == null ) return;
who_set.add( a.id );
acct_set.add( "@" + access_info.getFullAcct( a ) );
//
add( a.moved );
}
void add( @Nullable TootStatus s ){
if( s == null ) return;
add( s.account );
add( s.reblog );
//
if( s.tags != null ){
for( TootTag tag : s.tags ){
tag_set.add( tag.name );
}
}
}
void add( @Nullable TootNotification n ){
if( n == null ) return;
add( n.account );
add( n.status );
}
void update( @NonNull TootApiClient client ){
// アカウントIDの集合からRelationshipを取得してデータベースに記録する
int size = who_set.size();
if( size > 0 ){
long[] who_list = new long[ size ];
{
int n = 0;
for( Long l : who_set ){
who_list[ n++ ] = l;
}
}
long now = System.currentTimeMillis();
int n = 0;
while( n < size ){
StringBuilder sb = new StringBuilder();
sb.append( "/api/v1/accounts/relationships" );
for( int i = 0 ; i < RELATIONSHIP_LOAD_STEP ; ++ i ){
if( n >= size ) break;
sb.append( i == 0 ? '?' : '&' );
sb.append( "id[]=" );
sb.append( Long.toString( who_list[ n++ ] ) );
}
TootApiResult result = client.request( sb.toString() );
if( result == null ){
// cancelled.
break;
}else if( result.array != null ){
TootRelationShip.List list = TootRelationShip.parseList( result.array );
UserRelation.saveList( now, access_info.db_id, list );
}
}
log.d( "updateRelation: update %d relations.", n );
}
// 出現したacctをデータベースに記録する
size = acct_set.size();
if( size > 0 ){
String[] acct_list = new String[ size ];
{
int n = 0;
for( String l : acct_set ){
acct_list[ n++ ] = l;
}
}
long now = System.currentTimeMillis();
int n = 0;
while( n < size ){
int length = size - n;
if( length > ACCT_DB_STEP ) length = ACCT_DB_STEP;
AcctSet.saveList( now, acct_list, n, length );
n += length;
}
log.d( "updateRelation: update %d acct.", n );
}
// 出現したタグをデータベースに記録する
size = tag_set.size();
if( size > 0 ){
String[] tag_list = new String[ size ];
{
int n = 0;
for( String l : tag_set ){
tag_list[ n++ ] = l;
}
}
long now = System.currentTimeMillis();
int n = 0;
while( n < size ){
int length = size - n;
if( length > ACCT_DB_STEP ) length = ACCT_DB_STEP;
TagSet.saveList( now, tag_list, n, length );
n += length;
}
log.d( "updateRelation: update %d tag.", n );
}
}
}
//
private void updateRelation(
@NonNull TootApiClient client
, @Nullable ArrayList< Object > list_tmp
, @Nullable TootAccount who
){
if( access_info.isPseudo() ) return;
UpdateRelationEnv env = new UpdateRelationEnv();
env.add( who );
if( list_tmp != null ){
for( Object o : list_tmp ){
if( o instanceof TootAccount ){
env.add( (TootAccount) o );
}else if( o instanceof TootStatus ){
env.add( (TootStatus) o );
}else if( o instanceof TootNotification ){
env.add( (TootNotification) o );
}
}
}
env.update( client );
}
void startRefreshForPost( long status_id, int refresh_after_toot ){
switch( column_type ){
case TYPE_HOME:
case TYPE_LOCAL:
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;
}
}
private boolean bRefreshingTop;
void startRefresh( final boolean bSilent, final boolean bBottom, final long status_id, final int refresh_after_toot ){
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 );
}
}
if( ! bBottom ){
bRefreshingTop = true;
stopStreaming();
}
bRefreshLoading = true;
mRefreshLoadingError = null;
@SuppressLint("StaticFieldLeak") AsyncTask< Void, Void, TootApiResult > task = this.last_task = new AsyncTask< Void, Void, TootApiResult >() {
TootParser parser = new TootParser( context, access_info).setHighlightTrie( highlight_trie );
TootApiResult getAccountList( TootApiClient client, String path_base ){
long time_start = SystemClock.elapsedRealtime();
char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' );
String last_since_id = since_id;
TootApiResult result = client.request( addRange( bBottom, path_base ) );
if( result != null && result.array != null ){
saveRange( result, bBottom, ! bBottom );
list_tmp = new ArrayList<>();
TootAccount.List src = TootAccount.parseList( context, access_info, result.array );
list_tmp.addAll( src );
if( ! bBottom ){
for( ; ; ){
if( isCancelled() ){
log.d( "refresh-account-top: cancelled." );
break;
}
// max_id だけを指定した場合、必ずlimit個のデータが帰ってくるとは限らない
// 直前のデータが0個なら終了とみなすしかなさそう
if( src.isEmpty() ){
log.d( "refresh-account-top: previous size == 0." );
break;
}
// 隙間の最新のステータスIDは取得データ末尾のステータスIDである
String max_id = Long.toString( src.get( src.size() - 1 ).id );
if( SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){
log.d( "refresh-account-top: timeout. make gap." );
// タイムアウト
// 隙間ができるかもしれない。後ほど手動で試してもらうしかない
TootGap gap = new TootGap( max_id, last_since_id );
list_tmp.add( gap );
break;
}
String path = path_base + delimiter + "max_id=" + max_id + "&since_id=" + last_since_id;
TootApiResult result2 = client.request( path );
if( result2 == null || result2.array == null ){
log.d( "refresh-account-top: error or cancelled. make gap." );
// エラー
// 隙間ができるかもしれない。後ほど手動で試してもらうしかない
TootGap gap = new TootGap( max_id, last_since_id );
list_tmp.add( gap );
break;
}
src = TootAccount.parseList( context, access_info, result2.array );
list_tmp.addAll( src );
}
}
}
return result;
}
TootApiResult getDomainList( TootApiClient client, String path_base ){
long time_start = SystemClock.elapsedRealtime();
char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' );
String last_since_id = since_id;
TootApiResult result = client.request( addRange( bBottom, path_base ) );
if( result != null && result.array != null ){
saveRange( result, bBottom, ! bBottom );
list_tmp = new ArrayList<>();
TootDomainBlock.List src = TootDomainBlock.parseList( result.array );
list_tmp.addAll( src );
if( ! bBottom ){
for( ; ; ){
if( isCancelled() ){
log.d( "refresh-domain-top: cancelled." );
break;
}
// max_id だけを指定した場合、必ずlimit個のデータが帰ってくるとは限らない
// 直前のデータが0個なら終了とみなすしかなさそう
if( src.isEmpty() ){
log.d( "refresh-domain-top: previous size == 0." );
break;
}
if( SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){
log.d( "refresh-domain-top: timeout." );
// タイムアウト
break;
}
String path = path_base + delimiter + "max_id=" + max_id + "&since_id=" + last_since_id;
TootApiResult result2 = client.request( path );
if( result2 == null || result2.array == null ){
log.d( "refresh-domain-top: error or cancelled." );
// エラー
break;
}
src = TootDomainBlock.parseList( result2.array );
list_tmp.addAll( src );
}
}
}
return result;
}
TootApiResult getListList( TootApiClient client, String path_base ){
long time_start = SystemClock.elapsedRealtime();
char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' );
String last_since_id = since_id;
TootApiResult result = client.request( addRange( bBottom, path_base ) );
if( result != null && result.array != null ){
saveRange( result, bBottom, ! bBottom );
list_tmp = new ArrayList<>();
TootList.List src = TootList.parseList( result.array );
list_tmp.addAll( src );
if( ! bBottom ){
for( ; ; ){
if( isCancelled() ){
log.d( "refresh-list-top: cancelled." );
break;
}
// max_id だけを指定した場合、必ずlimit個のデータが帰ってくるとは限らない
// 直前のデータが0個なら終了とみなすしかなさそう
if( src.isEmpty() ){
log.d( "refresh-list-top: previous size == 0." );
break;
}
// 隙間の最新のステータスIDは取得データ末尾のステータスIDである
String max_id = Long.toString( src.get( src.size() - 1 ).id );
if( SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){
log.d( "refresh-list-top: timeout. make gap." );
// タイムアウト
// 隙間ができるかもしれない。後ほど手動で試してもらうしかない
TootGap gap = new TootGap( max_id, last_since_id );
list_tmp.add( gap );
break;
}
String path = path_base + delimiter + "max_id=" + max_id + "&since_id=" + last_since_id;
TootApiResult result2 = client.request( path );
if( result2 == null || result2.array == null ){
log.d( "refresh-list-top: timeout. error or retry. make gap." );
// エラー
// 隙間ができるかもしれない。後ほど手動で試してもらうしかない
TootGap gap = new TootGap( max_id, last_since_id );
list_tmp.add( gap );
break;
}
src = TootList.parseList( result2.array );
list_tmp.addAll( src );
}
}
}
return result;
}
TootApiResult getReportList( TootApiClient client, String path_base ){
long time_start = SystemClock.elapsedRealtime();
char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' );
String last_since_id = since_id;
TootApiResult result = client.request( addRange( bBottom, path_base ) );
if( result != null && result.array != null ){
saveRange( result, bBottom, ! bBottom );
list_tmp = new ArrayList<>();
TootReport.List src = TootReport.parseList( result.array );
list_tmp.addAll( src );
if( ! bBottom ){
for( ; ; ){
if( isCancelled() ){
log.d( "refresh-report-top: cancelled." );
break;
}
// max_id だけを指定した場合、必ずlimit個のデータが帰ってくるとは限らない
// 直前のデータが0個なら終了とみなすしかなさそう
if( src.isEmpty() ){
log.d( "refresh-report-top: previous size == 0." );
break;
}
// 隙間の最新のステータスIDは取得データ末尾のステータスIDである
String max_id = Long.toString( src.get( src.size() - 1 ).id );
if( SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){
log.d( "refresh-report-top: timeout. make gap." );
// タイムアウト
// 隙間ができるかもしれない。後ほど手動で試してもらうしかない
TootGap gap = new TootGap( max_id, last_since_id );
list_tmp.add( gap );
break;
}
String path = path_base + delimiter + "max_id=" + max_id + "&since_id=" + last_since_id;
TootApiResult result2 = client.request( path );
if( result2 == null || result2.array == null ){
log.d( "refresh-report-top: timeout. error or retry. make gap." );
// エラー
// 隙間ができるかもしれない。後ほど手動で試してもらうしかない
TootGap gap = new TootGap( max_id, last_since_id );
list_tmp.add( gap );
break;
}
src = TootReport.parseList( result2.array );
list_tmp.addAll( src );
}
}
}
return result;
}
TootApiResult getNotificationList( TootApiClient client, String path_base ){
long time_start = SystemClock.elapsedRealtime();
char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' );
String last_since_id = since_id;
TootApiResult result = client.request( addRange( bBottom, path_base ) );
if( result != null && result.array != null ){
saveRange( result, bBottom, ! bBottom );
list_tmp = new ArrayList<>();
TootNotification.List src = parser.notificationList( result.array );
addWithFilter( list_tmp, src );
if( ! bBottom ){
if( ! src.isEmpty() ){
PollingWorker.injectData( context, access_info.db_id, src );
}
for( ; ; ){
if( isCancelled() ){
log.d( "refresh-notification-top: cancelled." );
break;
}
// max_id だけを指定した場合、必ずlimit個のデータが帰ってくるとは限らない
// 直前のデータが0個なら終了とみなすしかなさそう
if( src.isEmpty() ){
log.d( "refresh-notification-top: previous size == 0." );
break;
}
// 隙間の最新のステータスIDは取得データ末尾のステータスIDである
String max_id = Long.toString( src.get( src.size() - 1 ).id );
if( SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){
log.d( "refresh-notification-top: timeout. make gap." );
// タイムアウト
// 隙間ができるかもしれない。後ほど手動で試してもらうしかない
TootGap gap = new TootGap( max_id, last_since_id );
list_tmp.add( gap );
break;
}
String path = path_base + delimiter + "max_id=" + max_id + "&since_id=" + last_since_id;
TootApiResult result2 = client.request( path );
if( result2 == null || result2.array == null ){
log.d( "refresh-notification-top: error or cancelled. make gap." );
// エラー
// 隙間ができるかもしれない。後ほど手動で試してもらうしかない
TootGap gap = new TootGap( max_id, last_since_id );
list_tmp.add( gap );
break;
}
src = parser.notificationList( result2.array );
if( ! src.isEmpty() ){
addWithFilter( list_tmp, src );
PollingWorker.injectData( context, access_info.db_id, src );
}
}
}else{
for( ; ; ){
if( isCancelled() ){
log.d( "refresh-notification-bottom: cancelled." );
break;
}
// bottomの場合、フィルタなしなら繰り返さない
if( ! isFilterEnabled() ){
log.d( "refresh-notification-bottom: isFiltered is false." );
break;
}
// max_id だけを指定した場合、必ずlimit個のデータが帰ってくるとは限らない
// 直前のデータが0個なら終了とみなすしかなさそう
if( src.isEmpty() ){
log.d( "refresh-notification-bottom: previous size == 0." );
break;
}
// 十分読んだらそれで終了
if( list_tmp.size() >= LOOP_READ_ENOUGH ){
log.d( "refresh-notification-bottom: read enough data." );
break;
}
if( SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){
// タイムアウト
log.d( "refresh-notification-bottom: loop timeout." );
break;
}
String path = path_base + delimiter + "max_id=" + max_id;
TootApiResult result2 = client.request( path );
if( result2 == null || result2.array == null ){
log.d( "refresh-notification-bottom: error or cancelled." );
break;
}
src = parser.notificationList( result2.array );
addWithFilter( list_tmp, src );
if( ! saveRangeEnd( result2 ) ){
log.d( "refresh-notification-bottom: saveRangeEnd failed." );
break;
}
}
}
}
return result;
}
ArrayList< Object > list_tmp;
TootApiResult getStatusList( TootApiClient client, String path_base ){
long time_start = SystemClock.elapsedRealtime();
char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' );
final String last_since_id = since_id;
TootApiResult result = client.request( addRange( bBottom, path_base ) );
if( result != null && result.array != null ){
saveRange( result, bBottom, ! bBottom );
TootStatus.List src = parser.statusList( result.array );
list_tmp = new ArrayList<>();
addWithFilter( list_tmp, src );
if( bBottom ){
for( ; ; ){
if( isCancelled() ){
log.d( "refresh-status-bottom: cancelled." );
break;
}
// bottomの場合、フィルタなしなら繰り返さない
if( ! isFilterEnabled() ){
log.d( "refresh-status-bottom: isFiltered is false." );
break;
}
// max_id だけを指定した場合、必ずlimit個のデータが帰ってくるとは限らない
// 直前のデータが0個なら終了とみなすしかなさそう
if( src.isEmpty() ){
log.d( "refresh-status-bottom: previous size == 0." );
break;
}
// 十分読んだらそれで終了
if( list_tmp.size() >= LOOP_READ_ENOUGH ){
log.d( "refresh-status-bottom: read enough data." );
break;
}
if( SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){
// タイムアウト
log.d( "refresh-status-bottom: loop timeout." );
break;
}
String path = path_base + delimiter + "max_id=" + max_id;
TootApiResult result2 = client.request( path );
if( result2 == null || result2.array == null ){
log.d( "refresh-status-bottom: error or cancelled." );
break;
}
src = parser.statusList( result2.array );
addWithFilter( list_tmp, src );
if( ! saveRangeEnd( result2 ) ){
log.d( "refresh-status-bottom: saveRangeEnd failed." );
break;
}
}
}else{
for( ; ; ){
if( isCancelled() ){
log.d( "refresh-status-top: cancelled." );
break;
}
// 頭の方を読む時は隙間を減らすため、フィルタの有無に関係なく繰り返しを行う
// max_id だけを指定した場合、必ずlimit個のデータが帰ってくるとは限らない
// 直前のデータが0個なら終了とみなすしかなさそう
if( src.isEmpty() ){
log.d( "refresh-status-top: previous size == 0." );
break;
}
// 隙間の最新のステータスIDは取得データ末尾のステータスIDである
String max_id = Long.toString( src.get( src.size() - 1 ).id );
if( list_tmp.size() >= LOOP_READ_ENOUGH ){
log.d( "refresh-status-top: read enough. make gap." );
// 隙間ができるかもしれない。後ほど手動で試してもらうしかない
TootGap gap = new TootGap( max_id, last_since_id );
list_tmp.add( gap );
break;
}
if( SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){
log.d( "refresh-status-top: timeout. make gap." );
// タイムアウト
// 隙間ができるかもしれない。後ほど手動で試してもらうしかない
TootGap gap = new TootGap( max_id, last_since_id );
list_tmp.add( gap );
break;
}
String path = path_base + delimiter + "max_id=" + max_id + "&since_id=" + last_since_id;
TootApiResult result2 = client.request( path );
if( result2 == null || result2.array == null ){
log.d( "refresh-status-top: error or cancelled. make gap." );
// エラー
// 隙間ができるかもしれない。後ほど手動で試してもらうしかない
TootGap gap = new TootGap( max_id, last_since_id );
list_tmp.add( gap );
break;
}
src = parser.statusList( result2.array );
addWithFilter( list_tmp, src );
}
}
}
return result;
}
@Override
protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( context, new TootApiClient.Callback() {
@Override public boolean isApiCancelled(){
return isCancelled() || is_dispose.get();
}
@Override public void publishApiProgress( @NonNull final String s ){
Utils.runOnMainThread( new Runnable() {
@Override public void run(){
if( isCancelled() ) return;
task_progress = s;
fireShowContent();
}
} );
}
} );
client.setAccount( access_info );
try{
switch( column_type ){
default:
case TYPE_HOME:
return getStatusList( client, PATH_HOME );
case TYPE_LOCAL:
return getStatusList( client, PATH_LOCAL );
case TYPE_FEDERATE:
return getStatusList( client, PATH_FEDERATE );
case TYPE_FAVOURITES:
return getStatusList( client, PATH_FAVOURITES );
case TYPE_REPORTS:
return getReportList( client, PATH_REPORTS );
case TYPE_NOTIFICATIONS:
return getNotificationList( client, PATH_NOTIFICATIONS );
case TYPE_BOOSTED_BY:
return getAccountList( client, String.format( Locale.JAPAN, PATH_BOOSTED_BY, status_id ) );
case TYPE_FAVOURITED_BY:
return getAccountList( client, String.format( Locale.JAPAN, PATH_FAVOURITED_BY, status_id ) );
case TYPE_PROFILE:
loadProfileAccount( client, false );
switch( profile_tab ){
default:
case TAB_STATUS:
if( access_info.isPseudo() ){
return client.request( PATH_INSTANCE );
}else{
String s = String.format( Locale.JAPAN, PATH_ACCOUNT_STATUSES, profile_id );
if( with_attachment ) s = s + "&only_media=1";
return getStatusList( client, s );
}
case TAB_FOLLOWING:
return getAccountList( client,
String.format( Locale.JAPAN, PATH_ACCOUNT_FOLLOWING, profile_id ) );
case TAB_FOLLOWERS:
return getAccountList( client,
String.format( Locale.JAPAN, PATH_ACCOUNT_FOLLOWERS, profile_id ) );
}
case TYPE_LIST_LIST:
return getListList( client, PATH_LIST_LIST );
case TYPE_LIST_TL:
loadListInfo( client, false );
return getStatusList( client, String.format( Locale.JAPAN, PATH_LIST_TL, profile_id ) );
case TYPE_LIST_MEMBER:
loadListInfo( client, false );
return getAccountList( client, String.format( Locale.JAPAN, PATH_LIST_MEMBER, profile_id ) );
case TYPE_MUTES:
return getAccountList( client, PATH_MUTES );
case TYPE_BLOCKS:
return getAccountList( client, PATH_BLOCKS );
case TYPE_DOMAIN_BLOCKS:
return getDomainList( client, PATH_DOMAIN_BLOCK );
case TYPE_FOLLOW_REQUESTS:
return getAccountList( client, PATH_FOLLOW_REQUESTS );
case TYPE_HASHTAG:
return getStatusList( client,
String.format( Locale.JAPAN, PATH_HASHTAG, Uri.encode( hashtag ) ) );
case TYPE_SEARCH_MSP:
if( ! bBottom ){
return new TootApiResult( "head of list." );
}else{
TootApiResult result;
String q = search_query.trim();
if( q.length() <= 0 ){
list_tmp = new ArrayList<>();
result = new TootApiResult( context.getString( R.string.end_of_list ) );
}else{
result = MSPClient.search( context, search_query, max_id, new MSPClient.Callback() {
@Override
public boolean isApiCancelled(){
return isCancelled() || is_dispose.get();
}
@Override
public void publishApiProgress( final String s ){
Utils.runOnMainThread( new Runnable() {
@Override
public void run(){
if( isCancelled() ) return;
task_progress = s;
fireShowContent();
}
} );
}
} );
if( result != null && result.array != null ){
// max_id の更新
max_id = MSPClient.getMaxId( result.array, max_id );
// リストデータの用意
MSPToot.List search_result = MSPToot.parseList( parser, result.array );
if( search_result != null ){
list_tmp = new ArrayList<>();
addWithFilter( list_tmp, search_result );
}
}
}
return result;
}
case TYPE_SEARCH_TS:
if( ! bBottom ){
return new TootApiResult( "head of list." );
}else{
TootApiResult result;
String q = search_query.trim();
if( q.length() <= 0 || TextUtils.isEmpty( max_id ) ){
list_tmp = new ArrayList<>();
result = new TootApiResult( context.getString( R.string.end_of_list ) );
}else{
result = TSClient.search( context, search_query, max_id, new TSClient.Callback() {
@Override public boolean isApiCancelled(){
return isCancelled() || is_dispose.get();
}
@Override public void publishApiProgress( final String s ){
Utils.runOnMainThread( new Runnable() {
@Override public void run(){
if( isCancelled() ) return;
task_progress = s;
fireShowContent();
}
} );
}
} );
if( result != null && result.object != null ){
// max_id の更新
max_id = TSClient.getMaxId( result.object, max_id );
// リストデータの用意
TSToot.List search_result = TSToot.parseList( parser, result.object );
list_tmp = new ArrayList<>();
addWithFilter( list_tmp, search_result );
}
}
return result;
}
}
}finally{
try{
updateRelation( client, list_tmp, who_account );
}catch( Throwable ex ){
log.trace( ex );
}
}
}
@Override
protected void onCancelled( TootApiResult result ){
onPostExecute( null );
}
@Override
protected void onPostExecute( TootApiResult result ){
if( is_dispose.get() ) return;
if( isCancelled() || result == null ){
return;
}
try{
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;
}
ArrayList< Object > list_new = duplicate_map.filterDuplicate( list_tmp );
if( list_new.isEmpty() ){
fireShowContent();
return;
}
// 事前にスクロール位置を覚えておく
ScrollPosition sp = null;
ColumnViewHolder holder = getViewHolder();
if( holder != null ){
sp = holder.getScrollPosition();
}
if( bBottom ){
list_data.addAll( list_new );
fireShowContent();
if( sp != null ){
holder.setScrollPosition( sp, 20f );
}
}else{
for( Object o : list_new ){
if( o instanceof TootStatusLike){
TootStatusLike s = (TootStatusLike) o;
if( s.highlight_sound != null ){
App1.sound( s.highlight_sound );
break;
}
}
}
int status_index = - 1;
for( int i = 0, ie = list_new.size() ; i < ie ; ++ i ){
Object o = list_new.get( i );
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 );
}
}
}
}finally{
if( ! bBottom ){
bRefreshingTop = false;
resumeStreaming( false );
}
}
}
};
task.executeOnExecutor( App1.task_executor );
}
void startGap( final TootGap gap ){
if( gap == null ){
Utils.showToast( context, true, "gap is null" );
return;
}
if( last_task != null ){
Utils.showToast( context, true, R.string.column_is_busy );
return;
}
ColumnViewHolder holder = getViewHolder();
if( holder != null ){
holder.getRefreshLayout().setRefreshing( true );
}
bRefreshLoading = true;
mRefreshLoadingError = null;
@SuppressLint("StaticFieldLeak")
AsyncTask< Void, Void, TootApiResult > task = this.last_task = new AsyncTask< Void, Void, TootApiResult >() {
String max_id = gap.max_id;
final String since_id = gap.since_id;
ArrayList< Object > list_tmp;
TootParser parser = new TootParser( context, access_info).setHighlightTrie( highlight_trie );
TootApiResult getAccountList( TootApiClient client, String path_base ){
long time_start = SystemClock.elapsedRealtime();
char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' );
list_tmp = new ArrayList<>();
TootApiResult result = null;
for( ; ; ){
if( isCancelled() ){
log.d( "gap-account: cancelled." );
break;
}
if( result != null && SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){
log.d( "gap-account: timeout. make gap." );
// タイムアウト
// 隙間が残る
list_tmp.add( new TootGap( max_id, since_id ) );
break;
}
String path = path_base + delimiter + "max_id=" + max_id + "&since_id=" + since_id;
TootApiResult r2 = client.request( path );
if( r2 == null || r2.array == null ){
log.d( "gap-account: error timeout. make gap." );
if( result == null ) result = r2;
// 隙間が残る
list_tmp.add( new TootGap( max_id, since_id ) );
break;
}
result = r2;
TootAccount.List src = TootAccount.parseList( context, access_info, r2.array );
if( src.isEmpty() ){
log.d( "gap-account: empty." );
break;
}
list_tmp.addAll( src );
// 隙間の最新のステータスIDは取得データ末尾のステータスIDである
max_id = Long.toString( src.get( src.size() - 1 ).id );
}
return result;
}
TootApiResult getReportList( TootApiClient client, String path_base ){
long time_start = SystemClock.elapsedRealtime();
char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' );
list_tmp = new ArrayList<>();
TootApiResult result = null;
for( ; ; ){
if( isCancelled() ){
log.d( "gap-report: cancelled." );
break;
}
if( result != null && SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){
log.d( "gap-report: timeout. make gap." );
// タイムアウト
// 隙間が残る
list_tmp.add( new TootGap( max_id, since_id ) );
break;
}
String path = path_base + delimiter + "max_id=" + max_id + "&since_id=" + since_id;
TootApiResult r2 = client.request( path );
if( r2 == null || r2.array == null ){
log.d( "gap-report: error or cancelled. make gap." );
if( result == null ) result = r2;
// 隙間が残る
list_tmp.add( new TootGap( max_id, since_id ) );
break;
}
result = r2;
TootReport.List src = TootReport.parseList( r2.array );
if( src.isEmpty() ){
log.d( "gap-report: empty." );
// コレ以上取得する必要はない
break;
}
list_tmp.addAll( src );
// 隙間の最新のステータスIDは取得データ末尾のステータスIDである
max_id = Long.toString( src.get( src.size() - 1 ).id );
}
return result;
}
TootApiResult getNotificationList( TootApiClient client, String path_base ){
long time_start = SystemClock.elapsedRealtime();
char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' );
list_tmp = new ArrayList<>();
TootApiResult result = null;
for( ; ; ){
if( isCancelled() ){
log.d( "gap-notification: cancelled." );
break;
}
if( result != null && SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){
log.d( "gap-notification: timeout. make gap." );
// タイムアウト
// 隙間が残る
list_tmp.add( new TootGap( max_id, since_id ) );
break;
}
String path = path_base + delimiter + "max_id=" + max_id + "&since_id=" + since_id;
TootApiResult r2 = client.request( path );
if( r2 == null || r2.array == null ){
// エラー
log.d( "gap-notification: error or response. make gap." );
if( result == null ) result = r2;
// 隙間が残る
list_tmp.add( new TootGap( max_id, since_id ) );
break;
}
result = r2;
TootNotification.List src = parser.notificationList( r2.array );
if( src.isEmpty() ){
log.d( "gap-notification: empty." );
break;
}
// 隙間の最新のステータスIDは取得データ末尾のステータスIDである
max_id = Long.toString( src.get( src.size() - 1 ).id );
addWithFilter( list_tmp, src );
PollingWorker.injectData( context, access_info.db_id, src );
}
return result;
}
TootApiResult getStatusList( TootApiClient client, String path_base ){
long time_start = SystemClock.elapsedRealtime();
char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' );
list_tmp = new ArrayList<>();
TootApiResult result = null;
for( ; ; ){
if( isCancelled() ){
log.d( "gap-statuses: cancelled." );
break;
}
if( result != null && SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){
log.d( "gap-statuses: timeout." );
// タイムアウト
// 隙間が残る
list_tmp.add( new TootGap( max_id, since_id ) );
break;
}
String path = path_base + delimiter + "max_id=" + max_id + "&since_id=" + since_id;
TootApiResult r2 = client.request( path );
if( r2 == null || r2.array == null ){
log.d( "gap-statuses: error or cancelled. make gap." );
// 成功データがない場合だけ、今回のエラーを返すようにする
if( result == null ) result = r2;
// 隙間が残る
list_tmp.add( new TootGap( max_id, since_id ) );
break;
}
// 成功した場合はそれを返したい
result = r2;
TootStatus.List src = parser.statusList( r2.array );
if( src.size() == 0 ){
// 直前の取得でカラのデータが帰ってきたら終了
log.d( "gap-statuses: empty." );
break;
}
// 隙間の最新のステータスIDは取得データ末尾のステータスIDである
max_id = Long.toString( src.get( src.size() - 1 ).id );
addWithFilter( list_tmp, src );
}
return result;
}
@Override protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( context, new TootApiClient.Callback() {
@Override public boolean isApiCancelled(){
return isCancelled() || is_dispose.get();
}
@Override public void publishApiProgress( @NonNull final String s ){
Utils.runOnMainThread( new Runnable() {
@Override
public void run(){
if( isCancelled() ) return;
task_progress = s;
fireShowContent();
}
} );
}
} );
client.setAccount( access_info );
try{
switch( column_type ){
default:
case TYPE_HOME:
return getStatusList( client, PATH_HOME );
case TYPE_LOCAL:
return getStatusList( client, PATH_LOCAL );
case TYPE_FEDERATE:
return getStatusList( client, PATH_FEDERATE );
case TYPE_LIST_TL:
return getStatusList( client, String.format( Locale.JAPAN, PATH_LIST_TL, profile_id ) );
case TYPE_FAVOURITES:
return getStatusList( client, PATH_FAVOURITES );
case TYPE_REPORTS:
return getReportList( client, PATH_REPORTS );
case TYPE_NOTIFICATIONS:
return getNotificationList( client, PATH_NOTIFICATIONS );
case TYPE_HASHTAG:
return getStatusList( client,
String.format( Locale.JAPAN, PATH_HASHTAG, Uri.encode( hashtag ) ) );
case TYPE_BOOSTED_BY:
return getAccountList( client, String.format( Locale.JAPAN, PATH_BOOSTED_BY, status_id ) );
case TYPE_FAVOURITED_BY:
return getAccountList( client, String.format( Locale.JAPAN, PATH_FAVOURITED_BY, status_id ) );
case TYPE_MUTES:
return getAccountList( client, PATH_MUTES );
case TYPE_BLOCKS:
return getAccountList( client, PATH_BLOCKS );
// ドメインブロックはギャップ表示がもともとない
//case TYPE_DOMAIN_BLOCKS:
case TYPE_FOLLOW_REQUESTS:
return getAccountList( client, PATH_FOLLOW_REQUESTS );
case TYPE_PROFILE:
switch( profile_tab ){
default:
case TAB_STATUS:
if( access_info.isPseudo() ){
return client.request( PATH_INSTANCE );
}else{
String s = String.format( Locale.JAPAN, PATH_ACCOUNT_STATUSES, profile_id );
if( with_attachment ) s = s + "&only_media=1";
return getStatusList( client, s );
}
case TAB_FOLLOWING:
return getAccountList( client,
String.format( Locale.JAPAN, PATH_ACCOUNT_FOLLOWING, profile_id ) );
case TAB_FOLLOWERS:
return getAccountList( client,
String.format( Locale.JAPAN, PATH_ACCOUNT_FOLLOWERS, profile_id ) );
}
}
}finally{
try{
updateRelation( client, list_tmp, who_account );
}catch( Throwable ex ){
log.trace( ex );
}
}
}
@Override
protected void onCancelled( TootApiResult result ){
onPostExecute( null );
}
@Override
protected void onPostExecute( TootApiResult result ){
if( is_dispose.get() ) return;
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 ){
fireShowContent();
return;
}
// 0個でもギャップを消すために以下の処理を続ける
int position = list_data.indexOf( gap );
if( position == - 1 ){
log.d( "gap is not found.." );
return;
}
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( holder, restore_idx );
}catch( IndexOutOfBoundsException ex ){
restore_idx = position;
try{
restore_y = getItemTop( holder, restore_idx );
}catch( IndexOutOfBoundsException ex2 ){
restore_idx = - 1;
}
}
}
int added = list_new.size(); // may 0
list_data.remove( position );
list_data.addAll( position, list_new );
fireShowContent();
if( holder != null ){
//noinspection StatementWithEmptyBody
if( restore_idx >= 0 ){
setItemTop( holder, restore_idx + added - 1, restore_y );
}else{
// ギャップが画面内にない場合、何もしない
}
}else{
if( scroll_save != null ){
scroll_save.pos += added - 1;
}
}
}
};
task.executeOnExecutor( App1.task_executor );
}
private static final int heightSpec = View.MeasureSpec.makeMeasureSpec( 0, View.MeasureSpec.UNSPECIFIED );
private static int getListItemHeight( ListView listView, int idx ){
int item_width = listView.getWidth() - listView.getPaddingLeft() - listView.getPaddingRight();
int widthSpec = View.MeasureSpec.makeMeasureSpec( item_width, View.MeasureSpec.EXACTLY );
View childView = listView.getAdapter().getView( idx, null, listView );
childView.measure( widthSpec, heightSpec );
return childView.getMeasuredHeight();
}
// 特定の要素が特定の位置に来るようにスクロール位置を調整する
private void setItemTop( @NonNull ColumnViewHolder holder, int idx, int y ){
MyListView listView = holder.getListView();
boolean hasHeader = holder.getHeaderView() != null;
if( hasHeader ){
// Adapter中から見たpositionとListViewから見たpositionにズレができる
idx = idx + 1;
}
while( y > 0 && idx > 0 ){
-- idx;
y -= getListItemHeight( listView, idx );
y -= listView.getDividerHeight();
}
listView.setSelectionFromTop( idx, y );
}
private int getItemTop( @NonNull ColumnViewHolder holder, int idx ){
MyListView listView = holder.getListView();
boolean hasHeader = holder.getHeaderView() != null;
if( hasHeader ){
// Adapter中から見たpositionとListViewから見たpositionにズレができる
idx = idx + 1;
}
int vs = listView.getFirstVisiblePosition();
int ve = listView.getLastVisiblePosition();
if( idx < vs || ve < idx ){
throw new IndexOutOfBoundsException( "not in visible range" );
}
int child_idx = idx - vs;
return listView.getChildAt( child_idx ).getTop();
}
////////////////////////////////////////////////////////////////////////
// Streaming
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" );
}
// ListViewの表示更新が追いつかないとスクロール位置が崩れるので
// 一定時間より短期間にはデータ更新しないようにする
private long last_show_stream_data;
private final LinkedList< Object > stream_data_queue = new LinkedList<>();
private 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() ){
PollingWorker.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 ) );
}
}
ColumnViewHolder holder = getViewHolder();
// 事前にスクロール位置を覚えておく
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( holder, restore_idx );
}catch( IndexOutOfBoundsException ex ){
restore_idx = - 1;
restore_y = 0;
}
}
}
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." );
}
}
for( Object o : list_new ){
if( o instanceof TootStatusLike){
TootStatusLike s = (TootStatusLike) o;
if( s.highlight_sound != null ){
App1.sound( s.highlight_sound );
break;
}
}
}
list_data.addAll( 0, list_new );
fireShowContent();
int added = list_new.size();
if( holder != null ){
//noinspection StatementWithEmptyBody
if( sp.pos == 0 && sp.top == 0 ){
// スクロール位置が先頭なら先頭のまま
}else if( restore_idx >= 0 ){
//
setItemTop( holder, restore_idx + added, restore_y );
}else{
// ギャップが画面内にない場合、何もしない
}
}else{
if( scroll_save == null || ( scroll_save.pos == 0 || scroll_save.top == 0 ) ){
// スクロール位置が先頭なら先頭のまま
}else{
// 現在の要素が表示され続けるようにしたい
scroll_save.pos += added;
}
}
}
};
@Override public void onStreamingMessage( String event_type, Object o ){
if( is_dispose.get() ) return;
if( "delete".equals( event_type ) ){
if( o instanceof Long ){
removeStatus( access_info, (Long) o );
}
}else{
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 != null && status.account.acct.indexOf( '@' ) != - 1 )
return;
if( isFiltered( status ) ) return;
if( this.enable_speech ){
App1.getAppState( context ).addSpeech( status.reblog != null ? status.reblog : status );
}
}
stream_data_queue.addFirst( o );
proc_stream_data.run();
}
}
void onStart( Callback callback ){
this.callback_ref = new WeakReference<>( callback );
// 破棄されたカラムなら何もしない
if( is_dispose.get() ){
log.d( "onStart: column was disposed." );
return;
}
// 未初期化なら何もしない
if( ! bFirstInitialized ){
log.d( "onStart: column is not initialized." );
return;
}
// 初期ロード中なら何もしない
if( bInitialLoading ){
log.d( "onStart: column is in initial loading." );
return;
}
// 始端リフレッシュの最中だった
// リフレッシュ終了時に自動でストリーミング開始するはず
if( bRefreshingTop ){
log.d( "onStart: bRefreshingTop is true." );
return;
}
if( ! bRefreshLoading
&& canAutoRefresh()
&& ! App1.getAppState( context ).pref.getBoolean( Pref.KEY_DONT_REFRESH_ON_RESUME, false )
&& ! dont_auto_refresh
){
// リフレッシュしてからストリーミング開始
log.d( "onStart: start auto refresh." );
startRefresh( true, false, - 1L, - 1 );
}else if( isSearchColumn() ){
// 検索カラムはリフレッシュもストリーミングもないが、表示開始のタイミングでリストの再描画を行いたい
fireShowContent();
}else{
// ギャップつきでストリーミング開始
log.d( "onStart: start streaming with gap." );
resumeStreaming( true );
}
}
// カラム設定に正規表現フィルタを含めるなら真
public boolean canStatusFilter(){
switch( column_type ){
case TYPE_REPORTS:
case TYPE_MUTES:
case TYPE_BLOCKS:
case TYPE_DOMAIN_BLOCKS:
case TYPE_FOLLOW_REQUESTS:
case TYPE_BOOSTED_BY:
case TYPE_FAVOURITED_BY:
case TYPE_INSTANCE_INFORMATION:
case TYPE_LIST_LIST:
case TYPE_LIST_MEMBER:
return false;
default:
return true;
}
}
// カラム設定に「すべての画像を隠す」ボタンを含めるなら真
boolean canNSFWDefault(){
return canStatusFilter();
}
// カラム設定に「ブーストを表示しない」ボタンを含めるなら真
public boolean canFilterBoost(){
switch( column_type ){
case TYPE_HOME:
case TYPE_PROFILE:
case TYPE_NOTIFICATIONS:
case TYPE_LIST_TL:
return true;
default:
return false;
}
}
// カラム設定に「変身を表示しない」ボタンを含めるなら真
public boolean canFilterReply(){
switch( column_type ){
case TYPE_HOME:
case TYPE_PROFILE:
case TYPE_LIST_TL:
return true;
default:
return false;
}
}
boolean canAutoRefresh(){
return getStreamPath() != null;
}
public boolean canReloadWhenRefreshTop(){
switch( column_type ){
default:
return false;
case TYPE_SEARCH:
case TYPE_SEARCH_MSP:
case TYPE_SEARCH_TS:
case TYPE_CONVERSATION:
case TYPE_LIST_LIST:
return true;
}
}
boolean canSpeech(){
return canStreaming() && column_type != TYPE_NOTIFICATIONS;
}
boolean canStreaming(){
return ! access_info.isNA() && (
access_info.isPseudo() ? isPublicStream() : getStreamPath() != null
);
}
private boolean bPutGap;
private void resumeStreaming( boolean bPutGap ){
// カラム種別によってはストリーミングAPIを利用できない
final String stream_path = getStreamPath();
if( stream_path == null ){
return;
}
// 疑似アカウントではストリーミングAPIを利用できない
// 2.1 では公開ストリームのみ利用できるらしい
if( access_info.isNA() || ( access_info.isPseudo() && ! isPublicStream() ) ){
return;
}
if( ! isActivityStart() ){
log.d( "resumeStreaming: isActivityStart is false." );
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;
}
if( App1.getAppState( context ).pref.getBoolean( Pref.KEY_DONT_USE_STREAMING, false ) ){
log.d( "resumeStreaming: disabled in app setting." );
return;
}
if( dont_streaming ){
log.d( "resumeStreaming: disabled in column setting." );
return;
}
this.bPutGap = bPutGap;
stream_data_queue.clear();
app_state.stream_reader.register(
access_info
, stream_path
, highlight_trie
, this
);
}
// onPauseの時はまとめて止められるが
// カラム破棄やリロード開始時は個別にストリーミングを止める必要がある
void stopStreaming(){
String stream_path = getStreamPath();
if( stream_path != null ){
app_state.stream_reader.unregister(
access_info
, stream_path
, this
);
}
}
public @NonNull String getListTitle(){
switch( column_type ){
default:
return "?";
case TYPE_LIST_MEMBER:
case TYPE_LIST_TL:
String sv = list_info == null ? null : list_info.title;
return ! TextUtils.isEmpty( sv ) ? sv : Long.toString( profile_id );
}
}
public long getListId(){
switch( column_type ){
default:
return - 1L;
case TYPE_LIST_MEMBER:
case TYPE_LIST_TL:
return profile_id;
}
}
public boolean isSearchColumn(){
switch( column_type ){
default:
return false;
case TYPE_SEARCH:
case TYPE_SEARCH_MSP:
case TYPE_SEARCH_TS:
return true;
}
}
}