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

4708 lines
154 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package jp.juggler.subwaytooter;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.customtabs.CustomTabsIntent;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.InputType;
import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.JsonReader;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
import android.support.design.widget.NavigationView;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewParent;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.HorizontalScrollView;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.apache.commons.io.IOUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
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.TootApiTask;
import jp.juggler.subwaytooter.api.entity.TootAccount;
import jp.juggler.subwaytooter.api.entity.TootApplication;
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.TootResults;
import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.api.entity.TootStatusLike;
import jp.juggler.subwaytooter.api_msp.entity.MSPToot;
import jp.juggler.subwaytooter.api_tootsearch.entity.TSToot;
import jp.juggler.subwaytooter.dialog.AccountPicker;
import jp.juggler.subwaytooter.dialog.DlgTextInput;
import jp.juggler.subwaytooter.dialog.DlgConfirm;
import jp.juggler.subwaytooter.dialog.LoginForm;
import jp.juggler.subwaytooter.dialog.ReportForm;
import jp.juggler.subwaytooter.table.AcctColor;
import jp.juggler.subwaytooter.table.MutedApp;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.table.UserRelation;
import jp.juggler.subwaytooter.dialog.ActionsDialog;
import jp.juggler.subwaytooter.util.LinkClickContext;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.MyClickableSpan;
import jp.juggler.subwaytooter.util.PostHelper;
import jp.juggler.subwaytooter.util.Utils;
import jp.juggler.subwaytooter.view.ColumnStripLinearLayout;
import jp.juggler.subwaytooter.view.GravitySnapHelper;
import jp.juggler.subwaytooter.view.MyEditText;
import okhttp3.Request;
import okhttp3.RequestBody;
@SuppressLint("StaticFieldLeak")
public class ActMain extends AppCompatActivity
implements NavigationView.OnNavigationItemSelectedListener, View.OnClickListener, ViewPager.OnPageChangeListener, Column.Callback, DrawerLayout.DrawerListener
{
public static final LogCategory log = new LogCategory( "ActMain" );
// @Override
// protected void attachBaseContext(Context newBase) {
// super.attachBaseContext( CalligraphyContextWrapper.wrap(newBase));
// }
public float density;
int acct_pad_lr;
SharedPreferences pref;
public Handler handler;
public AppState app_state;
// onActivityResultで設定されてonResumeで消化される
// 状態保存の必要なし
String posted_acct;
long posted_status_id;
float timeline_font_size_sp = Float.NaN;
float acct_font_size_sp = Float.NaN;
float validateFloat( float fv ){
if( Float.isNaN( fv ) ) return fv;
if( fv < 1f ) fv = 1f;
return fv;
}
@Override protected void onCreate( Bundle savedInstanceState ){
log.d( "onCreate" );
super.onCreate( savedInstanceState );
App1.setActivityTheme( this, true );
requestWindowFeature( Window.FEATURE_NO_TITLE );
handler = new Handler();
app_state = App1.getAppState( this );
pref = App1.pref;
this.density = app_state.density;
this.acct_pad_lr = (int) ( 0.5f + 4f * density );
timeline_font_size_sp = validateFloat( pref.getFloat( Pref.KEY_TIMELINE_FONT_SIZE, Float.NaN ) );
acct_font_size_sp = validateFloat( pref.getFloat( Pref.KEY_ACCT_FONT_SIZE, Float.NaN ) );
initUI();
updateColumnStrip();
if( ! app_state.column_list.isEmpty() ){
// 前回最後に表示していたカラムの位置にスクロールする
int column_pos = pref.getInt( Pref.KEY_LAST_COLUMN_POS, - 1 );
if( column_pos >= 0 && column_pos < app_state.column_list.size() ){
scrollToColumn( column_pos, true );
}
// 表示位置に合わせたイベントを発行
if( pager_adapter != null ){
onPageSelected( pager.getCurrentItem() );
}else{
resizeColumnWidth();
}
}
PollingWorker.queueUpdateNotification( this );
if( savedInstanceState != null && sent_intent2 != null ){
handleSentIntent( sent_intent2 );
}
}
@Override protected void onDestroy(){
log.d( "onDestroy" );
super.onDestroy();
post_helper.onDestroy();
// このアクティビティに関連する ColumnViewHolder への参照を全カラムから除去する
for( Column c : app_state.column_list ){
c.removeColumnViewHolderByActivity( this );
}
}
static final String STATE_CURRENT_PAGE = "current_page";
@Override protected void onSaveInstanceState( Bundle outState ){
log.d( "onSaveInstanceState" );
super.onSaveInstanceState( outState );
if( pager_adapter != null ){
outState.putInt( STATE_CURRENT_PAGE, pager.getCurrentItem() );
}else{
int ve = tablet_layout_manager.findLastVisibleItemPosition();
if( ve != RecyclerView.NO_POSITION ){
outState.putInt( STATE_CURRENT_PAGE, ve );
}
}
}
@Override protected void onRestoreInstanceState( Bundle savedInstanceState ){
log.d( "onRestoreInstanceState" );
super.onRestoreInstanceState( savedInstanceState );
int pos = savedInstanceState.getInt( STATE_CURRENT_PAGE );
if( pos > 0 && pos < app_state.column_list.size() ){
if( pager_adapter != null ){
pager.setCurrentItem( pos );
}else{
tablet_layout_manager.smoothScrollToPosition( tablet_pager, null, pos );
}
}
}
boolean bStart;
@Override public boolean isActivityStart(){
return bStart;
}
@Override protected void onStart(){
super.onStart();
bStart = true;
log.d( "onStart" );
// アカウント設定から戻ってきたら、カラムを消す必要があるかもしれない
{
ArrayList< Integer > new_order = new ArrayList<>();
for( int i = 0, ie = app_state.column_list.size() ; i < ie ; ++ i ){
Column column = app_state.column_list.get( i );
if( ! column.access_info.isNA() ){
SavedAccount sa = SavedAccount.loadAccount( ActMain.this, log, column.access_info.db_id );
if( sa == null ) continue;
}
new_order.add( i );
}
if( new_order.size() != app_state.column_list.size() ){
setOrder( new_order );
}
}
// 各カラムのアカウント設定を読み直す
reloadAccountSetting();
// 投稿直後ならカラムの再取得を行う
refreshAfterPost();
// 画面復帰時に再取得やストリーミング開始を行う
for( Column column : app_state.column_list ){
column.onStart( this );
}
// カラムの表示範囲インジケータを更新
updateColumnStripSelection( - 1, - 1f );
// 相対時刻表示
proc_updateRelativeTime.run();
}
@Override protected void onStop(){
log.d( "onStop" );
bStart = false;
handler.removeCallbacks( proc_updateRelativeTime );
post_helper.closeAcctPopup();
closeListItemPopup();
app_state.stream_reader.stopAll();
super.onStop();
}
@Override protected void onResume(){
super.onResume();
log.d( "onResume" );
MyClickableSpan.link_callback = new WeakReference<>( link_click_listener );
if( pref.getBoolean( Pref.KEY_DONT_SCREEN_OFF, false ) ){
getWindow().addFlags( WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON );
}else{
getWindow().clearFlags( WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON );
}
// 外部から受け取ったUriの処理
Uri uri = ActCallback.last_uri.getAndSet( null );
if( uri != null ){
handleIntentUri( uri );
}
// 外部から受け取ったUriの処理
Intent intent = ActCallback.sent_intent.getAndSet( null );
if( intent != null ){
handleSentIntent( intent );
}
}
@Override protected void onPause(){
log.d( "onPause" );
// 最後に表示していたカラムの位置
int column_pos;
if( pager_adapter != null ){
column_pos = pager.getCurrentItem();
}else{
column_pos = tablet_layout_manager.findFirstVisibleItemPosition();
}
pref.edit().putInt( Pref.KEY_LAST_COLUMN_POS, column_pos ).apply();
super.onPause();
}
void refreshAfterPost(){
if( ! TextUtils.isEmpty( posted_acct ) ){
int refresh_after_toot = pref.getInt( Pref.KEY_REFRESH_AFTER_TOOT, 0 );
if( refresh_after_toot != Pref.RAT_DONT_REFRESH ){
for( Column column : app_state.column_list ){
SavedAccount a = column.access_info;
if( ! Utils.equalsNullable( a.acct, posted_acct ) ) continue;
column.startRefreshForPost( posted_status_id, refresh_after_toot );
}
}
posted_acct = null;
}
}
static Intent sent_intent2;
private void handleSentIntent( final Intent intent ){
sent_intent2 = intent;
AccountPicker.pick( this
, false
, true
, getString( R.string.account_picker_toot )
, new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( @NonNull SavedAccount ai ){
sent_intent2 = null;
ActPost.open( ActMain.this, REQUEST_CODE_POST, ai.db_id, intent );
}
}
, new DialogInterface.OnDismissListener() {
@Override public void onDismiss( DialogInterface dialog ){
sent_intent2 = null;
}
}
);
}
// 画面上のUI操作で生成されて
// onPause,onPageDestroy 等のタイミングで閉じられる
// 状態保存の必要なし
StatusButtonsPopup list_item_popup;
void closeListItemPopup(){
if( list_item_popup != null ){
try{
list_item_popup.dismiss();
}catch( Throwable ignored ){
}
list_item_popup = null;
}
}
@Override public void onClick( View v ){
switch( v.getId() ){
case R.id.btnMenu:
if( ! drawer.isDrawerOpen( Gravity.START ) ){
drawer.openDrawer( Gravity.START );
}
break;
case R.id.btnToot:
performTootButton();
break;
case R.id.btnQuickToot:
performQuickPost( null );
break;
}
}
private void performQuickPost( SavedAccount account ){
if( account == null ){
if( pager_adapter != null ){
Column c = app_state.column_list.get( pager.getCurrentItem() );
if( ! c.access_info.isPseudo() ){
account = c.access_info;
}
}
if( account == null ){
AccountPicker.pick( this, false, true, getString( R.string.account_picker_toot ), new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( @NonNull SavedAccount ai ){
performQuickPost( ai );
}
} );
return;
}
}
post_helper.content = etQuickToot.getText().toString().trim();
post_helper.spoiler_text = null;
post_helper.visibility = account.visibility;
post_helper.bNSFW = false;
post_helper.in_reply_to_id = - 1L;
post_helper.attachment_list = null;
Utils.hideKeyboard( this, etQuickToot );
post_helper.post( account, false, false, new PostHelper.Callback() {
@Override public void onPostComplete( SavedAccount target_account, TootStatus status ){
etQuickToot.setText( "" );
posted_acct = target_account.acct;
posted_status_id = status.id;
refreshAfterPost();
}
} );
}
@Override
public void onPageScrolled( int position, float positionOffset, int positionOffsetPixels ){
updateColumnStripSelection( position, positionOffset );
}
@Override public void onPageSelected( final int position ){
handler.post( new Runnable() {
@Override public void run(){
if( position >= 0 && position < app_state.column_list.size() ){
Column column = app_state.column_list.get( position );
if( ! column.bFirstInitialized ){
column.startLoading();
}
scrollColumnStrip( position );
if( post_helper != null ){
post_helper.setInstance( column.access_info.isNA() ? null : column.access_info.host );
}
}
}
} );
}
@Override public void onPageScrollStateChanged( int state ){
}
boolean isOrderChanged( ArrayList< Integer > new_order ){
if( new_order.size() != app_state.column_list.size() ) return true;
for( int i = 0, ie = new_order.size() ; i < ie ; ++ i ){
if( new_order.get( i ) != i ) return true;
}
return false;
}
// リザルト
static final int RESULT_APP_DATA_IMPORT = RESULT_FIRST_USER;
// リクエスト
static final int REQUEST_CODE_COLUMN_LIST = 1;
static final int REQUEST_CODE_ACCOUNT_SETTING = 2;
static final int REQUEST_APP_ABOUT = 3;
static final int REQUEST_CODE_NICKNAME = 4;
static final int REQUEST_CODE_POST = 5;
static final int REQUEST_CODE_COLUMN_COLOR = 6;
static final int REQUEST_CODE_APP_SETTING = 7;
static final int REQUEST_CODE_TEXT = 8;
@Override protected void onActivityResult( int requestCode, int resultCode, Intent data ){
log.d( "onActivityResult" );
if( resultCode == RESULT_OK ){
if( requestCode == REQUEST_CODE_COLUMN_LIST ){
if( data != null ){
ArrayList< Integer > order = data.getIntegerArrayListExtra( ActColumnList.EXTRA_ORDER );
if( order != null && isOrderChanged( order ) ){
setOrder( order );
}
if( ! app_state.column_list.isEmpty() ){
int select = data.getIntExtra( ActColumnList.EXTRA_SELECTION, - 1 );
if( 0 <= select && select < app_state.column_list.size() ){
scrollToColumn( select, false );
}
}
}
}else if( requestCode == REQUEST_APP_ABOUT ){
if( data != null ){
String search = data.getStringExtra( ActAbout.EXTRA_SEARCH );
if( ! TextUtils.isEmpty( search ) ){
performAddTimeline( getDefaultInsertPosition(), true, Column.TYPE_SEARCH, search, true );
}
return;
}
}else if( requestCode == REQUEST_CODE_NICKNAME ){
updateColumnStrip();
for( Column column : app_state.column_list ){
column.fireShowColumnHeader();
}
}else if( requestCode == REQUEST_CODE_POST ){
if( data != null ){
etQuickToot.setText( "" );
posted_acct = data.getStringExtra( ActPost.EXTRA_POSTED_ACCT );
posted_status_id = data.getLongExtra( ActPost.EXTRA_POSTED_STATUS_ID, 0L );
}
}else if( requestCode == REQUEST_CODE_COLUMN_COLOR ){
if( data != null ){
app_state.saveColumnList();
int idx = data.getIntExtra( ActColumnCustomize.EXTRA_COLUMN_INDEX, 0 );
if( idx >= 0 && idx < app_state.column_list.size() ){
app_state.column_list.get( idx ).fireColumnColor();
app_state.column_list.get( idx ).fireShowContent();
}
updateColumnStrip();
}
}
}
if( requestCode == REQUEST_CODE_ACCOUNT_SETTING ){
updateColumnStrip();
for( Column column : app_state.column_list ){
column.fireShowColumnHeader();
}
if( resultCode == RESULT_OK && data != null ){
startAccessTokenUpdate( data );
}else if( resultCode == ActAccountSetting.RESULT_INPUT_ACCESS_TOKEN && data != null ){
long db_id = data.getLongExtra( ActAccountSetting.EXTRA_DB_ID, - 1L );
checkAccessToken2( db_id );
}
}else if( requestCode == REQUEST_CODE_APP_SETTING ){
showFooterColor();
if( resultCode == RESULT_APP_DATA_IMPORT ){
if( data != null ){
importAppData( data.getData() );
}
}
}else if( requestCode == REQUEST_CODE_TEXT ){
if( resultCode == ActText.RESULT_SEARCH_MSP ){
String text = data.getStringExtra( Intent.EXTRA_TEXT );
addColumn( getDefaultInsertPosition(), SavedAccount.getNA(), Column.TYPE_SEARCH_MSP, text );
}else if( resultCode == ActText.RESULT_SEARCH_TS ){
String text = data.getStringExtra( Intent.EXTRA_TEXT );
addColumn( getDefaultInsertPosition(), SavedAccount.getNA(), Column.TYPE_SEARCH_TS, text );
}
}
super.onActivityResult( requestCode, resultCode, data );
}
@Override
public void onBackPressed(){
// メニューが開いていたら閉じる
DrawerLayout drawer = findViewById( R.id.drawer_layout );
if( drawer.isDrawerOpen( GravityCompat.START ) ){
drawer.closeDrawer( GravityCompat.START );
return;
}
// カラムが0個ならアプリを終了する
if( app_state.column_list.isEmpty() ){
ActMain.this.finish();
return;
}
// カラム設定が開いているならカラム設定を閉じる
if( closeColumnSetting() ){
return;
}
// カラムが1個以上ある場合は設定に合わせて挙動を変える
switch( pref.getInt( Pref.KEY_BACK_BUTTON_ACTION, 0 ) ){
default:
case ActAppSetting.BACK_ASK_ALWAYS:
ActionsDialog dialog = new ActionsDialog();
Column current_column = null;
if( pager_adapter != null ){
current_column = app_state.column_list.get( pager.getCurrentItem() );
}else{
final int vs = tablet_layout_manager.findFirstVisibleItemPosition();
final int ve = tablet_layout_manager.findLastVisibleItemPosition();
if( vs == ve && vs != RecyclerView.NO_POSITION ){
current_column = app_state.column_list.get( vs );
}
}
if( current_column != null && ! current_column.dont_close ){
final Column _column = current_column;
dialog.addAction( getString( R.string.close_column ), new Runnable() {
@Override public void run(){
closeColumn( true, _column );
}
} );
}
dialog.addAction( getString( R.string.open_column_list ), new Runnable() {
@Override public void run(){
openColumnList();
}
} );
dialog.addAction( getString( R.string.app_exit ), new Runnable() {
@Override public void run(){
ActMain.this.finish();
}
} );
dialog.show( this, null );
break;
case ActAppSetting.BACK_CLOSE_COLUMN:
Column column = null;
if( pager_adapter != null ){
column = pager_adapter.getColumn( pager.getCurrentItem() );
}else{
final int vs = tablet_layout_manager.findFirstVisibleItemPosition();
final int ve = tablet_layout_manager.findLastVisibleItemPosition();
if( vs == ve && vs != RecyclerView.NO_POSITION ){
column = app_state.column_list.get( vs );
}else{
Utils.showToast( this, false, getString( R.string.cant_close_column_by_back_button_when_multiple_column_shown ) );
}
}
if( column != null ){
if( column.dont_close
&& pref.getBoolean( Pref.KEY_EXIT_APP_WHEN_CLOSE_PROTECTED_COLUMN, false )
&& pref.getBoolean( Pref.KEY_DONT_CONFIRM_BEFORE_CLOSE_COLUMN, false )
){
ActMain.this.finish();
return;
}
closeColumn( false, column );
}
break;
case ActAppSetting.BACK_EXIT_APP:
ActMain.this.finish();
break;
case ActAppSetting.BACK_OPEN_COLUMN_LIST:
openColumnList();
break;
}
}
@Override
public boolean onCreateOptionsMenu( Menu menu ){
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate( R.menu.act_main, menu );
return true;
}
@Override
public boolean onOptionsItemSelected( MenuItem item ){
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if( id == R.id.action_settings ){
return true;
}
return super.onOptionsItemSelected( item );
}
@Override public boolean onNavigationItemSelected( @NonNull MenuItem item ){
// Handle navigation view item clicks here.
int id = item.getItemId();
if( id == R.id.nav_account_add ){
performAccountAdd();
}else if( id == R.id.nav_add_tl_home ){
performAddTimeline( getDefaultInsertPosition(), false, Column.TYPE_HOME );
}else if( id == R.id.nav_add_tl_local ){
performAddTimeline( getDefaultInsertPosition(), true, Column.TYPE_LOCAL );
}else if( id == R.id.nav_add_tl_federate ){
performAddTimeline( getDefaultInsertPosition(), true, Column.TYPE_FEDERATE );
}else if( id == R.id.nav_add_favourites ){
performAddTimeline( getDefaultInsertPosition(), false, Column.TYPE_FAVOURITES );
// }else if( id == R.id.nav_add_reports ){
// performAddTimeline(Column.TYPE_REPORTS );
}else if( id == R.id.nav_add_statuses ){
performAddTimeline( getDefaultInsertPosition(), false, Column.TYPE_PROFILE );
}else if( id == R.id.nav_add_notifications ){
performAddTimeline( getDefaultInsertPosition(), false, Column.TYPE_NOTIFICATIONS );
}else if( id == R.id.nav_app_setting ){
ActAppSetting.open( this, REQUEST_CODE_APP_SETTING );
}else if( id == R.id.nav_account_setting ){
performAccountSetting();
}else if( id == R.id.nav_column_list ){
openColumnList();
}else if( id == R.id.nav_add_tl_search ){
performAddTimeline( getDefaultInsertPosition(), false, Column.TYPE_SEARCH, "", false );
}else if( id == R.id.nav_app_about ){
openAppAbout();
}else if( id == R.id.nav_oss_license ){
openOSSLicense();
}else if( id == R.id.nav_app_exit ){
finish();
}else if( id == R.id.nav_add_mutes ){
performAddTimeline( getDefaultInsertPosition(), false, Column.TYPE_MUTES );
}else if( id == R.id.nav_add_blocks ){
performAddTimeline( getDefaultInsertPosition(), false, Column.TYPE_BLOCKS );
}else if( id == R.id.nav_add_domain_blocks ){
performAddTimeline( getDefaultInsertPosition(), false, Column.TYPE_DOMAIN_BLOCKS );
}else if( id == R.id.nav_add_list ){
performAddTimeline( getDefaultInsertPosition(), false, Column.TYPE_LIST_LIST );
}else if( id == R.id.nav_follow_requests ){
performAddTimeline( getDefaultInsertPosition(), false, Column.TYPE_FOLLOW_REQUESTS );
}else if( id == R.id.nav_muted_app ){
startActivity( new Intent( this, ActMutedApp.class ) );
}else if( id == R.id.nav_muted_word ){
startActivity( new Intent( this, ActMutedWord.class ) );
}else if( id == R.id.mastodon_search_portal ){
addColumn( getDefaultInsertPosition(), SavedAccount.getNA(), Column.TYPE_SEARCH_MSP, "" );
}else if( id == R.id.tootsearch ){
addColumn( getDefaultInsertPosition(), SavedAccount.getNA(), Column.TYPE_SEARCH_TS, "" );
}
DrawerLayout drawer = findViewById( R.id.drawer_layout );
drawer.closeDrawer( GravityCompat.START );
return true;
}
ViewPager pager;
ColumnPagerAdapter pager_adapter;
View llEmpty;
DrawerLayout drawer;
ColumnStripLinearLayout llColumnStrip;
HorizontalScrollView svColumnStrip;
ImageButton btnMenu;
ImageButton btnToot;
View vFooterDivider1;
View vFooterDivider2;
RecyclerView tablet_pager;
TabletColumnPagerAdapter tablet_pager_adapter;
LinearLayoutManager tablet_layout_manager;
GravitySnapHelper tablet_snap_helper;
static final int COLUMN_WIDTH_MIN_DP = 300;
public Typeface timeline_font;
public Typeface timeline_font_bold;
boolean dont_crop_media_thumbnail;
boolean mShortAcctLocalUser;
int mAvatarIconSize;
View llQuickTootBar;
MyEditText etQuickToot;
ImageButton btnQuickToot;
PostHelper post_helper;
void initUI(){
setContentView( R.layout.act_main );
dont_crop_media_thumbnail = pref.getBoolean( Pref.KEY_DONT_CROP_MEDIA_THUMBNAIL, false );
String sv = pref.getString( Pref.KEY_TIMELINE_FONT, null );
if( ! TextUtils.isEmpty( sv ) ){
try{
timeline_font = Typeface.createFromFile( sv );
}catch( Throwable ex ){
log.trace( ex );
}
}
sv = pref.getString( Pref.KEY_TIMELINE_FONT_BOLD, null );
if( ! TextUtils.isEmpty( sv ) ){
try{
timeline_font_bold = Typeface.createFromFile( sv );
}catch( Throwable ex ){
log.trace( ex );
}
}else if( timeline_font != null ){
try{
timeline_font_bold = Typeface.create( timeline_font, Typeface.BOLD );
}catch( Throwable ex ){
log.trace( ex );
}
}
mShortAcctLocalUser = pref.getBoolean( Pref.KEY_SHORT_ACCT_LOCAL_USER, false );
{
float icon_size_dp = 48f;
try{
sv = pref.getString( Pref.KEY_AVATAR_ICON_SIZE, null );
float fv = TextUtils.isEmpty( sv ) ? Float.NaN : Float.parseFloat( sv );
if( Float.isNaN( fv ) || Float.isInfinite( fv ) || fv < 1f ){
// error or bad range
}else{
icon_size_dp = fv;
}
}catch( Throwable ex ){
log.trace( ex );
}
mAvatarIconSize = (int) ( 0.5f + icon_size_dp * density );
}
llEmpty = findViewById( R.id.llEmpty );
// // toolbar
// Toolbar toolbar = (Toolbar) findViewById( R.id.toolbar );
// setSupportActionBar( toolbar );
// navigation drawer
drawer = findViewById( R.id.drawer_layout );
// ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
// this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close );
drawer.addDrawerListener( this );
// toggle.syncState();
NavigationView navigationView = findViewById( R.id.nav_view );
navigationView.setNavigationItemSelectedListener( this );
btnMenu = findViewById( R.id.btnMenu );
btnToot = findViewById( R.id.btnToot );
vFooterDivider1 = findViewById( R.id.vFooterDivider1 );
vFooterDivider2 = findViewById( R.id.vFooterDivider2 );
llColumnStrip = findViewById( R.id.llColumnStrip );
svColumnStrip = findViewById( R.id.svColumnStrip );
llQuickTootBar = findViewById( R.id.llQuickTootBar );
etQuickToot = findViewById( R.id.etQuickToot );
btnQuickToot = findViewById( R.id.btnQuickToot );
if( ! pref.getBoolean( Pref.KEY_QUICK_TOOT_BAR, false ) ){
llQuickTootBar.setVisibility( View.GONE );
}
btnToot.setOnClickListener( this );
btnMenu.setOnClickListener( this );
btnQuickToot.setOnClickListener( this );
if( pref.getBoolean( Pref.KEY_DONT_USE_ACTION_BUTTON, false ) ){
etQuickToot.setInputType( InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE );
etQuickToot.setImeOptions( EditorInfo.IME_ACTION_NONE );
// 最後に指定する必要がある?
etQuickToot.setMaxLines( 5 );
etQuickToot.setVerticalScrollBarEnabled( true );
etQuickToot.setScrollbarFadingEnabled( false );
}else{
etQuickToot.setInputType( InputType.TYPE_CLASS_TEXT );
etQuickToot.setImeOptions( EditorInfo.IME_ACTION_SEND );
etQuickToot.setOnEditorActionListener( new TextView.OnEditorActionListener() {
@Override public boolean onEditorAction( TextView v, int actionId, KeyEvent event ){
if( actionId == EditorInfo.IME_ACTION_SEND ){
btnQuickToot.performClick();
return true;
}
return false;
}
} );
// 最後に指定する必要がある?
etQuickToot.setMaxLines( 1 );
}
svColumnStrip.setHorizontalFadingEdgeEnabled( true );
post_helper = new PostHelper( this, pref, app_state.handler );
DisplayMetrics dm = getResources().getDisplayMetrics();
float density = dm.density;
int media_thumb_height = 64;
sv = pref.getString( Pref.KEY_MEDIA_THUMB_HEIGHT, "" );
if( ! TextUtils.isEmpty( sv ) ){
try{
int iv = Integer.parseInt( sv );
if( iv >= 32 ){
media_thumb_height = iv;
}
}catch( Throwable ex ){
log.trace( ex );
}
}
app_state.media_thumb_height = (int) ( 0.5f + media_thumb_height * density );
int column_w_min_dp = COLUMN_WIDTH_MIN_DP;
sv = pref.getString( Pref.KEY_COLUMN_WIDTH, "" );
if( ! TextUtils.isEmpty( sv ) ){
try{
int iv = Integer.parseInt( sv );
if( iv >= 100 ){
column_w_min_dp = iv;
}
}catch( Throwable ex ){
log.trace( ex );
}
}
int column_w_min = (int) ( 0.5f + column_w_min_dp * density );
int sw = dm.widthPixels;
pager = findViewById( R.id.viewPager );
tablet_pager = findViewById( R.id.rvPager );
if( pref.getBoolean( Pref.KEY_DISABLE_TABLET_MODE, false ) || sw < column_w_min * 2 ){
tablet_pager.setVisibility( View.GONE );
// SmartPhone mode
pager_adapter = new ColumnPagerAdapter( this );
pager.setAdapter( pager_adapter );
pager.addOnPageChangeListener( this );
resizeAutoCW( sw );
}else{
pager.setVisibility( View.GONE );
// tablet mode
tablet_pager_adapter = new TabletColumnPagerAdapter( this );
tablet_layout_manager = new LinearLayoutManager( this, LinearLayoutManager.HORIZONTAL, false );
tablet_pager.setAdapter( tablet_pager_adapter );
tablet_pager.setLayoutManager( tablet_layout_manager );
tablet_pager.addOnScrollListener( new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged( RecyclerView recyclerView, int newState ){
super.onScrollStateChanged( recyclerView, newState );
int vs = tablet_layout_manager.findFirstVisibleItemPosition();
int ve = tablet_layout_manager.findLastVisibleItemPosition();
// 端に近い方に合わせる
int distance_left = Math.abs( vs );
int distance_right = Math.abs( ( app_state.column_list.size() - 1 ) - ve );
if( distance_left < distance_right ){
scrollColumnStrip( vs );
}else{
scrollColumnStrip( ve );
}
}
@Override public void onScrolled( RecyclerView recyclerView, int dx, int dy ){
super.onScrolled( recyclerView, dx, dy );
updateColumnStripSelection( - 1, - 1f );
}
} );
///////tablet_pager.setHasFixedSize( true );
// tablet_pager.addItemDecoration( new TabletColumnDivider( this ) );
tablet_snap_helper = new GravitySnapHelper( Gravity.START );
tablet_snap_helper.attachToRecyclerView( tablet_pager );
}
showFooterColor();
post_helper.attachEditText( findViewById( R.id.llFormRoot ), etQuickToot, true, new PostHelper.Callback2() {
@Override public void onTextUpdate(){
}
@Override public boolean canOpenPopup(){
return drawer != null && ! drawer.isDrawerOpen( Gravity.START );
}
} );
}
void updateColumnStrip(){
llEmpty.setVisibility( app_state.column_list.isEmpty() ? View.VISIBLE : View.GONE );
llColumnStrip.removeAllViews();
for( int i = 0, ie = app_state.column_list.size() ; i < ie ; ++ i ){
final Column column = app_state.column_list.get( i );
View viewRoot = getLayoutInflater().inflate( R.layout.lv_column_strip, llColumnStrip, false );
ImageView ivIcon = viewRoot.findViewById( R.id.ivIcon );
viewRoot.setTag( i );
viewRoot.setOnClickListener( new View.OnClickListener() {
@Override public void onClick( View v ){
scrollToColumn( (Integer) v.getTag(), false );
}
} );
viewRoot.setContentDescription( column.getColumnName( true ) );
//
int c = column.header_bg_color;
if( c == 0 ){
viewRoot.setBackgroundResource( R.drawable.btn_bg_ddd );
}else{
ViewCompat.setBackground( viewRoot, Styler.getAdaptiveRippleDrawable(
c,
( column.header_fg_color != 0 ? column.header_fg_color :
Styler.getAttributeColor( this, R.attr.colorRippleEffect ) )
) );
}
c = column.header_fg_color;
if( c == 0 ){
Styler.setIconDefaultColor( this, ivIcon, column.getIconAttrId( column.column_type ) );
}else{
Styler.setIconCustomColor( this, ivIcon, c, column.getIconAttrId( column.column_type ) );
}
//
AcctColor ac = AcctColor.load( column.access_info.acct );
if( AcctColor.hasColorForeground( ac ) ){
View vAcctColor = viewRoot.findViewById( R.id.vAcctColor );
vAcctColor.setBackgroundColor( ac.color_fg );
}
//
llColumnStrip.addView( viewRoot );
//
}
svColumnStrip.requestLayout();
updateColumnStripSelection( - 1, - 1f );
}
private void updateColumnStripSelection( final int position, final float positionOffset ){
handler.post( new Runnable() {
@Override public void run(){
if( isFinishing() ) return;
if( app_state.column_list.isEmpty() ){
llColumnStrip.setColumnRange( - 1, - 1, 0f );
}else if( pager_adapter != null ){
if( position >= 0 ){
llColumnStrip.setColumnRange( position, position, positionOffset );
}else{
int c = pager.getCurrentItem();
llColumnStrip.setColumnRange( c, c, 0f );
}
}else{
int first = tablet_layout_manager.findFirstVisibleItemPosition();
int last = tablet_layout_manager.findLastVisibleItemPosition();
if( last - first > nScreenColumn - 1 ){
last = first + nScreenColumn - 1;
}
float slide_ratio = 0f;
if( first != RecyclerView.NO_POSITION && nColumnWidth > 0 ){
View child = tablet_layout_manager.findViewByPosition( first );
slide_ratio = Math.abs( child.getLeft() / (float) nColumnWidth );
}
llColumnStrip.setColumnRange( first, last, slide_ratio );
}
}
} );
}
private void scrollColumnStrip( final int select ){
int child_count = llColumnStrip.getChildCount();
if( select < 0 || select >= child_count ){
return;
}
View icon = llColumnStrip.getChildAt( select );
int sv_width = ( (View) llColumnStrip.getParent() ).getWidth();
int ll_width = llColumnStrip.getWidth();
int icon_width = icon.getWidth();
int icon_left = icon.getLeft();
if( sv_width == 0 || ll_width == 0 || icon_width == 0 ){
handler.postDelayed( new Runnable() {
@Override public void run(){
scrollColumnStrip( select );
}
}, 20L );
}
int sx = icon_left + icon_width / 2 - sv_width / 2;
svColumnStrip.smoothScrollTo( sx, 0 );
}
public void performAccountAdd(){
LoginForm.showLoginForm( this, null, new LoginForm.LoginFormCallback() {
@Override
public void startLogin(
final Dialog dialog
, final String instance
, final boolean bPseudoAccount
, final boolean bInputAccessToken
){
new TootApiTask( ActMain.this, instance, true ) {
@Override protected TootApiResult doInBackground( Void... voids ){
if( bPseudoAccount ){
return client.checkInstance();
}else{
String client_name = Pref.pref( ActMain.this ).getString( Pref.KEY_CLIENT_NAME, "" );
return client.authorize1( client_name );
}
}
@Override protected void handleResult( TootApiResult result ){
if( result == null ) return; // cancelled.
if( result.error != null ){
String sv = result.error;
// エラーはブラウザ用URLかもしれない
if( sv.startsWith( "https" ) ){
if( bInputAccessToken ){
// アクセストークンの手動入力
DlgTextInput.show( ActMain.this, getString( R.string.access_token ), null, new DlgTextInput.Callback() {
@Override
public void onOK( Dialog dialog_token, String access_token ){
checkAccessToken( dialog, dialog_token, instance, access_token, null );
}
@Override public void onEmptyError(){
Utils.showToast( ActMain.this, true, R.string.token_not_specified );
}
} );
}else{
// OAuth認証が必要
Intent data = new Intent();
data.setData( Uri.parse( sv ) );
startAccessTokenUpdate( data );
try{
dialog.dismiss();
}catch( Throwable ignored ){
// IllegalArgumentException がたまに出る
}
}
return;
}
log.e( result.error );
if( sv.contains( "SSLHandshakeException" )
&& ( Build.VERSION.RELEASE.startsWith( "7.0" )
|| ( Build.VERSION.RELEASE.startsWith( "7.1" ) && ! Build.VERSION.RELEASE.startsWith( "7.1." ) ) )
){
new AlertDialog.Builder( ActMain.this )
.setMessage( sv + "\n\n" + getString( R.string.ssl_bug_7_0 ) )
.setNeutralButton( R.string.close, null )
.show();
return;
}
// 他のエラー
Utils.showToast( ActMain.this, true, sv );
}else{
SavedAccount a = addPseudoAccount( instance );
if( a != null ){
// 疑似アカウントが追加された
Utils.showToast( ActMain.this, false, R.string.server_confirmed );
int pos = app_state.column_list.size();
addColumn( pos, a, Column.TYPE_LOCAL );
try{
dialog.dismiss();
}catch( Throwable ignored ){
// IllegalArgumentException がたまに出る
}
}
}
}
}.executeOnExecutor( App1.task_executor );
}
} );
}
@Nullable SavedAccount addPseudoAccount( String host ){
try{
String username = "?";
String full_acct = username + "@" + host;
SavedAccount account = SavedAccount.loadAccountByAcct( this, log, full_acct );
if( account != null ){
return account;
}
JSONObject account_info = new JSONObject();
account_info.put( "username", username );
account_info.put( "acct", username );
long row_id = SavedAccount.insert( host, full_acct, account_info, new JSONObject() );
account = SavedAccount.loadAccount( ActMain.this, log, row_id );
if( account == null ){
throw new RuntimeException( "loadAccount returns null." );
}
account.notification_follow = false;
account.notification_favourite = false;
account.notification_boost = false;
account.notification_mention = false;
account.saveSetting();
return account;
}catch( Throwable ex ){
log.trace( ex );
log.e( ex, "addPseudoAccount failed." );
Utils.showToast( this, ex, "addPseudoAccount failed." );
}
return null;
}
private void startAccessTokenUpdate( Intent data ){
Uri uri = data.getData();
if( uri == null ) return;
// ブラウザで開く
try{
Intent intent = new Intent( Intent.ACTION_VIEW, uri );
startActivity( intent );
}catch( Throwable ex ){
log.trace( ex );
}
}
// ActOAuthCallbackで受け取ったUriを処理する
private void handleIntentUri( @NonNull final Uri uri ){
if( "subwaytooter".equals( uri.getScheme() ) ){
try{
handleOAuth2CallbackUri( uri );
}catch( Throwable ex ){
log.trace( ex );
}
return;
}
final String url = uri.toString();
Matcher m = reStatusPage.matcher( url );
if( m.find() ){
try{
// https://mastodon.juggler.jp/@SubwayTooter/(status_id)
final String host = m.group( 1 );
final long status_id = Long.parseLong( m.group( 3 ), 10 );
// ステータスをアプリ内で開く
openStatusOtherInstance( getDefaultInsertPosition(), null, uri.toString(), status_id, host, status_id );
}catch( Throwable ex ){
Utils.showToast( this, ex, "can't parse status id." );
}
return;
}
m = reUserPage.matcher( url );
if( m.find() ){
// https://mastodon.juggler.jp/@SubwayTooter
final String host = m.group( 1 );
final String user = Uri.decode( m.group( 2 ) );
// ユーザページをアプリ内で開く
openProfileByHostUser( getDefaultInsertPosition(), null, uri.toString(), host, user );
return;
}
// このアプリでは処理できないURLだった
// 外部ブラウザを開きなおそうとすると無限ループの恐れがある
// アプリケーションチューザーを表示する
String error_message = getString( R.string.cant_handle_uri_of, url );
try{
int query_flag;
if( Build.VERSION.SDK_INT >= 23 ){
// Android 6.0以降
// MATCH_DEFAULT_ONLY だと標準の設定に指定されたアプリがあるとソレしか出てこない
// MATCH_ALL を指定すると 以前と同じ挙動になる
query_flag = PackageManager.MATCH_ALL;
}else{
// Android 5.xまでは MATCH_DEFAULT_ONLY でマッチするすべてのアプリを取得できる
query_flag = PackageManager.MATCH_DEFAULT_ONLY;
}
// queryIntentActivities に渡すURLは実在しないホストのものにする
Intent intent = new Intent( Intent.ACTION_VIEW, Uri.parse( "https://dummy.subwaytooter.club/" ) );
intent.setFlags( Intent.FLAG_ACTIVITY_NEW_TASK );
List< ResolveInfo > resolveInfoList = getPackageManager().queryIntentActivities( intent, query_flag );
if( resolveInfoList.isEmpty() ){
throw new RuntimeException( "resolveInfoList is empty." );
}
// このアプリ以外の選択肢を集める
String my_name = getPackageName();
ArrayList< Intent > choice_list = new ArrayList<>();
for( ResolveInfo ri : resolveInfoList ){
// 選択肢からこのアプリを除外
if( my_name.equals( ri.activityInfo.packageName ) ) continue;
// 選択肢のIntentは目的のUriで作成する
Intent choice = new Intent( Intent.ACTION_VIEW, uri );
intent.setFlags( Intent.FLAG_ACTIVITY_NEW_TASK );
choice.setPackage( ri.activityInfo.packageName );
choice.setClassName( ri.activityInfo.packageName, ri.activityInfo.name );
choice_list.add( choice );
}
if( choice_list.isEmpty() ){
throw new RuntimeException( "choice_list is empty." );
}
// 指定した選択肢でチューザーを作成して開く
Intent chooser = Intent.createChooser( choice_list.remove( 0 ), error_message );
chooser.putExtra( Intent.EXTRA_INITIAL_INTENTS, choice_list.toArray( new Intent[ choice_list.size() ] ) );
startActivity( chooser );
return;
}catch( Throwable ex ){
log.trace( ex );
}
new AlertDialog.Builder( this )
.setCancelable( true )
.setMessage( error_message )
.setPositiveButton( R.string.close, null )
.show();
}
private void handleOAuth2CallbackUri( @NonNull final Uri uri ){
// 通知タップ
// subwaytooter://notification_click/?db_id=(db_id)
String sv = uri.getQueryParameter( "db_id" );
if( ! TextUtils.isEmpty( sv ) ){
try{
long db_id = Long.parseLong( sv, 10 );
SavedAccount account = SavedAccount.loadAccount( ActMain.this, log, db_id );
if( account != null ){
Column column = addColumn( getDefaultInsertPosition(), account, Column.TYPE_NOTIFICATIONS );
// 通知を読み直す
if( ! column.bInitialLoading ){
column.startLoading();
}
PollingWorker.queueNotificationClicked( this, db_id );
}
}catch( Throwable ex ){
log.trace( ex );
}
return;
}
// OAuth2 認証コールバック
// subwaytooter://oauth/?...
new TootApiTask( ActMain.this, true ) {
TootAccount ta;
SavedAccount sa;
String host;
@Override
protected TootApiResult doInBackground( Void... params ){
// エラー時
// subwaytooter://oauth
// ?error=access_denied
// &error_description=%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E3%81%AE%E6%89%80%E6%9C%89%E8%80%85%E3%81%BE%E3%81%9F%E3%81%AF%E8%AA%8D%E8%A8%BC%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%8C%E8%A6%81%E6%B1%82%E3%82%92%E6%8B%92%E5%90%A6%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F%E3%80%82
// &state=db%3A3
String error = uri.getQueryParameter( "error_description" );
if( ! TextUtils.isEmpty( error ) ){
return new TootApiResult( error );
}
// subwaytooter://oauth
// ?code=113cc036e078ac500d3d0d3ad345cd8181456ab087abc67270d40f40a4e9e3c2
// &state=host%3Amastodon.juggler.jp
String code = uri.getQueryParameter( "code" );
if( TextUtils.isEmpty( code ) ){
return new TootApiResult( "missing code in callback url." );
}
String sv = uri.getQueryParameter( "state" );
if( TextUtils.isEmpty( sv ) ){
return new TootApiResult( "missing state in callback url." );
}
if( sv.startsWith( "db:" ) ){
try{
long db_id = Long.parseLong( sv.substring( 3 ), 10 );
this.sa = SavedAccount.loadAccount( ActMain.this, log, db_id );
if( sa == null ){
return new TootApiResult( "missing account db_id=" + db_id );
}
client.setAccount( sa );
}catch( Throwable ex ){
log.trace( ex );
return new TootApiResult( Utils.formatError( ex, "invalid state" ) );
}
}else if( sv.startsWith( "host:" ) ){
String host = sv.substring( 5 );
client.setInstance( host );
}
if( client.instance == null ){
return new TootApiResult( "missing instance in callback url." );
}
this.host = client.instance;
String client_name = Pref.pref( ActMain.this ).getString( Pref.KEY_CLIENT_NAME, "" );
TootApiResult result = client.authorize2( client_name, code );
if( result != null && result.object != null ){
// taは使い捨てなので、生成に使うLinkClickContextはダミーで問題ない
LinkClickContext lcc = new LinkClickContext() {
@Override public AcctColor findAcctColor( String url ){
return null;
}
};
this.ta = TootAccount.parse( ActMain.this, lcc, result.object );
}
return result;
}
@Override protected void handleResult( TootApiResult result ){
afterAccountVerify( result, ta, sa, host );
}
}.executeOnExecutor( App1.task_executor );
}
boolean afterAccountVerify( @Nullable TootApiResult result, @Nullable TootAccount ta, @Nullable SavedAccount sa, @Nullable String host ){
//noinspection StatementWithEmptyBody
if( result == null ){
// cancelled.
}else if( result.error != null ){
Utils.showToast( ActMain.this, true, result.error );
}else if( result.token_info == null ){
Utils.showToast( ActMain.this, true, "can't get access token." );
}else if( result.object == null ){
Utils.showToast( ActMain.this, true, "can't parse json response." );
}else if( ta == null ){
// 自分のユーザネームを取れなかった
// …普通はエラーメッセージが設定されてるはずだが
Utils.showToast( ActMain.this, true, "can't verify user credential." );
}else if( sa != null ){
// アクセストークン更新時
// インスタンスは同じだと思うが、ユーザ名が異なる可能性がある
if( ! sa.username.equals( ta.username ) ){
Utils.showToast( ActMain.this, true, R.string.user_name_not_match );
}else{
Utils.showToast( ActMain.this, false, R.string.access_token_updated_for, sa.acct );
// DBの情報を更新する
sa.updateTokenInfo( result.token_info );
// 各カラムの持つアカウント情報をリロードする
reloadAccountSetting();
// 自動でリロードする
for( Column c : app_state.column_list ){
if( c.access_info.acct.equals( sa.acct ) ){
c.startLoading();
}
}
// 通知の更新が必要かもしれない
PollingWorker.queueUpdateNotification( ActMain.this );
return true;
}
}else if( host != null ){
// アカウント追加時
String user = ta.username + "@" + host;
long row_id = SavedAccount.insert( host, user, result.object, result.token_info );
SavedAccount account = SavedAccount.loadAccount( ActMain.this, log, row_id );
if( account != null ){
boolean bModified = false;
if( account.locked ){
bModified = true;
account.visibility = TootStatus.VISIBILITY_PRIVATE;
}
if( ta.source != null ){
if( ta.source.privacy != null ){
bModified = true;
account.visibility = ta.source.privacy;
}
// FIXME ta.source.sensitive パラメータを読んで「添付画像をデフォルトでNSFWにする」を実現する
// 現在、アカウント設定にはこの項目はない( 「NSFWな添付メディアを隠さない」はあるが全く別の効果)
}
if( bModified ){
account.saveSetting();
}
Utils.showToast( ActMain.this, false, R.string.account_confirmed );
// 通知の更新が必要かもしれない
PollingWorker.queueUpdateNotification( ActMain.this );
// 適当にカラムを追加する
long count = SavedAccount.getCount();
if( count > 1 ){
addColumn( getDefaultInsertPosition(), account, Column.TYPE_HOME );
}else{
addColumn( getDefaultInsertPosition(), account, Column.TYPE_HOME );
addColumn( getDefaultInsertPosition(), account, Column.TYPE_NOTIFICATIONS );
addColumn( getDefaultInsertPosition(), account, Column.TYPE_LOCAL );
addColumn( getDefaultInsertPosition(), account, Column.TYPE_FEDERATE );
}
return true;
}
}
return false;
}
// アクセストークンを手動で入力した場合
void checkAccessToken(
@Nullable final Dialog dialog_host
, @Nullable final Dialog dialog_token
, @NonNull final String host
, @NonNull final String access_token
, @Nullable final SavedAccount sa
){
new TootApiTask( ActMain.this, host, true ) {
TootAccount ta;
@Override
protected TootApiResult doInBackground( Void... params ){
TootApiResult result = client.checkAccessToken( access_token );
if( result != null && result.object != null ){
// taは使い捨てなので、生成に使うLinkClickContextはダミーで問題ない
LinkClickContext lcc = new LinkClickContext() {
@Override public AcctColor findAcctColor( String url ){
return null;
}
};
this.ta = TootAccount.parse( ActMain.this, lcc, result.object );
}
return result;
}
@Override protected void handleResult( TootApiResult result ){
if( afterAccountVerify( result, ta, sa, host ) ){
try{
if( dialog_host != null ) dialog_host.dismiss();
}catch( Throwable ignored ){
// IllegalArgumentException がたまに出る
}
try{
if( dialog_token != null ) dialog_token.dismiss();
}catch( Throwable ignored ){
// IllegalArgumentException がたまに出る
}
}
}
}.executeOnExecutor( App1.task_executor );
}
// アクセストークンの手動入力(更新)
void checkAccessToken2( long db_id ){
final SavedAccount sa = SavedAccount.loadAccount( this, log, db_id );
if( sa == null ) return;
DlgTextInput.show( this, getString( R.string.access_token ), null, new DlgTextInput.Callback() {
@Override public void onOK( Dialog dialog_token, String access_token ){
checkAccessToken( null, dialog_token, sa.host, access_token, sa );
}
@Override public void onEmptyError(){
Utils.showToast( ActMain.this, true, R.string.token_not_specified );
}
} );
}
void reloadAccountSetting(){
ArrayList< SavedAccount > done_list = new ArrayList<>();
for( Column column : app_state.column_list ){
SavedAccount a = column.access_info;
if( done_list.contains( a ) ) continue;
done_list.add( a );
if( ! a.isNA() ) a.reloadSetting( ActMain.this );
column.fireShowColumnHeader();
}
}
void reloadAccountSetting( SavedAccount account ){
ArrayList< SavedAccount > done_list = new ArrayList<>();
for( Column column : app_state.column_list ){
SavedAccount a = column.access_info;
if( ! Utils.equalsNullable( a.acct, account.acct ) ) continue;
if( done_list.contains( a ) ) continue;
done_list.add( a );
if( ! a.isNA() ) a.reloadSetting( ActMain.this );
column.fireShowColumnHeader();
}
}
public void closeColumn( boolean bConfirm, final Column column ){
if( column.dont_close ){
Utils.showToast( this, false, R.string.column_has_dont_close_option );
return;
}
if( ! bConfirm && ! pref.getBoolean( Pref.KEY_DONT_CONFIRM_BEFORE_CLOSE_COLUMN, false ) ){
new AlertDialog.Builder( this )
.setMessage( R.string.confirm_close_column )
.setNegativeButton( R.string.cancel, null )
.setPositiveButton( R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick( DialogInterface dialog, int which ){
closeColumn( true, column );
}
} )
.show();
return;
}
int page_delete = app_state.column_list.indexOf( column );
if( pager_adapter != null ){
int page_showing = pager.getCurrentItem();
removeColumn( column );
if( ! app_state.column_list.isEmpty() && page_delete > 0 && page_showing == page_delete ){
int idx = page_delete - 1;
scrollToColumn( idx, false );
Column c = app_state.column_list.get( idx );
if( ! c.bFirstInitialized ){
c.startLoading();
}
}
}else{
removeColumn( column );
if( ! app_state.column_list.isEmpty() && page_delete > 0 ){
int idx = page_delete - 1;
scrollToColumn( idx, false );
Column c = app_state.column_list.get( idx );
if( ! c.bFirstInitialized ){
c.startLoading();
}
}
}
}
//////////////////////////////////////////////////////////////
// カラム追加系
public Column addColumn( int index, SavedAccount ai, int type, Object... params ){
// 既に同じカラムがあればそこに移動する
for( Column column : app_state.column_list ){
if( column.isSameSpec( ai, type, params ) ){
index = app_state.column_list.indexOf( column );
scrollToColumn( index, false );
return column;
}
}
//
Column col = new Column( app_state, ai, this, type, params );
index = addColumn( col, index );
scrollToColumn( index, false );
if( ! col.bFirstInitialized ){
col.startLoading();
}
return col;
}
private void performAddTimeline( final int pos, boolean bAllowPseudo, final int type, final Object... args ){
AccountPicker.pick( this, bAllowPseudo, true
, getString( R.string.account_picker_add_timeline_of, Column.getColumnTypeName( this, type ) )
, new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( @NonNull SavedAccount ai ){
switch( type ){
default:
addColumn( pos, ai, type, args );
break;
case Column.TYPE_PROFILE:
addColumn( pos, ai, type, ai.id );
break;
}
}
} );
}
public void performMuteApp( @NonNull TootApplication application ){
MutedApp.save( application.name );
for( Column column : app_state.column_list ){
column.removeMuteApp();
}
Utils.showToast( ActMain.this, false, R.string.app_was_muted );
}
//////////////////////////////////////////////////////////////
interface FindAccountCallback {
// return account information
// if failed, account is null.
void onFindAccount( @Nullable TootAccount account );
}
// ユーザ名からアカウントIDを取得するために検索APIを使う
void startFindAccount( final SavedAccount access_info, final String host, final String user, final FindAccountCallback callback ){
new TootApiTask( this, access_info, true ) {
@Override protected TootApiResult doInBackground( Void... voids ){
String path = "/api/v1/accounts/search" + "?q=" + Uri.encode( user );
TootApiResult result = client.request( path );
if( result != null && result.array != null ){
for( int i = 0, ie = result.array.length() ; i < ie ; ++ i ){
TootAccount a = TootAccount.parse( ActMain.this, access_info, result.array.optJSONObject( i ) );
if( ! a.username.equals( user ) ) continue;
if( access_info.getFullAcct( a ).equalsIgnoreCase( user + "@" + host ) ){
who = a;
break;
}
}
}
return result;
}
TootAccount who;
@Override protected void handleResult( TootApiResult result ){
callback.onFindAccount( who );
}
}.executeOnExecutor( App1.task_executor );
}
static final Pattern reUrlHashTag = Pattern.compile( "\\Ahttps://([^/]+)/tags/([^?#]+)(?:\\z|[?#])" );
static final Pattern reUserPage = Pattern.compile( "\\Ahttps://([^/]+)/@([A-Za-z0-9_]+)(?:\\z|[?#])" );
static final Pattern reStatusPage = Pattern.compile( "\\Ahttps://([^/]+)/@([A-Za-z0-9_]+)/(\\d+)(?:\\z|[?#])" );
public void openChromeTab( final int pos, @Nullable final SavedAccount access_info, final String url, boolean noIntercept ){
try{
log.d( "openChromeTab url=%s", url );
if( ! noIntercept && access_info != null ){
// ハッシュタグをアプリ内で開く
Matcher m = reUrlHashTag.matcher( url );
if( m.find() ){
// https://mastodon.juggler.jp/tags/%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%82%BF%E3%82%B0
String host = m.group( 1 );
String tag_without_sharp = Uri.decode( m.group( 2 ) );
if( access_info.isNA() || ! host.equalsIgnoreCase( access_info.host ) ){
openHashTagOtherInstance( pos, access_info, url, host, tag_without_sharp );
}else{
openHashTag( pos, access_info, tag_without_sharp );
}
return;
}
// ステータスページをアプリから開く
m = reStatusPage.matcher( url );
if( m.find() ){
try{
// https://mastodon.juggler.jp/@SubwayTooter/(status_id)
final String host = m.group( 1 );
final long status_id = Long.parseLong( m.group( 3 ), 10 );
if( access_info.isNA() || ! host.equalsIgnoreCase( access_info.host ) ){
openStatusOtherInstance( pos, access_info, url, status_id, host, status_id );
}else{
openStatusLocal( pos, access_info, status_id );
}
}catch( Throwable ex ){
Utils.showToast( this, ex, "can't parse status id." );
}
return;
}
// ユーザページをアプリ内で開く
m = reUserPage.matcher( url );
if( m.find() ){
// https://mastodon.juggler.jp/@SubwayTooter
final String host = m.group( 1 );
final String user = Uri.decode( m.group( 2 ) );
openProfileByHostUser( pos, access_info, url, host, user );
return;
}
}
do{
if( pref.getBoolean( Pref.KEY_PRIOR_CHROME, true ) ){
try{
// 初回はChrome指定で試す
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
builder.setToolbarColor( Styler.getAttributeColor( this, R.attr.colorPrimary ) ).setShowTitle( true );
CustomTabsIntent customTabsIntent = builder.build();
customTabsIntent.intent.setComponent( new ComponentName( "com.android.chrome", "com.google.android.apps.chrome.Main" ) );
customTabsIntent.launchUrl( this, Uri.parse( url ) );
break;
}catch( Throwable ex2 ){
log.e( ex2, "openChromeTab: missing chrome. retry to other application." );
}
}
// chromeがないなら ResolverActivity でアプリを選択させる
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
builder.setToolbarColor( Styler.getAttributeColor( this, R.attr.colorPrimary ) ).setShowTitle( true );
CustomTabsIntent customTabsIntent = builder.build();
customTabsIntent.launchUrl( this, Uri.parse( url ) );
}while( false );
}catch( Throwable ex ){
// log.trace( ex );
log.e( ex, "openChromeTab failed. url=%s", url );
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
public void openHashTag( int pos, @NonNull SavedAccount access_info, @NonNull String tag_without_sharp ){
addColumn( pos, access_info, Column.TYPE_HASHTAG, tag_without_sharp );
}
// 他インスタンスのハッシュタグの表示
private void openHashTagOtherInstance(
int pos
, @NonNull SavedAccount access_info
, @NonNull String url
, @NonNull String host
, @NonNull String tag_without_sharp
){
openHashTagOtherInstance_sub( pos, access_info, url, host, tag_without_sharp );
}
// 他インスタンスのハッシュタグの表示
private void openHashTagOtherInstance_sub(
final int pos
, @NonNull final SavedAccount access_info
, @NonNull final String url
, @NonNull final String host
, @NonNull final String tag_without_sharp
){
ActionsDialog dialog = new ActionsDialog();
// 各アカウント
ArrayList< SavedAccount > account_list = SavedAccount.loadAccountList( ActMain.this, log );
// ソートする
SavedAccount.sort( account_list );
ArrayList< SavedAccount > list_original = new ArrayList<>();
ArrayList< SavedAccount > list_original_pseudo = new ArrayList<>();
ArrayList< SavedAccount > list_other = new ArrayList<>();
for( SavedAccount a : account_list ){
log.d( "sort? %s", a.acct );
if( ! host.equalsIgnoreCase( a.host ) ){
list_other.add( a );
}else if( a.isPseudo() ){
list_original_pseudo.add( a );
}else{
list_original.add( a );
}
}
// ブラウザで表示する
dialog.addAction( getString( R.string.open_web_on_host, host ), new Runnable() {
@Override public void run(){
openChromeTab( pos, access_info, url, true );
}
} );
if( list_original.isEmpty() && list_original_pseudo.isEmpty() ){
// 疑似アカウントを作成して開く
dialog.addAction( getString( R.string.open_in_pseudo_account, "?@" + host ), new Runnable() {
@Override public void run(){
SavedAccount sa = addPseudoAccount( host );
if( sa != null ){
openHashTag( pos, sa, tag_without_sharp );
}
}
} );
}
//
for( SavedAccount a : list_original ){
final SavedAccount _a = a;
dialog.addAction( AcctColor.getStringWithNickname( ActMain.this, R.string.open_in_account, a.acct ), new Runnable() {
@Override public void run(){
openHashTag( pos, _a, tag_without_sharp );
}
} );
}
//
for( SavedAccount a : list_original_pseudo ){
final SavedAccount _a = a;
dialog.addAction( AcctColor.getStringWithNickname( ActMain.this, R.string.open_in_account, a.acct ), new Runnable() {
@Override public void run(){
openHashTag( pos, _a, tag_without_sharp );
}
} );
}
//
for( SavedAccount a : list_other ){
final SavedAccount _a = a;
dialog.addAction( AcctColor.getStringWithNickname( ActMain.this, R.string.open_in_account, a.acct ), new Runnable() {
@Override public void run(){
openHashTag( pos, _a, tag_without_sharp );
}
} );
}
dialog.show( this, "#" + tag_without_sharp );
}
final MyClickableSpan.LinkClickCallback link_click_listener = new MyClickableSpan.LinkClickCallback() {
@Override public void onClickLink( View view, @NonNull final MyClickableSpan span ){
View view_orig = view;
Column column = null;
while( view != null ){
Object tag = view.getTag();
if( tag instanceof ItemViewHolder ){
column = ( (ItemViewHolder) tag ).column;
break;
}else if( tag instanceof HeaderViewHolderProfile ){
column = ( (HeaderViewHolderProfile) tag ).column;
break;
}else if( tag instanceof TabletColumnViewHolder ){
column = ( (TabletColumnViewHolder) tag ).vh.column;
break;
}else{
ViewParent parent = view.getParent();
if( parent instanceof View ){
view = (View) parent;
}else{
break;
}
}
}
final int pos = nextPosition( column );
// ハッシュタグはいきなり開くのではなくメニューがある
Matcher m = reUrlHashTag.matcher( span.url );
if( m.find() ){
// https://mastodon.juggler.jp/tags/%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%82%BF%E3%82%B0
final String host = m.group( 1 );
final String tag_with_sharp = span.text.startsWith( "#" ) ? span.text : "#" + Uri.decode( m.group( 2 ) );
final String tag_without_sharp = tag_with_sharp.substring( 1 );
ActionsDialog d = new ActionsDialog()
.addAction( getString( R.string.open_hashtag_column ), new Runnable() {
@Override public void run(){
openHashTagOtherInstance( pos, (SavedAccount) span.lcc, span.url, host, tag_without_sharp );
}
} )
.addAction( getString( R.string.quote_hashtag_of, tag_with_sharp ), new Runnable() {
@Override public void run(){
openPost( tag_with_sharp + " " );
}
} );
final ArrayList< String > tag_list = new ArrayList<>();
try{
//noinspection ConstantConditions
CharSequence cs = ( (TextView) view_orig ).getText();
if( cs instanceof Spannable ){
Spannable content = (Spannable) cs;
for( MyClickableSpan s : content.getSpans( 0, content.length(), MyClickableSpan.class ) ){
m = reUrlHashTag.matcher( s.url );
if( m.find() ){
String s_tag = s.text.startsWith( "#" ) ? s.text : "#" + Uri.decode( m.group( 2 ) );
tag_list.add( s_tag );
}
}
}
}catch( Throwable ex ){
log.trace( ex );
}
if( tag_list.size() > 1 ){
StringBuilder sb = new StringBuilder();
for( String s : tag_list ){
if( sb.length() > 0 ) sb.append( ' ' );
sb.append( s );
}
final String tag_all = sb.toString();
d.addAction( getString( R.string.quote_all_hashtag_of, tag_all ), new Runnable() {
@Override public void run(){
openPost( tag_all + " " );
}
} );
}
d.show( ActMain.this, tag_with_sharp );
return;
}
openChromeTab( pos, (SavedAccount) span.lcc, span.url, false );
}
};
private void performTootButton(){
openPost( etQuickToot.getText().toString() );
}
public void openPost( final String initial_text ){
post_helper.closeAcctPopup();
if( pager_adapter != null ){
Column c = pager_adapter.getColumn( pager.getCurrentItem() );
if( c != null && ! c.access_info.isPseudo() ){
ActPost.open( this, REQUEST_CODE_POST, c.access_info.db_id, initial_text );
return;
}
}else{
long db_id = pref.getLong( Pref.KEY_TABLET_TOOT_DEFAULT_ACCOUNT, - 1L );
SavedAccount a = SavedAccount.loadAccount( ActMain.this, log, db_id );
if( a != null ){
ActPost.open( this, REQUEST_CODE_POST, a.db_id, initial_text );
return;
}
}
AccountPicker.pick( this, false, true, getString( R.string.account_picker_toot ), new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( @NonNull SavedAccount ai ){
ActPost.open( ActMain.this, REQUEST_CODE_POST, ai.db_id, initial_text );
}
} );
}
public void performMention( SavedAccount account, @NonNull TootAccount who ){
ActPost.open( this, REQUEST_CODE_POST, account.db_id, "@" + account.getFullAcct( who ) + " " );
}
public void performMentionFromAnotherAccount( SavedAccount access_info, @Nullable final TootAccount who ){
if( who == null ) return;
String who_host = access_info.getAccountHost( who );
final String initial_text = "@" + access_info.getFullAcct( who ) + " ";
AccountPicker.pick( this, false, false
, getString( R.string.account_picker_toot )
, makeAccountListNonPseudo( log, who_host )
, new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( @NonNull SavedAccount ai ){
ActPost.open( ActMain.this, REQUEST_CODE_POST, ai.db_id, initial_text );
}
} );
}
/////////////////////////////////////////////////////////////////////////
private void showColumnMatchAccount( SavedAccount account ){
for( Column column : app_state.column_list ){
if( account.acct.equals( column.access_info.acct ) ){
column.fireShowContent();
}
}
}
/////////////////////////////////////////////////////////////////////////
// open profile
private void openProfileRemote( final int pos, final SavedAccount access_info, final String who_url ){
new TootApiTask( this, access_info, true ) {
@Override protected TootApiResult doInBackground( Void... params ){
// 検索APIに他タンスのユーザのURLを投げると、自タンスのURLを得られる
String path = String.format( Locale.JAPAN, Column.PATH_SEARCH, Uri.encode( who_url ) );
path = path + "&resolve=1";
TootApiResult result = client.request( path );
if( result != null && result.object != null ){
TootResults tmp = TootResults.parse( ActMain.this, access_info, result.object );
if( tmp != null ){
if( tmp.accounts != null && ! tmp.accounts.isEmpty() ){
who_local = tmp.accounts.get( 0 );
}
}
if( who_local == null ){
return new TootApiResult( getString( R.string.user_id_conversion_failed ) );
}
}
return result;
}
TootAccount who_local;
@Override protected void handleResult( TootApiResult result ){
if( result == null ){
// cancelled.
}else if( who_local != null ){
addColumn( pos, access_info, Column.TYPE_PROFILE, who_local.id );
}else{
Utils.showToast( ActMain.this, true, result.error );
// 仕方ないのでchrome tab で開く
openChromeTab( pos, access_info, who_url, true );
}
}
}.executeOnExecutor( App1.task_executor );
}
void openProfileFromAnotherAccount( final int pos, @NonNull final SavedAccount access_info, @Nullable final TootAccount who ){
if( who == null ) return;
String who_host = access_info.getAccountHost( who );
AccountPicker.pick( this, false, false
, getString( R.string.account_picker_open_user_who, AcctColor.getNickname( who.acct ) )
, makeAccountListNonPseudo( log, who_host )
, new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( @NonNull SavedAccount ai ){
if( ai.host.equalsIgnoreCase( access_info.host ) ){
addColumn( pos, ai, Column.TYPE_PROFILE, who.id );
}else{
openProfileRemote( pos, ai, who.url );
}
}
} );
}
void openProfile( int pos, @NonNull SavedAccount access_info, @Nullable TootAccount who ){
if( who == null ){
Utils.showToast( this, false, "user is null" );
}else if( access_info.isPseudo() ){
openProfileFromAnotherAccount( pos, access_info, who );
}else{
addColumn( pos, access_info, Column.TYPE_PROFILE, who.id );
}
}
// Intent-FilterからUser URL で指定されたユーザのプロフを開く
// openChromeTabからUser URL で指定されたユーザのプロフを開く
private void openProfileByHostUser(
final int pos
, @Nullable final SavedAccount access_info
, @NonNull final String url
, @NonNull final String host
, @NonNull final String user
){
// リンクタップした文脈のアカウントが疑似でないなら
if( access_info != null && ! access_info.isPseudo() ){
if( access_info.host.equalsIgnoreCase( host ) ){
// 文脈のアカウントと同じインスタンスなら、アカウントIDを探して開いてしまう
startFindAccount( access_info, host, user, new FindAccountCallback() {
@Override public void onFindAccount( TootAccount who ){
if( who != null ){
openProfile( pos, access_info, who );
return;
}
// ダメならchromeで開く
openChromeTab( pos, access_info, url, true );
}
} );
}else{
// 文脈のアカウント異なるインスタンスなら、別アカウントで開く
openProfileRemote( pos, access_info, url );
}
return;
}
// 文脈がない、もしくは疑似アカウントだった
// 疑似ではないアカウントの一覧
if( ! SavedAccount.hasRealAccount( log ) ){
// 疑似アカウントではユーザ情報APIを呼べないし検索APIも使えない
// chrome tab で開くしかない
openChromeTab( pos, access_info, url, true );
}else{
// アカウントを選択して開く
AccountPicker.pick( this, false, false
, getString( R.string.account_picker_open_user_who, AcctColor.getNickname( user + "@" + host ) )
, makeAccountListNonPseudo( log, host )
, new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( @NonNull SavedAccount ai ){
openProfileRemote( pos, ai, url );
}
} );
}
}
/////////////////////////////////////////////////////////////////////////
// favourite
public void performFavourite(
final SavedAccount access_info
, final TootStatusLike arg_status
, final int nCrossAccountMode
, final boolean bSet
, final RelationChangedCallback callback
){
if( app_state.isBusyFav( access_info, arg_status ) ){
Utils.showToast( this, false, R.string.wait_previous_operation );
return;
}
//
app_state.setBusyFav( access_info, arg_status );
//
new TootApiTask( this, access_info, false ) {
@Override protected TootApiResult doInBackground( Void... params ){
TootApiResult result;
TootStatusLike target_status;
if( nCrossAccountMode == CROSS_ACCOUNT_REMOTE_INSTANCE ){
// 検索APIに他タンスのステータスのURLを投げると、自タンスのステータスを得られる
String path = String.format( Locale.JAPAN, Column.PATH_SEARCH, Uri.encode( arg_status.url ) );
path = path + "&resolve=1";
result = client.request( path );
if( result == null || result.object == null ){
return result;
}
target_status = null;
TootResults tmp = TootResults.parse( ActMain.this, access_info, result.object );
if( tmp != null ){
if( tmp.statuses != null && ! tmp.statuses.isEmpty() ){
target_status = tmp.statuses.get( 0 );
log.d( "status id conversion %s => %s", arg_status.id, target_status.id );
}
}
if( target_status == null ){
return new TootApiResult( getString( R.string.status_id_conversion_failed ) );
}else if( target_status.favourited ){
return new TootApiResult( getString( R.string.already_favourited ) );
}
}else{
target_status = arg_status;
}
Request.Builder request_builder = new Request.Builder()
.post( RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, ""
) );
result = client.request(
( bSet
? "/api/v1/statuses/" + target_status.id + "/favourite"
: "/api/v1/statuses/" + target_status.id + "/unfavourite"
)
, request_builder );
if( result != null && result.object != null ){
new_status = TootStatus.parse( ActMain.this, access_info, result.object );
}
return result;
}
TootStatus new_status;
@Override protected void handleResult( TootApiResult result ){
app_state.resetBusyFav( access_info, arg_status );
//noinspection StatementWithEmptyBody
if( result == null ){
// cancelled.
}else if( new_status != null ){
// カウント数は遅延があるみたいなので、恣意的に表示を変更する
if( bSet && new_status.favourited && new_status.favourites_count <= arg_status.favourites_count ){
// 星をつけたのにカウントが上がらないのは違和感あるので、表示をいじる
new_status.favourites_count = arg_status.favourites_count + 1;
}else if( ! bSet && ! new_status.favourited && new_status.favourites_count >= arg_status.favourites_count ){
// 星を外したのにカウントが下がらないのは違和感あるので、表示をいじる
new_status.favourites_count = arg_status.favourites_count - 1;
// 0未満にはならない
if( new_status.favourites_count < 0 ){
new_status.favourites_count = 0;
}
}
for( Column column : app_state.column_list ){
column.findStatus( access_info.host, new_status.id, new Column.StatusEntryCallback() {
@Override
public boolean onIterate( SavedAccount account, TootStatus status ){
status.favourites_count = new_status.favourites_count;
if( access_info.acct.equalsIgnoreCase( account.acct ) ){
status.favourited = new_status.favourited;
}
return true;
}
} );
}
if( callback != null ) callback.onRelationChanged();
}else{
Utils.showToast( ActMain.this, true, result.error );
}
// 結果に関わらず、更新中状態から復帰させる
showColumnMatchAccount( access_info );
}
}.executeOnExecutor( App1.task_executor );
// ファボ表示を更新中にする
showColumnMatchAccount( access_info );
}
/////////////////////////////////////////////////////////////////////////
// boost
public void performBoost(
final SavedAccount access_info
, final TootStatusLike arg_status
, final int nCrossAccountMode
, final boolean bSet
, final boolean bConfirmed
, final RelationChangedCallback callback
){
// アカウントからステータスにブースト操作を行っているなら、何もしない
if( app_state.isBusyBoost( access_info, arg_status ) ){
Utils.showToast( this, false, R.string.wait_previous_operation );
return;
}
// クロスアカウント操作ではないならステータス内容を使ったチェックを行える
if( nCrossAccountMode == NOT_CROSS_ACCOUNT ){
if( arg_status.reblogged ){
if( app_state.isBusyFav( access_info, arg_status ) || arg_status.favourited ){
// FAVがついているか、FAV操作中はBoostを外せない
Utils.showToast( this, false, R.string.cant_remove_boost_while_favourited );
return;
}
}
}
// 必要なら確認を出す
if( bSet && ! bConfirmed ){
DlgConfirm.open(
this
, getString( R.string.confirm_boost_from, AcctColor.getNickname( access_info.acct ) )
, new DlgConfirm.Callback() {
@Override public boolean isConfirmEnabled(){
return access_info.confirm_boost;
}
@Override public void setConfirmEnabled( boolean bv ){
access_info.confirm_boost = bv;
access_info.saveSetting();
reloadAccountSetting( access_info );
}
@Override public void onOK(){
performBoost( access_info, arg_status, nCrossAccountMode, true, true, callback );
}
}
);
return;
}
app_state.setBusyBoost( access_info, arg_status );
new TootApiTask( this, access_info, false ) {
@Override protected TootApiResult doInBackground( Void... params ){
TootApiResult result;
TootStatusLike target_status;
if( nCrossAccountMode == CROSS_ACCOUNT_REMOTE_INSTANCE ){
// 検索APIに他タンスのステータスのURLを投げると、自タンスのステータスを得られる
String path = String.format( Locale.JAPAN, Column.PATH_SEARCH, Uri.encode( arg_status.url ) );
path = path + "&resolve=1";
result = client.request( path );
if( result == null || result.object == null ){
return result;
}
target_status = null;
TootResults tmp = TootResults.parse( ActMain.this, access_info, result.object );
if( tmp != null ){
if( tmp.statuses != null && ! tmp.statuses.isEmpty() ){
target_status = tmp.statuses.get( 0 );
}
}
if( target_status == null ){
return new TootApiResult( getString( R.string.status_id_conversion_failed ) );
}else if( target_status.reblogged ){
return new TootApiResult( getString( R.string.already_boosted ) );
}
}else{
// 既に自タンスのステータスがある
target_status = arg_status;
}
Request.Builder request_builder = new Request.Builder()
.post( RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, ""
) );
result = client.request(
"/api/v1/statuses/" + target_status.id + ( bSet ? "/reblog" : "/unreblog" )
, request_builder );
if( result != null && result.object != null ){
new_status = TootStatus.parse( ActMain.this, access_info, result.object );
// reblogはreblogを表すStatusを返す
// unreblogはreblogしたStatusを返す
if( new_status != null && new_status.reblog != null )
new_status = new_status.reblog;
// // reblog,unreblog のレスポンスは信用ならんのでステータスを再取得する
// result = client.request( "/api/v1/statuses/" + target_status.id );
// if( result != null && result.object != null ){
// }
}
return result;
}
TootStatus new_status;
@Override protected void handleResult( TootApiResult result ){
app_state.resetBusyBoost( access_info, arg_status );
//noinspection StatementWithEmptyBody
if( result == null ){
// cancelled.
}else if( new_status != null ){
// カウント数は遅延があるみたいなので、恣意的に表示を変更する
// ブーストカウント数を加工する
if( bSet && new_status.reblogged && new_status.reblogs_count <= arg_status.reblogs_count ){
// 星をつけたのにカウントが上がらないのは違和感あるので、表示をいじる
new_status.reblogs_count = arg_status.reblogs_count + 1;
}else if( ! bSet && ! new_status.reblogged && new_status.reblogs_count >= arg_status.reblogs_count ){
// 星を外したのにカウントが下がらないのは違和感あるので、表示をいじる
new_status.reblogs_count = arg_status.reblogs_count - 1;
// 0未満にはならない
if( new_status.reblogs_count < 0 ){
new_status.reblogs_count = 0;
}
}
for( Column column : app_state.column_list ){
column.findStatus( access_info.host, new_status.id, new Column.StatusEntryCallback() {
@Override
public boolean onIterate( SavedAccount account, TootStatus status ){
status.reblogs_count = new_status.reblogs_count;
if( access_info.acct.equalsIgnoreCase( account.acct ) ){
status.reblogged = new_status.reblogged;
}
return true;
}
} );
}
if( callback != null ) callback.onRelationChanged();
}else{
Utils.showToast( ActMain.this, true, result.error );
}
// 結果に関わらず、更新中状態から復帰させる
showColumnMatchAccount( access_info );
}
}.executeOnExecutor( App1.task_executor );
showColumnMatchAccount( access_info );
}
/////////////////////////////////////////////////////////////////////////////////
// reply
public void performReply(
final SavedAccount access_info
, final TootStatus arg_status
){
ActPost.open( this, REQUEST_CODE_POST, access_info.db_id, arg_status );
}
public void performReplyRemote(
final SavedAccount access_info
, final String remote_status_url
){
new TootApiTask( this, access_info, true ) {
TootStatus target_status;
@Override protected TootApiResult doInBackground( Void... params ){
// 検索APIに他タンスのステータスのURLを投げると、自タンスのステータスを得られる
String path = String.format( Locale.JAPAN, Column.PATH_SEARCH, Uri.encode( remote_status_url ) );
path = path + "&resolve=1";
TootApiResult result = client.request( path );
if( result != null && result.object != null ){
TootResults tmp = TootResults.parse( ActMain.this, access_info, result.object );
if( tmp != null && tmp.statuses != null && ! tmp.statuses.isEmpty() ){
target_status = tmp.statuses.get( 0 );
log.d( "status id conversion %s => %s", remote_status_url, target_status.id );
}
if( target_status == null ){
return new TootApiResult( getString( R.string.status_id_conversion_failed ) );
}
}
return result;
}
@Override protected void handleResult( TootApiResult result ){
if( result == null ){
// cancelled.
}else if( target_status != null ){
ActPost.open( ActMain.this, REQUEST_CODE_POST, access_info.db_id, target_status );
}else{
Utils.showToast( ActMain.this, true, result.error );
}
}
}
.setProgressPrefix( getString( R.string.progress_synchronize_toot ) )
.executeOnExecutor( App1.task_executor );
}
/////////////////////////////////////////////////////////////////////////////////////
// open conversation
public void openStatusLocal( int pos, @NonNull SavedAccount access_info, long status_id ){
addColumn( pos, access_info, Column.TYPE_CONVERSATION, status_id );
}
public void openStatus( int pos, @NonNull SavedAccount access_info, @NonNull TootStatusLike status ){
if( access_info.isNA() || ! access_info.host.equalsIgnoreCase( status.host_access ) ){
openStatusOtherInstance( pos, access_info, status );
}else{
openStatusLocal( pos, access_info, status.id );
}
}
public void openStatusOtherInstance( int pos, @NonNull SavedAccount access_info, @Nullable TootStatusLike status ){
// アカウント情報がないと出来ないことがある
if( status == null || status.account == null ) return;
if( status instanceof MSPToot ){
openStatusOtherInstance( pos, access_info, status.url
, status.id
, null, - 1L
);
}else if( status instanceof TSToot ){
// Tootsearch ではステータスのアクセス元ホストは分からない
// uri から投稿元タンスでのステータスIDを調べる
long status_id_original = TootStatusLike.parseStatusId( status );
openStatusOtherInstance( pos, access_info, status.url
, status_id_original
, null, - 1L
);
}else if( status instanceof TootStatus ){
if( status.host_original.equals( status.host_access ) ){
// TLアカウントのホストとトゥートのアカウントのホストが同じ場合
openStatusOtherInstance( pos, access_info, status.url
, status.id
, null, - 1L
);
}else{
// TLアカウントのホストとトゥートのアカウントのホストが異なる場合
// uri から投稿元タンスでのステータスIDを調べる
long status_id_original = TootStatusLike.parseStatusId( status );
openStatusOtherInstance( pos, access_info, status.url
, status_id_original
, status.host_access, status.id
);
}
}
}
void openStatusOtherInstance(
final int pos
, @Nullable final SavedAccount access_info
, @NonNull final String url
, final long status_id_original
, final String host_access, final long status_id_access
){
ActionsDialog dialog = new ActionsDialog();
final String host_original = Uri.parse( url ).getAuthority();
// 選択肢:ブラウザで表示する
dialog.addAction( getString( R.string.open_web_on_host, host_original ), new Runnable() {
@Override public void run(){
openChromeTab( pos, access_info, url, true );
}
} );
// トゥートの投稿元タンスにあるアカウント
ArrayList< SavedAccount > local_account_list = new ArrayList<>();
// TLを読んだタンスにあるアカウント
ArrayList< SavedAccount > access_account_list = new ArrayList<>();
// その他のタンスにあるアカウント
ArrayList< SavedAccount > other_account_list = new ArrayList<>();
for( SavedAccount a : SavedAccount.loadAccountList( ActMain.this, log ) ){
// 疑似アカウントは後でまとめて処理する
if( a.isPseudo() ) continue;
if( status_id_original >= 0L && host_original.equalsIgnoreCase( a.host ) ){
// アクセス情報ステータスID でアクセスできるなら
// 同タンスのアカウントならステータスIDの変換なしに表示できる
local_account_list.add( a );
}else if( status_id_access >= 0L && host_access.equalsIgnoreCase( a.host ) ){
// 既に変換済みのステータスIDがあるなら、そのアカウントでもステータスIDの変換は必要ない
access_account_list.add( a );
}else{
// 別タンスでも実アカウントなら検索APIでステータスIDを変換できる
other_account_list.add( a );
}
}
// 同タンスのアカウントがないなら、疑似アカウントで開く選択肢
if( local_account_list.isEmpty() ){
if( status_id_original >= 0L ){
dialog.addAction( getString( R.string.open_in_pseudo_account, "?@" + host_original ), new Runnable() {
@Override public void run(){
SavedAccount sa = addPseudoAccount( host_original );
if( sa != null ){
openStatusLocal( pos, sa, status_id_original );
}
}
} );
}else{
dialog.addAction( getString( R.string.open_in_pseudo_account, "?@" + host_original ), new Runnable() {
@Override public void run(){
SavedAccount sa = addPseudoAccount( host_original );
if( sa != null ){
openStatusRemote( pos, sa, url );
}
}
} );
}
}
// ローカルアカウント
SavedAccount.sort( local_account_list );
for( SavedAccount a : local_account_list ){
final SavedAccount _a = a;
dialog.addAction( AcctColor.getStringWithNickname( ActMain.this, R.string.open_in_account, a.acct ), new Runnable() {
@Override public void run(){
openStatusLocal( pos, _a, status_id_original );
}
} );
}
// アクセスしたアカウント
SavedAccount.sort( access_account_list );
for( SavedAccount a : access_account_list ){
final SavedAccount _a = a;
dialog.addAction( AcctColor.getStringWithNickname( ActMain.this, R.string.open_in_account, a.acct ), new Runnable() {
@Override public void run(){
openStatusLocal( pos, _a, status_id_access );
}
} );
}
// その他の実アカウント
SavedAccount.sort( other_account_list );
for( SavedAccount a : other_account_list ){
final SavedAccount _a = a;
dialog.addAction( AcctColor.getStringWithNickname( ActMain.this, R.string.open_in_account, a.acct ), new Runnable() {
@Override public void run(){
openStatusRemote( pos, _a, url );
}
} );
}
dialog.show( this, getString( R.string.open_status_from ) );
}
static final Pattern reDetailedStatusTime = Pattern.compile( "<a\\b[^>]*?\\bdetailed-status__datetime\\b[^>]*href=\"https://[^/]+/@[^/]+/(\\d+)\"" );
public void openStatusRemote(
final int pos
, final SavedAccount access_info
, final String remote_status_url
){
new TootApiTask( this, access_info, true ) {
long local_status_id = - 1L;
@Override protected TootApiResult doInBackground( Void... params ){
TootApiResult result;
if( access_info.isPseudo() ){
result = client.getHttp( remote_status_url );
if( result != null && result.json != null ){
try{
Matcher m = reDetailedStatusTime.matcher( result.json );
if( m.find() ){
local_status_id = Long.parseLong( m.group( 1 ), 10 );
}
}catch( Throwable ex ){
log.e( ex, "openStatusRemote: can't parse status id from HTML data." );
}
if( local_status_id == - 1L ){
result = new TootApiResult( getString( R.string.status_id_conversion_failed ) );
}
}
}else{
// 検索APIに他タンスのステータスのURLを投げると、自タンスのステータスを得られる
String path = String.format( Locale.JAPAN, Column.PATH_SEARCH, Uri.encode( remote_status_url ) );
path = path + "&resolve=1";
result = client.request( path );
if( result != null && result.object != null ){
TootResults tmp = TootResults.parse( ActMain.this, access_info, result.object );
if( tmp != null && tmp.statuses != null && ! tmp.statuses.isEmpty() ){
TootStatus status = tmp.statuses.get( 0 );
local_status_id = status.id;
log.d( "status id conversion %s => %s", remote_status_url, status.id );
}
if( local_status_id == - 1L ){
result = new TootApiResult( getString( R.string.status_id_conversion_failed ) );
}
}
}
return result;
}
@Override protected void handleResult( TootApiResult result ){
if( result == null ){
// cancelled.
}else if( local_status_id != - 1L ){
openStatusLocal( pos, access_info, local_status_id );
}else{
Utils.showToast( ActMain.this, true, result.error );
}
}
}
.setProgressPrefix( getString( R.string.progress_synchronize_toot ) )
.executeOnExecutor( App1.task_executor );
}
////////////////////////////////////////
// profile pin
public void setProfilePin( @NonNull final SavedAccount access_info, @NonNull final TootStatusLike status, final boolean bSet ){
new TootApiTask( this, access_info, true ) {
TootStatus new_status;
@Override protected TootApiResult doInBackground( Void... params ){
TootApiResult result;
Request.Builder request_builder = new Request.Builder()
.post( RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, ""
) );
result = client.request(
( bSet
? "/api/v1/statuses/" + status.id + "/pin"
: "/api/v1/statuses/" + status.id + "/unpin"
)
, request_builder );
if( result != null && result.object != null ){
new_status = TootStatus.parse( ActMain.this, access_info, result.object );
}
return result;
}
@Override protected void handleResult( TootApiResult result ){
//noinspection StatementWithEmptyBody
if( result == null ){
// cancelled.
}else if( new_status != null ){
for( Column column : app_state.column_list ){
column.findStatus( access_info.host, new_status.id, new Column.StatusEntryCallback() {
@Override
public boolean onIterate( SavedAccount account, TootStatus status ){
status.pinned = bSet;
return true;
}
} );
}
}else{
Utils.showToast( ActMain.this, true, result.error );
}
// 結果に関わらず、更新中状態から復帰させる
showColumnMatchAccount( access_info );
}
}
.setProgressPrefix( getString( R.string.profile_pin_progress ) )
.executeOnExecutor( App1.task_executor );
}
////////////////////////////////////////
// delete notification
public void deleteNotificationOne( @NonNull final SavedAccount access_info, @NonNull final TootNotification notification ){
new TootApiTask( this, access_info, true ) {
@Override protected TootApiResult doInBackground( Void... params ){
Request.Builder request_builder = new Request.Builder()
.post( RequestBody.create( TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, "id=" + Long.toString( notification.id )
)
);
return client.request(
"/api/v1/notifications/dismiss"
, request_builder );
}
@Override protected void handleResult( TootApiResult result ){
if( result == null ){
// cancelled.
}else if( result.object != null ){
// 成功したら空オブジェクトが返される
for( Column column : app_state.column_list ){
column.removeNotificationOne( access_info, notification );
}
Utils.showToast( ActMain.this, true, R.string.delete_succeeded );
}else{
Utils.showToast( ActMain.this, true, result.error );
}
}
}.executeOnExecutor( App1.task_executor );
}
////////////////////////////////////////
public void toggleConversationMute( @NonNull final SavedAccount access_info, @NonNull final TootStatusLike status ){
final boolean bMute = ! status.muted;
new TootApiTask( this, access_info, true ) {
@Override protected TootApiResult doInBackground( Void... params ){
Request.Builder request_builder = new Request.Builder()
.post( RequestBody.create( TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED, "" ) );
TootApiResult result = client.request(
"/api/v1/statuses/" + status.id + ( bMute ? "/mute" : "/unmute" )
, request_builder
);
if( result != null && result.object != null ){
new_status = TootStatus.parse( ActMain.this, access_info, result.object );
}
return result;
}
TootStatus new_status;
@Override protected void handleResult( TootApiResult result ){
if( result == null ){
// cancelled.
}else if( new_status != null ){
for( Column column : app_state.column_list ){
column.findStatus( access_info.host, new_status.id, new Column.StatusEntryCallback() {
@Override
public boolean onIterate( SavedAccount account, TootStatus status ){
if( access_info.acct.equalsIgnoreCase( account.acct ) ){
status.muted = bMute;
}
return true;
}
} );
}
Utils.showToast( ActMain.this, true, bMute ? R.string.mute_succeeded : R.string.unmute_succeeded );
}else{
Utils.showToast( ActMain.this, true, result.error );
}
}
}.executeOnExecutor( App1.task_executor );
}
////////////////////////////////////////
private void performAccountSetting(){
AccountPicker.pick( this, true, true
, getString( R.string.account_picker_open_setting )
, new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( @NonNull SavedAccount ai ){
ActAccountSetting.open( ActMain.this, ai, REQUEST_CODE_ACCOUNT_SETTING );
}
} );
}
////////////////////////////////////////////////////////
// column list
private void openColumnList(){
if( pager_adapter != null ){
ActColumnList.open( this, pager.getCurrentItem(), REQUEST_CODE_COLUMN_LIST );
}else{
ActColumnList.open( this, - 1, REQUEST_CODE_COLUMN_LIST );
}
}
////////////////////////////////////////////////////////////////////////////
interface RelationChangedCallback {
void onRelationChanged();
}
private static class RelationResult {
TootApiResult result;
@Nullable UserRelation relation;
}
private @Nullable
UserRelation saveUserRelation( @NonNull SavedAccount access_info, @Nullable TootRelationShip src ){
if( src == null ) return null;
long now = System.currentTimeMillis();
return UserRelation.save1( now, access_info.db_id, src );
}
// relationshipを取得
@NonNull RelationResult loadRelation1(
@NonNull TootApiClient client
, @NonNull SavedAccount access_info
, long who_id
){
RelationResult rr = new RelationResult();
TootApiResult r2 = rr.result = client.request( "/api/v1/accounts/relationships?id=" + who_id );
if( r2 != null && r2.array != null ){
TootRelationShip.List list = TootRelationShip.parseList( r2.array );
if( ! list.isEmpty() ){
rr.relation = saveUserRelation( access_info, list.get( 0 ) );
}
}
return rr;
}
public void callFollow(
int pos
, @NonNull final SavedAccount access_info
, @NonNull final TootAccount who
, final boolean bFollow
, @Nullable final RelationChangedCallback callback
){
callFollow( pos, access_info, who, bFollow, false, false, callback );
}
private void callFollow(
final int pos
, @NonNull final SavedAccount access_info
, @NonNull final TootAccount who
, final boolean bFollow
, final boolean bConfirmMoved
, final boolean bConfirmed
, @Nullable final RelationChangedCallback callback
){
if( access_info.isMe( who ) ){
Utils.showToast( this, false, R.string.it_is_you );
return;
}
if( ! bConfirmMoved && bFollow && who.moved != null ){
new AlertDialog.Builder( this )
.setMessage( getString( R.string.jump_moved_user
, access_info.getFullAcct( who )
, access_info.getFullAcct( who.moved )
) )
.setPositiveButton( R.string.ok, new DialogInterface.OnClickListener() {
@Override public void onClick( DialogInterface dialog, int which ){
openProfileFromAnotherAccount( pos, access_info, who.moved );
}
} )
.setNeutralButton( R.string.ignore_suggestion, new DialogInterface.OnClickListener() {
@Override public void onClick( DialogInterface dialog, int which ){
callFollow( pos, access_info, who, true, true, false, callback );
}
} )
.setNegativeButton( android.R.string.cancel, null )
.show();
return;
}
if( ! bConfirmed ){
if( bFollow && who.locked ){
DlgConfirm.open( this
, getString( R.string.confirm_follow_request_who_from, who.decoded_display_name, AcctColor.getNickname( access_info.acct ) )
, new DlgConfirm.Callback() {
@Override public boolean isConfirmEnabled(){
return access_info.confirm_follow_locked;
}
@Override public void setConfirmEnabled( boolean bv ){
access_info.confirm_follow_locked = bv;
access_info.saveSetting();
reloadAccountSetting( access_info );
}
@Override public void onOK(){
//noinspection ConstantConditions
callFollow( pos, access_info, who, bFollow, bConfirmMoved, true, callback );
}
}
);
return;
}else if( bFollow ){
String msg = getString( R.string.confirm_follow_who_from
, who.decoded_display_name
, AcctColor.getNickname( access_info.acct )
);
DlgConfirm.open( this
, msg
, new DlgConfirm.Callback() {
@Override public boolean isConfirmEnabled(){
return access_info.confirm_follow;
}
@Override public void setConfirmEnabled( boolean bv ){
access_info.confirm_follow = bv;
access_info.saveSetting();
reloadAccountSetting( access_info );
}
@Override public void onOK(){
//noinspection ConstantConditions
callFollow( pos, access_info, who, bFollow, bConfirmMoved, true, callback );
}
}
);
return;
}else{
DlgConfirm.open( this
, getString( R.string.confirm_unfollow_who_from, who.decoded_display_name, AcctColor.getNickname( access_info.acct ) )
, new DlgConfirm.Callback() {
@Override public boolean isConfirmEnabled(){
return access_info.confirm_unfollow;
}
@Override public void setConfirmEnabled( boolean bv ){
access_info.confirm_unfollow = bv;
access_info.saveSetting();
reloadAccountSetting( access_info );
}
@Override public void onOK(){
//noinspection ConstantConditions
callFollow( pos, access_info, who, bFollow, bConfirmMoved, true, callback );
}
}
);
return;
}
}
new TootApiTask( this, access_info, false ) {
@Override protected TootApiResult doInBackground( Void... params ){
TootApiResult result;
if( bFollow & who.acct.contains( "@" ) ){
// リモートフォローする
Request.Builder request_builder = new Request.Builder().post(
RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, "uri=" + Uri.encode( who.acct )
) );
result = client.request( "/api/v1/follows", request_builder );
if( result != null ){
if( result.object != null ){
TootAccount remote_who = TootAccount.parse( ActMain.this, access_info, result.object );
if( remote_who != null ){
RelationResult rr = loadRelation1( client, access_info, remote_who.id );
result = rr.result;
relation = rr.relation;
}
}
}
}else{
// ローカルでフォロー/アンフォローする
Request.Builder request_builder = new Request.Builder().post(
RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, "" // 空データ
) );
result = client.request( "/api/v1/accounts/" + who.id
+ ( bFollow ? "/follow" : "/unfollow" )
, request_builder );
if( result != null && result.object != null ){
relation = saveUserRelation( access_info, TootRelationShip.parse( result.object ) );
}
}
return result;
}
UserRelation relation;
@Override protected void handleResult( TootApiResult result ){
if( result == null ) return; // cancelled.
if( relation != null ){
showColumnMatchAccount( access_info );
if( bFollow && relation.getRequested( who ) ){
// 鍵付きアカウントにフォローリクエストを申請した状態
Utils.showToast( ActMain.this, false, R.string.follow_requested );
}else if( ! bFollow && relation.getRequested( who ) ){
Utils.showToast( ActMain.this, false, R.string.follow_request_cant_remove_by_sender );
}else{
// ローカル操作成功、もしくはリモートフォロー成功
if( callback != null ) callback.onRelationChanged();
}
}else if( bFollow && who.locked && result.response != null && result.response.code() == 422 ){
Utils.showToast( ActMain.this, false, R.string.cant_follow_locked_user );
}else{
Utils.showToast( ActMain.this, false, result.error );
}
}
}.executeOnExecutor( App1.task_executor );
}
// acct で指定したユーザをリモートフォローする
private void callRemoteFollow(
@NonNull final SavedAccount access_info
, @NonNull final String acct
, final boolean locked
, @Nullable final RelationChangedCallback callback
){
callRemoteFollow( access_info, acct, locked, false, callback );
}
// acct で指定したユーザをリモートフォローする
private void callRemoteFollow(
@NonNull final SavedAccount access_info
, @NonNull final String acct
, final boolean locked
, final boolean bConfirmed
, @Nullable final RelationChangedCallback callback
){
if( access_info.isMe( acct ) ){
Utils.showToast( this, false, R.string.it_is_you );
return;
}
if( ! bConfirmed ){
if( locked ){
DlgConfirm.open( this
, getString( R.string.confirm_follow_request_who_from, AcctColor.getNickname( acct ), AcctColor.getNickname( access_info.acct ) )
, new DlgConfirm.Callback() {
@Override public boolean isConfirmEnabled(){
return access_info.confirm_follow_locked;
}
@Override public void setConfirmEnabled( boolean bv ){
access_info.confirm_follow_locked = bv;
access_info.saveSetting();
reloadAccountSetting( access_info );
}
@Override public void onOK(){
//noinspection ConstantConditions
callRemoteFollow( access_info, acct, locked, true, callback );
}
}
);
return;
}else{
DlgConfirm.open( this
, getString( R.string.confirm_follow_who_from, AcctColor.getNickname( acct ), AcctColor.getNickname( access_info.acct ) )
, new DlgConfirm.Callback() {
@Override public boolean isConfirmEnabled(){
return access_info.confirm_follow;
}
@Override public void setConfirmEnabled( boolean bv ){
access_info.confirm_follow = bv;
access_info.saveSetting();
reloadAccountSetting();
}
@Override public void onOK(){
//noinspection ConstantConditions
callRemoteFollow( access_info, acct, locked, true, callback );
}
}
);
return;
}
}
new TootApiTask( this, access_info, false ) {
@Override protected TootApiResult doInBackground( Void... params ){
Request.Builder request_builder = new Request.Builder().post(
RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, "uri=" + Uri.encode( acct )
) );
TootApiResult result = client.request( "/api/v1/follows", request_builder );
if( result != null ){
if( result.object != null ){
remote_who = TootAccount.parse( ActMain.this, access_info, result.object );
if( remote_who != null ){
RelationResult rr = loadRelation1( client, access_info, remote_who.id );
result = rr.result;
relation = rr.relation;
}
}
}
return result;
}
TootAccount remote_who;
UserRelation relation;
@Override protected void handleResult( TootApiResult result ){
if( result == null ) return; // cancelled.
if( relation != null ){
showColumnMatchAccount( access_info );
if( callback != null ) callback.onRelationChanged();
}else if( locked && result.response != null && result.response.code() == 422 ){
Utils.showToast( ActMain.this, false, R.string.cant_follow_locked_user );
}else{
Utils.showToast( ActMain.this, false, result.error );
}
}
}.executeOnExecutor( App1.task_executor );
}
////////////////////////////////////////
void callMute(
@NonNull final SavedAccount access_info
, @NonNull final TootAccount who
, final boolean bMute
, final boolean bMuteNotification
){
if( access_info.isMe( who ) ){
Utils.showToast( this, false, R.string.it_is_you );
return;
}
new TootApiTask( this, access_info, true ) {
@Override protected TootApiResult doInBackground( Void... params ){
Request.Builder request_builder = new Request.Builder().post(
! bMute ? RequestBody.create( TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED, "" )
: bMuteNotification ? RequestBody.create( TootApiClient.MEDIA_TYPE_JSON, "{\"notifications\": true}" )
: RequestBody.create( TootApiClient.MEDIA_TYPE_JSON, "{\"notifications\": false}" )
);
TootApiResult result = client.request( "/api/v1/accounts/" + who.id + ( bMute ? "/mute" : "/unmute" )
, request_builder );
if( result != null && result.object != null ){
relation = saveUserRelation( access_info, TootRelationShip.parse( result.object ) );
}
return result;
}
UserRelation relation;
@Override protected void handleResult( TootApiResult result ){
if( result == null ) return; // cancelled.
if( relation != null ){
// 未確認だが、自分をミュートしようとするとリクエストは成功するがレスポンス中のmutingはfalseになるはず
if( bMute && ! relation.muting ){
Utils.showToast( ActMain.this, false, R.string.not_muted );
return;
}
if( relation.muting ){
for( Column column : app_state.column_list ){
column.removeAccountInTimeline( access_info, who.id );
}
}else{
for( Column column : app_state.column_list ){
column.removeFromMuteList( access_info, who.id );
}
}
Utils.showToast( ActMain.this, false, relation.muting ? R.string.mute_succeeded : R.string.unmute_succeeded );
}else{
Utils.showToast( ActMain.this, false, result.error );
}
}
}.executeOnExecutor( App1.task_executor );
}
void callBlock(
@NonNull final SavedAccount access_info
, @NonNull final TootAccount who
, final boolean bBlock
){
if( access_info.isMe( who ) ){
Utils.showToast( this, false, R.string.it_is_you );
return;
}
new TootApiTask( this, access_info, true ) {
@Override protected TootApiResult doInBackground( Void... params ){
Request.Builder request_builder = new Request.Builder().post(
RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, "" // 空データ
) );
TootApiResult result = client.request(
"/api/v1/accounts/" + who.id + ( bBlock ? "/block" : "/unblock" )
, request_builder
);
if( result != null ){
if( result.object != null ){
relation = saveUserRelation( access_info, TootRelationShip.parse( result.object ) );
}
}
return result;
}
UserRelation relation;
@Override protected void handleResult( TootApiResult result ){
if( result == null ) return; // cancelled.
if( relation != null ){
// 自分をブロックしようとすると、blocking==falseで帰ってくる
if( bBlock && ! relation.blocking ){
Utils.showToast( ActMain.this, false, R.string.not_blocked );
return;
}
for( Column column : app_state.column_list ){
if( relation.blocking ){
column.removeAccountInTimeline( access_info, who.id );
}else{
column.removeFromBlockList( access_info, who.id );
}
}
Utils.showToast( ActMain.this, false, relation.blocking ? R.string.block_succeeded : R.string.unblock_succeeded );
}else{
Utils.showToast( ActMain.this, false, result.error );
}
}
}.executeOnExecutor( App1.task_executor );
}
void callDomainBlock(
@NonNull final SavedAccount access_info
, @NonNull final String domain
, final boolean bBlock
){
if( access_info.host.equalsIgnoreCase( domain ) ){
Utils.showToast( this, false, R.string.it_is_you );
return;
}
new TootApiTask( this, access_info, true ) {
@Override protected TootApiResult doInBackground( Void... params ){
RequestBody body = RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, "domain=" + Uri.encode( domain )
);
Request.Builder request_builder = new Request.Builder();
request_builder = bBlock ? request_builder.post( body ) : request_builder.delete( body );
return client.request( "/api/v1/domain_blocks", request_builder );
}
@Override protected void handleResult( TootApiResult result ){
if( result == null ) return; // cancelled.
if( result.object != null ){
for( Column column : app_state.column_list ){
column.onDomainBlockChanged( access_info, domain, bBlock );
}
Utils.showToast( ActMain.this, false, bBlock ? R.string.block_succeeded : R.string.unblock_succeeded );
}else{
Utils.showToast( ActMain.this, false, result.error );
}
}
}.executeOnExecutor( App1.task_executor );
}
void callFollowRequestAuthorize(
@NonNull final SavedAccount access_info
, @NonNull final TootAccount who
, final boolean bAllow
){
if( access_info.isMe( who ) ){
Utils.showToast( this, false, R.string.it_is_you );
return;
}
new TootApiTask( this, access_info, true ) {
@Override protected TootApiResult doInBackground( Void... params ){
Request.Builder request_builder = new Request.Builder().post(
RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, "" // 空データ
) );
return client.request(
"/api/v1/follow_requests/" + who.id + ( bAllow ? "/authorize" : "/reject" )
, request_builder );
}
@Override protected void handleResult( TootApiResult result ){
if( result == null ) return; // cancelled.
if( result.object != null ){
for( Column column : app_state.column_list ){
column.removeFollowRequest( access_info, who.id );
}
Utils.showToast( ActMain.this, false, ( bAllow ? R.string.follow_request_authorized : R.string.follow_request_rejected ), who.decoded_display_name );
}else{
Utils.showToast( ActMain.this, false, result.error );
}
}
}.executeOnExecutor( App1.task_executor );
}
void deleteStatus( final SavedAccount access_info, final long status_id ){
new TootApiTask( this, access_info, true ) {
@Override protected TootApiResult doInBackground( Void... params ){
Request.Builder request_builder = new Request.Builder().delete(); // method is delete
return client.request( "/api/v1/statuses/" + status_id, request_builder );
}
@Override protected void handleResult( TootApiResult result ){
if( result == null ) return; // cancelled.
if( result.object != null ){
Utils.showToast( ActMain.this, false, R.string.delete_succeeded );
for( Column column : app_state.column_list ){
column.removeStatus( access_info, status_id );
}
}else{
Utils.showToast( ActMain.this, false, result.error );
}
}
}.executeOnExecutor( App1.task_executor );
}
interface ReportCompleteCallback {
void onReportComplete( TootApiResult result );
}
private void callReport(
@NonNull final SavedAccount access_info
, @NonNull final TootAccount who
, @NonNull final TootStatus status
, @NonNull final String comment
, @Nullable final ReportCompleteCallback callback
){
if( access_info.isMe( who ) ){
Utils.showToast( this, false, R.string.it_is_you );
return;
}
new TootApiTask( this, access_info, true ) {
@Override protected TootApiResult doInBackground( Void... params ){
String sb = "account_id=" + Long.toString( who.id )
+ "&comment=" + Uri.encode( comment )
+ "&status_ids[]=" + Long.toString( status.id );
Request.Builder request_builder = new Request.Builder().post(
RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, sb
) );
return client.request( "/api/v1/reports", request_builder );
}
@Override protected void handleResult( TootApiResult result ){
if( result == null ) return; // cancelled.
if( result.object != null ){
if( callback != null ) callback.onReportComplete( result );
}else{
Utils.showToast( ActMain.this, true, result.error );
}
}
}.executeOnExecutor( App1.task_executor );
}
public void callFollowingReblogs( @NonNull final SavedAccount access_info, @NonNull final TootAccount who, final boolean bShow ){
if( access_info.isMe( who ) ){
Utils.showToast( this, false, R.string.it_is_you );
return;
}
new TootApiTask( this, access_info, true ) {
@Override protected TootApiResult doInBackground( Void... params ){
JSONObject content = new JSONObject();
try{
content.put( "reblogs", bShow );
}catch( Throwable ex ){
return new TootApiResult( Utils.formatError( ex, "json encoding error" ) );
}
Request.Builder request_builder = new Request.Builder().post(
RequestBody.create(
TootApiClient.MEDIA_TYPE_JSON
, content.toString()
) );
TootApiResult result = client.request( "/api/v1/accounts/" + who.id + "/follow", request_builder );
if( result != null && result.object != null ){
relation = TootRelationShip.parse( result.object );
}
return result;
}
TootRelationShip relation;
@Override protected void handleResult( TootApiResult result ){
if( result == null ) return; // cancelled.
if( relation != null ){
saveUserRelation( access_info, relation );
Utils.showToast( ActMain.this, true, R.string.operation_succeeded );
}else{
Utils.showToast( ActMain.this, true, result.error );
}
}
}.executeOnExecutor( App1.task_executor );
}
void openReportForm( @NonNull final SavedAccount account, @NonNull final TootAccount who, @NonNull final TootStatus status ){
ReportForm.showReportForm( this, who, status, new ReportForm.ReportFormCallback() {
@Override public void startReport( final Dialog dialog, String comment ){
// レポートの送信を開始する
callReport( account, who, status, comment, new ReportCompleteCallback() {
@Override public void onReportComplete( TootApiResult result ){
// 成功したらダイアログを閉じる
try{
dialog.dismiss();
}catch( Throwable ignored ){
// IllegalArgumentException がたまに出る
}
Utils.showToast( ActMain.this, false, R.string.report_completed );
}
} );
}
} );
}
////////////////////////////////////////////////
////////////////////////////////////////////////
final RelationChangedCallback follow_complete_callback = new RelationChangedCallback() {
@Override public void onRelationChanged(){
Utils.showToast( ActMain.this, false, R.string.follow_succeeded );
}
};
final RelationChangedCallback unfollow_complete_callback = new RelationChangedCallback() {
@Override public void onRelationChanged(){
Utils.showToast( ActMain.this, false, R.string.unfollow_succeeded );
}
};
final ActMain.RelationChangedCallback favourite_complete_callback = new ActMain.RelationChangedCallback() {
@Override public void onRelationChanged(){
Utils.showToast( ActMain.this, false, R.string.favourite_succeeded );
}
};
final ActMain.RelationChangedCallback unfavourite_complete_callback = new ActMain.RelationChangedCallback() {
@Override public void onRelationChanged(){
Utils.showToast( ActMain.this, false, R.string.unfavourite_succeeded );
}
};
final ActMain.RelationChangedCallback boost_complete_callback = new ActMain.RelationChangedCallback() {
@Override public void onRelationChanged(){
Utils.showToast( ActMain.this, false, R.string.boost_succeeded );
}
};
final ActMain.RelationChangedCallback unboost_complete_callback = new ActMain.RelationChangedCallback() {
@Override public void onRelationChanged(){
Utils.showToast( ActMain.this, false, R.string.unboost_succeeded );
}
};
private void openOSSLicense(){
startActivity( new Intent( this, ActOSSLicense.class ) );
}
private void openAppAbout(){
startActivityForResult( new Intent( this, ActAbout.class ), REQUEST_APP_ABOUT );
}
public void deleteNotification( boolean bConfirmed, final SavedAccount target_account ){
if( ! bConfirmed ){
new AlertDialog.Builder( this )
.setMessage( R.string.confirm_delete_notification )
.setNegativeButton( R.string.cancel, null )
.setPositiveButton( R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick( DialogInterface dialog, int which ){
deleteNotification( true, target_account );
}
} )
.show();
return;
}
new TootApiTask( this, target_account, true ) {
@Override protected TootApiResult doInBackground( Void... params ){
Request.Builder request_builder = new Request.Builder().post(
RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, "" // 空データ
) );
return client.request( "/api/v1/notifications/clear", request_builder );
}
@Override protected void handleResult( TootApiResult result ){
if( result == null ) return; // cancelled.
if( result.object != null ){
// ok. api have return empty object.
for( Column column : app_state.column_list ){
if( column.column_type == Column.TYPE_NOTIFICATIONS
&& column.access_info.acct.equals( target_account.acct )
){
column.removeNotifications();
}
}
Utils.showToast( ActMain.this, false, R.string.delete_succeeded );
}else{
Utils.showToast( ActMain.this, false, result.error );
}
}
}.executeOnExecutor( App1.task_executor );
}
private void showFooterColor(){
int footer_button_bg_color = pref.getInt( Pref.KEY_FOOTER_BUTTON_BG_COLOR, 0 );
int footer_button_fg_color = pref.getInt( Pref.KEY_FOOTER_BUTTON_FG_COLOR, 0 );
int footer_tab_bg_color = pref.getInt( Pref.KEY_FOOTER_TAB_BG_COLOR, 0 );
int footer_tab_divider_color = pref.getInt( Pref.KEY_FOOTER_TAB_DIVIDER_COLOR, 0 );
int footer_tab_indicator_color = pref.getInt( Pref.KEY_FOOTER_TAB_INDICATOR_COLOR, 0 );
int c = footer_button_bg_color;
if( c == 0 ){
btnMenu.setBackgroundResource( R.drawable.btn_bg_ddd );
btnToot.setBackgroundResource( R.drawable.btn_bg_ddd );
btnQuickToot.setBackgroundResource( R.drawable.btn_bg_ddd );
}else{
int fg = ( footer_button_fg_color != 0
? footer_button_fg_color
: Styler.getAttributeColor( this, R.attr.colorRippleEffect ) );
ViewCompat.setBackground( btnToot, Styler.getAdaptiveRippleDrawable( c, fg ) );
ViewCompat.setBackground( btnMenu, Styler.getAdaptiveRippleDrawable( c, fg ) );
ViewCompat.setBackground( btnQuickToot, Styler.getAdaptiveRippleDrawable( c, fg ) );
}
c = footer_button_fg_color;
if( c == 0 ){
Styler.setIconDefaultColor( this, btnToot, R.attr.ic_edit );
Styler.setIconDefaultColor( this, btnMenu, R.attr.ic_hamburger );
Styler.setIconDefaultColor( this, btnQuickToot, R.attr.btn_post );
}else{
Styler.setIconCustomColor( this, btnToot, c, R.attr.ic_edit );
Styler.setIconCustomColor( this, btnMenu, c, R.attr.ic_hamburger );
Styler.setIconCustomColor( this, btnQuickToot, c, R.attr.btn_post );
}
c = footer_tab_bg_color;
if( c == 0 ){
svColumnStrip.setBackgroundColor( Styler.getAttributeColor( this, R.attr.colorColumnStripBackground ) );
llQuickTootBar.setBackgroundColor( Styler.getAttributeColor( this, R.attr.colorColumnStripBackground ) );
}else{
svColumnStrip.setBackgroundColor( c );
svColumnStrip.setBackgroundColor( Styler.getAttributeColor( this, R.attr.colorColumnStripBackground ) );
}
c = footer_tab_divider_color;
if( c == 0 ){
vFooterDivider1.setBackgroundColor( Styler.getAttributeColor( this, R.attr.colorImageButton ) );
vFooterDivider2.setBackgroundColor( Styler.getAttributeColor( this, R.attr.colorImageButton ) );
}else{
vFooterDivider1.setBackgroundColor( c );
vFooterDivider2.setBackgroundColor( c );
}
c = footer_tab_indicator_color;
llColumnStrip.setColor( c );
}
public ArrayList< SavedAccount > makeAccountListNonPseudo( @NonNull LogCategory log, @Nullable String pickup_host ){
ArrayList< SavedAccount > list_same_host = new ArrayList<>();
ArrayList< SavedAccount > list_other_host = new ArrayList<>();
for( SavedAccount a : SavedAccount.loadAccountList( ActMain.this, log ) ){
if( a.isPseudo() ) continue;
( pickup_host == null || pickup_host.equalsIgnoreCase( a.host ) ? list_same_host : list_other_host ).add( a );
}
SavedAccount.sort( list_same_host );
SavedAccount.sort( list_other_host );
list_same_host.addAll( list_other_host );
return list_same_host;
}
// 別アカ操作と別タンスの関係
static final int NOT_CROSS_ACCOUNT = 1;
static final int CROSS_ACCOUNT_SAME_INSTANCE = 2;
static final int CROSS_ACCOUNT_REMOTE_INSTANCE = 3;
int calcCrossAccountMode( @NonNull final SavedAccount timeline_account, @NonNull final SavedAccount action_account ){
if( ! timeline_account.host.equalsIgnoreCase( action_account.host ) ){
return CROSS_ACCOUNT_REMOTE_INSTANCE;
}else if( ! timeline_account.acct.equalsIgnoreCase( action_account.acct ) ){
return CROSS_ACCOUNT_SAME_INSTANCE;
}else{
return NOT_CROSS_ACCOUNT;
}
}
void openBoostFromAnotherAccount( @NonNull final SavedAccount timeline_account, @Nullable final TootStatusLike status ){
if( status == null ) return;
String who_host = status.account == null ? null : timeline_account.getAccountHost( status.account );
AccountPicker.pick( this, false, false
, getString( R.string.account_picker_boost )
, makeAccountListNonPseudo( log, who_host )
, new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( @NonNull SavedAccount action_account ){
performBoost(
action_account
, status
, calcCrossAccountMode( timeline_account, action_account )
, true
, false
, boost_complete_callback
);
}
} );
}
void openFavouriteFromAnotherAccount( @NonNull final SavedAccount timeline_account, @Nullable final TootStatusLike status ){
if( status == null ) return;
String who_host = status.account == null ? null : timeline_account.getAccountHost( status.account );
AccountPicker.pick( this, false, false
, getString( R.string.account_picker_favourite )
, makeAccountListNonPseudo( log, who_host )
, new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( @NonNull SavedAccount action_account ){
performFavourite(
action_account
, status
, calcCrossAccountMode( timeline_account, action_account )
, true
, favourite_complete_callback
);
}
} );
}
void openReplyFromAnotherAccount( @NonNull final SavedAccount timeline_account, @Nullable final TootStatusLike status ){
if( status == null ) return;
String who_host = status.account == null ? null : timeline_account.getAccountHost( status.account );
AccountPicker.pick( this, false, false
, getString( R.string.account_picker_reply )
, makeAccountListNonPseudo( log, who_host )
, new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( @NonNull SavedAccount ai ){
if( status instanceof TootStatus ){
if( ai.host.equalsIgnoreCase( status.host_access ) ){
// アクセス元ホストが同じならステータスIDを使って返信できる
performReply( ai, (TootStatus) status );
return;
}
}
// それ以外の場合、ステータスのURLを検索APIに投げることで返信できる
performReplyRemote( ai, status.url );
}
} );
}
// void openReplyFromAnotherAccount( @NonNull final SavedAccount access_info, final String status_url,final long status_id ){
//
// final String status_host = getHostFromStatusUrl(status_url);
// if( status_host ==null ) return;
//
// AccountPicker.pick( this, false, false
// , getString( R.string.account_picker_reply )
// , makeAccountListNonPseudo( log ), new AccountPicker.AccountPickerCallback() {
// @Override public void onAccountPicked( @NonNull SavedAccount ai ){
// performReplyRemote( ai,status_url,status_id );
// }
// } );
// }
// void openFollowFromAnotherAccount( @NonNull SavedAccount access_info, TootStatus status ){
// if( status == null ) return;
// openFollowFromAnotherAccount( access_info, status.account );
// }
void openFollowFromAnotherAccount( int pos, @NonNull SavedAccount access_info, @Nullable final TootAccount account ){
openFollowFromAnotherAccount( pos, access_info, account, false );
}
void openFollowFromAnotherAccount( final int pos, @NonNull final SavedAccount access_info, @Nullable final TootAccount account, final boolean bConfirmMoved ){
if( account == null ) return;
if( ! bConfirmMoved && account.moved != null ){
new AlertDialog.Builder( this )
.setMessage( getString( R.string.jump_moved_user
, access_info.getFullAcct( account )
, access_info.getFullAcct( account.moved )
) )
.setPositiveButton( R.string.ok, new DialogInterface.OnClickListener() {
@Override public void onClick( DialogInterface dialog, int which ){
openProfileFromAnotherAccount( pos, access_info, account.moved );
}
} )
.setNeutralButton( R.string.ignore_suggestion, new DialogInterface.OnClickListener() {
@Override public void onClick( DialogInterface dialog, int which ){
openFollowFromAnotherAccount( pos, access_info, account, true );
}
} )
.setNegativeButton( android.R.string.cancel, null )
.show();
return;
}
final String who_host = access_info.getAccountHost( account );
final String who_acct = access_info.getFullAcct( account );
AccountPicker.pick( this, false, false
, getString( R.string.account_picker_follow )
, makeAccountListNonPseudo( log, who_host )
, new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( @NonNull SavedAccount ai ){
callRemoteFollow( ai, who_acct, account.locked, follow_complete_callback );
}
} );
}
/////////////////////////////////////////////////////////////////////////
// タブレット対応で必要になった関数など
private boolean closeColumnSetting(){
if( pager_adapter != null ){
ColumnViewHolder vh = pager_adapter.getColumnViewHolder( pager.getCurrentItem() );
if( vh != null && vh.isColumnSettingShown() ){
vh.closeColumnSetting();
return true;
}
}else{
for( int i = 0, ie = tablet_layout_manager.getChildCount() ; i < ie ; ++ i ){
View v = tablet_layout_manager.getChildAt( i );
TabletColumnViewHolder holder = (TabletColumnViewHolder) tablet_pager.getChildViewHolder( v );
if( holder != null && holder.vh.isColumnSettingShown() ){
holder.vh.closeColumnSetting();
return true;
}
}
}
return false;
}
private int getDefaultInsertPosition(){
if( pager_adapter != null ){
return 1 + pager.getCurrentItem();
}else{
return Integer.MAX_VALUE;
}
}
int nextPosition( Column column ){
if( column != null ){
int pos = app_state.column_list.indexOf( column );
if( pos != - 1 ) return pos + 1;
}
return getDefaultInsertPosition();
}
private int addColumn( Column column, int index ){
int size = app_state.column_list.size();
if( index > size ) index = size;
if( pager_adapter != null ){
pager.setAdapter( null );
app_state.column_list.add( index, column );
pager.setAdapter( pager_adapter );
}else{
app_state.column_list.add( index, column );
resizeColumnWidth();
}
app_state.saveColumnList();
updateColumnStrip();
return index;
}
private void removeColumn( Column column ){
int idx_column = app_state.column_list.indexOf( column );
if( idx_column == - 1 ) return;
if( pager_adapter != null ){
pager.setAdapter( null );
app_state.column_list.remove( idx_column ).dispose();
pager.setAdapter( pager_adapter );
}else{
app_state.column_list.remove( idx_column ).dispose();
resizeColumnWidth();
}
app_state.saveColumnList();
updateColumnStrip();
}
private void setOrder( ArrayList< Integer > new_order ){
if( pager_adapter != null ){
pager.setAdapter( null );
}
ArrayList< Column > tmp_list = new ArrayList<>();
HashSet< Integer > used_set = new HashSet<>();
for( Integer i : new_order ){
used_set.add( i );
tmp_list.add( app_state.column_list.get( i ) );
}
for( int i = 0, ie = app_state.column_list.size() ; i < ie ; ++ i ){
if( used_set.contains( i ) ) continue;
app_state.column_list.get( i ).dispose();
}
app_state.column_list.clear();
app_state.column_list.addAll( tmp_list );
if( pager_adapter != null ){
pager.setAdapter( pager_adapter );
}else{
resizeColumnWidth();
}
app_state.saveColumnList();
updateColumnStrip();
}
int nScreenColumn;
int nColumnWidth;
private void resizeColumnWidth(){
int column_w_min_dp = COLUMN_WIDTH_MIN_DP;
String sv = pref.getString( Pref.KEY_COLUMN_WIDTH, "" );
if( ! TextUtils.isEmpty( sv ) ){
try{
int iv = Integer.parseInt( sv );
if( iv >= 100 ){
column_w_min_dp = iv;
}
}catch( Throwable ex ){
log.trace( ex );
}
}
DisplayMetrics dm = getResources().getDisplayMetrics();
final int sw = dm.widthPixels;
float density = dm.density;
int column_w_min = (int) ( 0.5f + column_w_min_dp * density );
if( column_w_min < 1 ) column_w_min = 1;
if( sw < column_w_min * 2 ){
// 最小幅で2つ表示できないのなら1カラム表示
tablet_pager_adapter.setColumnWidth( sw );
resizeAutoCW( sw );
}else{
// カラム最小幅から計算した表示カラム数
nScreenColumn = sw / column_w_min;
if( nScreenColumn < 1 ) nScreenColumn = 1;
// データのカラム数より大きくならないようにする
// (でも最小は1)
int column_count = app_state.column_list.size();
if( column_count > 0 ){
if( nScreenColumn > column_count ){
nScreenColumn = column_count;
}
}
// 表示カラム数から計算したカラム幅
int column_w = sw / nScreenColumn;
// 最小カラム幅の1.5倍よりは大きくならないようにする
int column_w_max = (int) ( 0.5f + column_w_min * 1.5f );
if( column_w > column_w_max ){
column_w = column_w_max;
}
resizeAutoCW( column_w );
nColumnWidth = column_w;
tablet_pager_adapter.setColumnWidth( column_w );
tablet_snap_helper.setColumnWidth( column_w );
}
// 並べ直す
tablet_pager_adapter.notifyDataSetChanged();
}
private void scrollToColumn( int index, boolean bAlign ){
scrollColumnStrip( index );
if( pager_adapter != null ){
pager.setCurrentItem( index, true );
}else if( ! bAlign ){
// 指定したカラムが画面内に表示されるように動いてくれるようだ
tablet_pager.smoothScrollToPosition( index );
}else{
// 指定位置が表示範囲の左端にくるようにスクロール
tablet_pager.scrollToPosition( index );
}
}
//////////////////////////////////////////////////////////////////////////////////////////////
private void importAppData( final Uri uri ){
// remove all columns
{
if( pager_adapter != null ){
pager.setAdapter( null );
}
for( Column c : app_state.column_list ){
c.dispose();
}
app_state.column_list.clear();
if( pager_adapter != null ){
pager.setAdapter( pager_adapter );
}else{
resizeColumnWidth();
}
updateColumnStrip();
}
//noinspection deprecation
final ProgressDialog progress = new ProgressDialog( this );
final AsyncTask< Void, String, ArrayList< Column > > task = new AsyncTask< Void, String, ArrayList< Column > >() {
void setProgressMessage( final String sv ){
Utils.runOnMainThread( new Runnable() {
@Override public void run(){
progress.setMessage( sv );
}
} );
}
@Override protected ArrayList< Column > doInBackground( Void... params ){
try{
setProgressMessage( "import data to local storage..." );
File cache_dir = getCacheDir();
//noinspection ResultOfMethodCallIgnored
cache_dir.mkdir();
File file = new File( cache_dir, "SubwayTooter." + android.os.Process.myPid() + "." + android.os.Process.myTid() + ".json" );
// ローカルファイルにコピーする
InputStream is = getContentResolver().openInputStream( uri );
if( is == null ){
Utils.showToast( ActMain.this, true, "openInputStream failed." );
return null;
}
try{
FileOutputStream os = new FileOutputStream( file );
try{
IOUtils.copy( is, os );
}finally{
IOUtils.closeQuietly( os );
}
}finally{
IOUtils.closeQuietly( is );
}
// 通知サービスを止める
setProgressMessage( "reset Notification..." );
PollingWorker.queueAppDataImportBefore( ActMain.this );
while( PollingWorker.mBusyAppDataImportBefore.get() ){
Thread.sleep( 100L );
}
// JSONを読みだす
setProgressMessage( "reading app data..." );
Reader r = new InputStreamReader( new FileInputStream( file ), "UTF-8" );
try{
JsonReader reader = new JsonReader( r );
return AppDataExporter.decodeAppData( ActMain.this, reader );
}finally{
IOUtils.closeQuietly( r );
}
}catch( Throwable ex ){
log.trace( ex );
Utils.showToast( ActMain.this, ex, "importAppData failed." );
}
return null;
}
@Override protected void onCancelled( ArrayList< Column > result ){
onPostExecute( result );
}
@Override protected void onPostExecute( ArrayList< Column > result ){
try{
progress.dismiss();
}catch( Throwable ignored ){
}
try{
getWindow().clearFlags( WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON );
}catch( Throwable ignored ){
}
if( isCancelled() || result == null ){
// cancelled.
return;
}
{
if( pager_adapter != null ){
pager.setAdapter( null );
}
app_state.column_list.clear();
app_state.column_list.addAll( result );
app_state.saveColumnList();
if( pager_adapter != null ){
pager.setAdapter( pager_adapter );
}else{
resizeColumnWidth();
}
updateColumnStrip();
}
// 通知サービスをリスタート
PollingWorker.queueAppDataImportAfter( ActMain.this );
}
};
try{
getWindow().addFlags( WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON );
}catch( Throwable ignored ){
}
progress.setIndeterminate( true );
progress.setCancelable( false );
progress.setOnCancelListener( new DialogInterface.OnCancelListener() {
@Override public void onCancel( DialogInterface dialog ){
task.cancel( true );
}
} );
progress.show();
task.executeOnExecutor( App1.task_executor );
}
public void openTimelineFor( @NonNull String host ){
final ArrayList< SavedAccount > account_list = new ArrayList<>();
for( SavedAccount a : SavedAccount.loadAccountList( ActMain.this, log ) ){
if( host.equalsIgnoreCase( a.host ) ) account_list.add( a );
}
if( account_list.isEmpty() ){
SavedAccount ai = addPseudoAccount( host );
if( ai != null ){
addColumn( getDefaultInsertPosition(), ai, Column.TYPE_LOCAL );
}
}else{
SavedAccount.sort( account_list );
AccountPicker.pick( this, true, false
, getString( R.string.account_picker_add_timeline_of, host )
, account_list
, new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( @NonNull SavedAccount ai ){
addColumn( getDefaultInsertPosition(), ai, Column.TYPE_LOCAL );
}
} );
}
}
@Override public void onDrawerSlide( View drawerView, float slideOffset ){
if( post_helper != null ){
post_helper.closeAcctPopup();
}
}
@Override public void onDrawerOpened( View drawerView ){
if( post_helper != null ){
post_helper.closeAcctPopup();
}
}
@Override public void onDrawerClosed( View drawerView ){
if( post_helper != null ){
post_helper.closeAcctPopup();
}
}
@Override public void onDrawerStateChanged( int newState ){
if( post_helper != null ){
post_helper.closeAcctPopup();
}
}
public void openInstanceInformation( int pos, String host ){
addColumn( pos, SavedAccount.getNA(), Column.TYPE_INSTANCE_INFORMATION, host );
}
private final Runnable proc_updateRelativeTime = new Runnable() {
@Override public void run(){
handler.removeCallbacks( proc_updateRelativeTime );
if( ! bStart ) return;
for( Column c : app_state.column_list ){
c.fireShowContent();
}
if( pref.getBoolean( Pref.KEY_RELATIVE_TIMESTAMP, false ) ){
handler.postDelayed( proc_updateRelativeTime, 10000L );
}
}
};
int nAutoCwCellWidth = 0;
int nAutoCwLines = 0;
private void resizeAutoCW( int column_w ){
String sv = pref.getString( Pref.KEY_AUTO_CW_LINES, "" );
nAutoCwLines = Utils.parse_int( sv, - 1 );
if( nAutoCwLines > 0 ){
int lv_pad = (int) ( 0.5f + 12 * density );
int icon_width = mAvatarIconSize;
int icon_end = (int) ( 0.5f + 4 * density );
nAutoCwCellWidth = column_w - lv_pad * 2 - icon_width - icon_end;
}
// この後各カラムは再描画される
}
void checkAutoCW( @NonNull TootStatusLike status, @NonNull CharSequence text ){
if( nAutoCwCellWidth <= 0 ){
// 設定が無効
status.auto_cw = null;
return;
}
TootStatusLike.AutoCW a = status.auto_cw;
if( a != null && a.refActivity.get() == ActMain.this && a.cell_width == nAutoCwCellWidth ){
// 以前に計算した値がまだ使える
return;
}
if( a == null ) a = status.auto_cw = new TootStatusLike.AutoCW();
// 計算時の条件(文字フォント、文字サイズ、カラム幅)を覚えておいて、再利用時に同じか確認する
a.refActivity = new WeakReference< Object >( ActMain.this );
a.cell_width = nAutoCwCellWidth;
a.decoded_spoiler_text = null;
// テキストをレイアウトして行数を測定
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( nAutoCwCellWidth, LinearLayout.LayoutParams.WRAP_CONTENT );
TextView tv = new TextView( this );
tv.setLayoutParams( lp );
if( ! Float.isNaN( timeline_font_size_sp ) ){
tv.setTextSize( timeline_font_size_sp );
}
if( timeline_font != null ){
tv.setTypeface( timeline_font );
}
tv.setText( text );
tv.measure(
View.MeasureSpec.makeMeasureSpec( nAutoCwCellWidth, View.MeasureSpec.EXACTLY )
, View.MeasureSpec.makeMeasureSpec( 0, View.MeasureSpec.UNSPECIFIED )
);
Layout l = tv.getLayout();
if( l != null ){
int line_count = a.originalLineCount = l.getLineCount();
if( nAutoCwLines > 0
&& line_count > nAutoCwLines
&& TextUtils.isEmpty( status.spoiler_text )
){
SpannableStringBuilder sb = new SpannableStringBuilder();
sb.append( getString( R.string.auto_cw_prefix ) );
sb.append( text, 0, l.getLineEnd( nAutoCwLines - 1 ) );
int last = sb.length();
while( last > 0 ){
char c = sb.charAt( last - 1 );
if( c == '\n' || Character.isWhitespace( c ) ){
-- last;
continue;
}
break;
}
if( last < sb.length() ){
sb.delete( last, sb.length() );
}
sb.append( '…' );
a.decoded_spoiler_text = sb;
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////
public void createNewList( @NonNull final SavedAccount access_info, @NonNull final String title ){
new TootApiTask( this, access_info, true ) {
@Override protected TootApiResult doInBackground( Void... params ){
JSONObject content = new JSONObject();
try{
content.put( "title", title );
}catch( Throwable ex ){
return new TootApiResult( Utils.formatError( ex, "can't encoding json parameter." ) );
}
Request.Builder request_builder = new Request.Builder().post(
RequestBody.create(
TootApiClient.MEDIA_TYPE_JSON
, content.toString()
) );
TootApiResult result = client.request( "/api/v1/lists", request_builder );
if( result != null ){
if( result.object != null ){
list = TootList.parse( result.object );
}
}
return result;
}
TootList list;
@Override protected void handleResult( TootApiResult result ){
if( result == null ) return; // cancelled.
if( list != null ){
for( Column column : app_state.column_list ){
column.onListListUpdated( access_info );
}
Utils.showToast( ActMain.this, false, R.string.list_created );
}else{
Utils.showToast( ActMain.this, false, result.error );
}
}
}.executeOnExecutor( App1.task_executor );
}
public void callDeleteList( @NonNull final SavedAccount access_info, final long list_id ){
new TootApiTask( this, access_info, true ) {
@Override protected TootApiResult doInBackground( Void... params ){
return client.request( "/api/v1/lists/" + list_id, new Request.Builder().delete() );
}
@Override protected void handleResult( TootApiResult result ){
if( result == null ) return; // cancelled.
if( result.object != null ){
for( Column column : app_state.column_list ){
column.onListListUpdated( access_info );
}
Utils.showToast( ActMain.this, false, R.string.delete_succeeded );
}else{
Utils.showToast( ActMain.this, false, result.error );
}
}
}.executeOnExecutor( App1.task_executor );
}
public interface ListMemberCallback {
void onListMemberUpdated( boolean willRegistered, boolean bSuccess );
}
static final Pattern reFollowError = Pattern.compile( "follow", Pattern.CASE_INSENSITIVE );
public void callListMemberAdd(
@NonNull final SavedAccount access_info
, final long list_id
, @NonNull final TootAccount local_who
, final boolean bFollow
, @Nullable final ListMemberCallback callback
){
new TootApiTask( this, access_info, true ) {
@Override protected TootApiResult doInBackground( Void... params ){
if( access_info.isMe( local_who ) ){
return new TootApiResult( getString( R.string.it_is_you ) );
}
TootApiResult result;
if( bFollow ){
TootRelationShip relation;
if( access_info.isLocalUser( local_who ) ){
Request.Builder request_builder = new Request.Builder().post(
RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, "" // 空データ
) );
result = client.request( "/api/v1/accounts/" + local_who.id + "/follow", request_builder );
}else{
// リモートフォローする
Request.Builder request_builder = new Request.Builder().post(
RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, "uri=" + Uri.encode( local_who.acct )
) );
result = client.request( "/api/v1/follows", request_builder );
if( result == null || result.object == null ) return result;
TootAccount a = TootAccount.parse( ActMain.this, access_info, result.object );
if( a == null ){
return new TootApiResult( "parse error." );
}
// リモートフォローの後にリレーションシップを取得しなおす
result = client.request( "/api/v1/accounts/relationships?id[]=" + a.id );
}
if( result == null || result.array == null ){
return result;
}
TootRelationShip.List relation_list = TootRelationShip.parseList( result.array );
relation = relation_list.isEmpty() ? null : relation_list.get( 0 );
if( relation == null ){
return new TootApiResult( "parse error." );
}
saveUserRelation( access_info, relation );
if( ! relation.following ){
if( relation.requested ){
return new TootApiResult( getString( R.string.cant_add_list_follow_requesting ) );
}else{
// リモートフォローの場合、正常ケースでもここを通る場合がある
// 何もしてはいけない…
}
}
}
// リストメンバー追加
JSONObject content = new JSONObject();
try{
JSONArray account_ids = new JSONArray();
account_ids.put( Long.toString( local_who.id ) );
content.put( "account_ids", account_ids );
}catch( Throwable ex ){
return new TootApiResult( Utils.formatError( ex, "can't encoding json parameter." ) );
}
Request.Builder request_builder = new Request.Builder().post(
RequestBody.create(
TootApiClient.MEDIA_TYPE_JSON
, content.toString()
) );
return client.request( "/api/v1/lists/" + list_id + "/accounts", request_builder );
}
@Override protected void handleResult( TootApiResult result ){
boolean bSuccess = false;
try{
//noinspection StatementWithEmptyBody
if( result == null ) return; // cancelled.
if( result.object != null ){
for( Column column : app_state.column_list ){
// リストメンバー追加イベントをカラムに伝達
column.onListMemberUpdated( access_info, list_id, local_who, true );
}
// フォロー状態の更新を表示に反映させる
if( bFollow ) showColumnMatchAccount( access_info );
Utils.showToast( ActMain.this, false, R.string.list_member_added );
bSuccess = true;
}else{
if( result.response != null
&& result.response.code() == 422
&& result.error != null && reFollowError.matcher( result.error ).find()
){
if( ! bFollow ){
DlgConfirm.openSimple(
ActMain.this
, getString( R.string.list_retry_with_follow, access_info.getFullAcct( local_who ) )
, new Runnable() {
@Override public void run(){
callListMemberAdd( access_info, list_id, local_who, true, callback );
}
}
);
}else{
new android.app.AlertDialog.Builder( ActMain.this )
.setCancelable( true )
.setMessage( R.string.cant_add_list_follow_requesting )
.setNeutralButton( R.string.close, null )
.show();
}
return;
}
Utils.showToast( ActMain.this, true, result.error );
}
}finally{
if( callback != null ) callback.onListMemberUpdated( true, bSuccess );
}
}
}.executeOnExecutor( App1.task_executor );
}
public void callListMemberDelete(
@NonNull final SavedAccount access_info
, final long list_id
, @NonNull final TootAccount local_who
, @Nullable final ListMemberCallback callback
){
new TootApiTask( this, access_info, true ) {
@Override protected TootApiResult doInBackground( Void... params ){
return client.request(
"/api/v1/lists/" + list_id + "/accounts?account_ids[]=" + local_who.id
, new Request.Builder().delete()
);
}
@Override protected void handleResult( TootApiResult result ){
boolean bSuccess = false;
try{
if( result == null ) return; // cancelled.
if( result.object != null ){
for( Column column : app_state.column_list ){
column.onListMemberUpdated( access_info, list_id, local_who, false );
}
Utils.showToast( ActMain.this, false, R.string.delete_succeeded );
bSuccess = true;
}else{
Utils.showToast( ActMain.this, false, result.error );
}
}finally{
if( callback != null ) callback.onListMemberUpdated( false, bSuccess );
}
}
}.executeOnExecutor( App1.task_executor );
}
}