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

2020 lines
62 KiB
Java

package jp.juggler.subwaytooter;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.customtabs.CustomTabsIntent;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.os.AsyncTaskCompat;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AlertDialog;
import android.text.TextUtils;
import android.view.Gravity;
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.Window;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jp.juggler.subwaytooter.api.TootApiClient;
import jp.juggler.subwaytooter.api.TootApiResult;
import jp.juggler.subwaytooter.api.entity.TootAccount;
import jp.juggler.subwaytooter.api.entity.TootRelationShip;
import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.dialog.AccountPicker;
import jp.juggler.subwaytooter.dialog.LoginForm;
import jp.juggler.subwaytooter.dialog.ReportForm;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.ActionsDialog;
import jp.juggler.subwaytooter.util.HTMLDecoder;
import jp.juggler.subwaytooter.util.LinkClickContext;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
import okhttp3.Request;
import okhttp3.RequestBody;
public class ActMain extends AppCompatActivity
implements NavigationView.OnNavigationItemSelectedListener {
public static final LogCategory log = new LogCategory( "ActMain" );
static boolean update_at_resume = false;
// @Override
// protected void attachBaseContext(Context newBase) {
// super.attachBaseContext( CalligraphyContextWrapper.wrap(newBase));
// }
float density;
SharedPreferences pref;
Handler handler;
@Override
protected void onCreate( Bundle savedInstanceState ){
super.onCreate( savedInstanceState );
App1.setActivityTheme(this,true);
this.density = getResources().getDisplayMetrics().density;
requestWindowFeature( Window.FEATURE_NO_TITLE );
pref = Pref.pref( this );
handler = new Handler( );
initUI();
AlarmService.startCheck( this );
loadColumnList();
}
@Override protected void onDestroy(){
super.onDestroy();
}
@Override protected void onResume(){
super.onResume();
HTMLDecoder.link_callback = link_click_listener;
// アカウント設定から戻ってきたら、カラムを消す必要があるかもしれない
{
ArrayList< Integer > new_order = new ArrayList<>();
boolean bRemoved = false;
for( int i = 0, ie = pager_adapter.getCount() ; i < ie ; ++ i ){
Column column = pager_adapter.getColumn( i );
SavedAccount sa = SavedAccount.loadAccount( log, column.access_info.db_id );
if( sa == null ){
bRemoved = true;
}else{
new_order.add( i );
}
}
if( bRemoved ){
pager_adapter.setOrder( pager, new_order );
saveColumnList();
}
}
// 各カラムのアカウント設定を読み直す
reloadAccountSetting();
if( update_at_resume ){
update_at_resume = false;
// TODO: 各カラムを更新する
}
if( pager_adapter.getCount() == 0 ){
llEmpty.setVisibility( View.VISIBLE );
}
Uri uri = ActOAuthCallback.last_uri.getAndSet( null );
if( uri != null ){
handleIntentUri( uri );
}
}
@Override protected void onPause(){
HTMLDecoder.link_callback = null;
super.onPause();
}
boolean isOrderChanged( ArrayList< Integer > new_order ){
if( new_order.size() != pager_adapter.getCount() ) 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 REQUEST_CODE_COLUMN_LIST = 1;
static final int REQUEST_CODE_ACCOUNT_SETTING = 2;
static final int REQUEST_APP_ABOUT = 3;
@Override
protected void onActivityResult( int requestCode, int resultCode, Intent data ){
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 ) ){
pager_adapter.setOrder( pager, order );
saveColumnList();
}
if( pager_adapter.column_list.isEmpty() ){
llEmpty.setVisibility( View.VISIBLE );
}else{
int select = data.getIntExtra( ActColumnList.EXTRA_SELECTION, - 1 );
if( select != - 1 ){
pager.setCurrentItem( select, true );
}
}
}
}else if( requestCode == REQUEST_CODE_ACCOUNT_SETTING ){
if( data != null ){
startAccessTokenUpdate( data );
return;
}
}else if( requestCode == REQUEST_APP_ABOUT ){
if( data != null ){
String search = data.getStringExtra( ActAbout.EXTRA_SEARCH );
if( ! TextUtils.isEmpty( search ) ){
performAddTimeline( Column.TYPE_SEARCH, search, true );
}
return;
}
}
}
super.onActivityResult( requestCode, resultCode, data );
}
@Override
public void onBackPressed(){
DrawerLayout drawer = (DrawerLayout) findViewById( R.id.drawer_layout );
if( drawer.isDrawerOpen( GravityCompat.START ) ){
drawer.closeDrawer( GravityCompat.START );
return;
}
switch( pref.getInt( Pref.KEY_BACK_BUTTON_ACTION, 0 ) ){
default:
case ActAppSetting.BACK_ASK_ALWAYS:
ActionsDialog dialog = new ActionsDialog();
dialog.addAction( getString( R.string.close_column ), new Runnable() {
@Override public void run(){
performColumnClose( true, pager_adapter.getColumn( pager.getCurrentItem() ) );
}
} );
dialog.addAction( getString( R.string.open_column_list ), new Runnable() {
@Override public void run(){
performColumnList();
}
} );
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:
performColumnClose( false, pager_adapter.getColumn( pager.getCurrentItem() ) );
break;
case ActAppSetting.BACK_EXIT_APP:
ActMain.this.finish();
break;
case ActAppSetting.BACK_OPEN_COLUMN_LIST:
performColumnList();
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 );
}
@SuppressWarnings("StatementWithEmptyBody")
@Override
public boolean onNavigationItemSelected( 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( Column.TYPE_HOME );
}else if( id == R.id.nav_add_tl_local ){
performAddTimeline( Column.TYPE_LOCAL );
}else if( id == R.id.nav_add_tl_federate ){
performAddTimeline( Column.TYPE_FEDERATE );
}else if( id == R.id.nav_add_favourites ){
performAddTimeline( Column.TYPE_FAVOURITES );
// }else if( id == R.id.nav_add_reports ){
// performAddTimeline(Column.TYPE_REPORTS );
}else if( id == R.id.nav_add_statuses ){
performAddTimeline( Column.TYPE_PROFILE );
}else if( id == R.id.nav_add_notifications ){
performAddTimeline( Column.TYPE_NOTIFICATIONS );
}else if( id == R.id.nav_app_setting ){
performAppSetting();
}else if( id == R.id.nav_account_setting ){
performAccountSetting();
}else if( id == R.id.nav_column_list ){
performColumnList();
}else if( id == R.id.nav_add_tl_search ){
performAddTimeline( 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( Column.TYPE_MUTES );
}else if( id == R.id.nav_add_blocks ){
performAddTimeline( Column.TYPE_BLOCKS );
// Handle the camera action
// }else if( id == R.id.nav_gallery ){
//
// }else if( id == R.id.nav_slideshow ){
//
// }else if( id == R.id.nav_manage ){
//
// }else if( id == R.id.nav_share ){
//
// }else if( id == R.id.nav_send ){
}
DrawerLayout drawer = (DrawerLayout) findViewById( R.id.drawer_layout );
drawer.closeDrawer( GravityCompat.START );
return true;
}
ViewPager pager;
ColumnPagerAdapter pager_adapter;
View llEmpty;
void initUI(){
setContentView( R.layout.act_main );
llEmpty = findViewById( R.id.llEmpty );
// // toolbar
// Toolbar toolbar = (Toolbar) findViewById( R.id.toolbar );
// setSupportActionBar( toolbar );
// navigation drawer
final DrawerLayout drawer = (DrawerLayout) findViewById( R.id.drawer_layout );
// ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
// this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close );
// drawer.addDrawerListener( toggle );
// toggle.syncState();
NavigationView navigationView = (NavigationView) findViewById( R.id.nav_view );
navigationView.setNavigationItemSelectedListener( this );
// floating action button
FloatingActionButton fabToot = (FloatingActionButton) findViewById( R.id.fabToot );
fabToot.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick( View view ){
performTootButton();
}
} );
// floating action button
FloatingActionButton fabMenu = (FloatingActionButton) findViewById( R.id.fabMenu );
fabMenu.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick( View view ){
if( ! drawer.isDrawerOpen( Gravity.START ) ){
drawer.openDrawer( Gravity.START );
}
}
} );
// ViewPager
pager = (ViewPager) findViewById( R.id.viewPager );
pager_adapter = new ColumnPagerAdapter( this );
pager.setAdapter( pager_adapter );
}
public void performAccountAdd(){
LoginForm.showLoginForm( this, null, new LoginForm.LoginFormCallback() {
@Override
public void startLogin( final Dialog dialog, final String instance ){
final ProgressDialog progress = new ProgressDialog( ActMain.this );
final AsyncTask< Void, String, TootApiResult > task = new AsyncTask< Void, String, TootApiResult >() {
@Override
protected TootApiResult doInBackground( Void... params ){
TootApiClient api_client = new TootApiClient( ActMain.this, new TootApiClient.Callback() {
@Override
public boolean isApiCancelled(){
return isCancelled();
}
@Override
public void publishApiProgress( final String s ){
Utils.runOnMainThread( new Runnable() {
@Override
public void run(){
progress.setMessage( s );
}
} );
}
} );
api_client.setInstance( instance );
return api_client.authorize1();
}
@Override
protected void onPostExecute( TootApiResult result ){
progress.dismiss();
if( result == null ){
// cancelled.
}else if( result.error != null ){
// エラー?
String sv = result.error;
if( sv.startsWith( "https" ) ){
// OAuth認証が必要
Intent data = new Intent();
data.setData( Uri.parse( sv ) );
startAccessTokenUpdate( data );
dialog.dismiss();
return;
}
// 他のエラー
Utils.showToast( ActMain.this, true, sv );
log.e( result.error );
}else{
// 多分ここは通らないはず
Utils.showToast( ActMain.this, false, R.string.access_token_updated_for );
}
}
};
progress.setIndeterminate( true );
progress.setCancelable( true );
progress.setOnCancelListener( new DialogInterface.OnCancelListener() {
@Override
public void onCancel( DialogInterface dialog ){
task.cancel( true );
}
} );
progress.show();
AsyncTaskCompat.executeParallel( task );
}
} );
}
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 ){
ex.printStackTrace();
}
}
// ActOAuthCallbackで受け取ったUriを処理する
private void handleIntentUri( @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( log, db_id );
if( account != null ){
Column column = addColumn( account, Column.TYPE_NOTIFICATIONS );
if( ! column.bInitialLoading ){
column.startLoading();
}
}
}catch( Throwable ex ){
ex.printStackTrace();
}
return;
}
final ProgressDialog progress = new ProgressDialog( ActMain.this );
final AsyncTask< Void, Void, TootApiResult > task = new AsyncTask< Void, Void, TootApiResult >() {
long row_id;
TootAccount ta;
SavedAccount sa;
String host;
@Override
protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( ActMain.this, new TootApiClient.Callback() {
@Override public boolean isApiCancelled(){
return isCancelled();
}
@Override public void publishApiProgress( final String s ){
Utils.runOnMainThread( new Runnable() {
@Override
public void run(){
progress.setMessage( s );
}
} );
}
} );
// エラー時
// 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( log, db_id );
if( sa == null ){
return new TootApiResult( "missing account db_id=" + db_id );
}
client.setAccount( sa );
}catch( Throwable ex ){
ex.printStackTrace();
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;
TootApiResult result = client.authorize2( code );
if( result != null && result.object != null ){
// taは使い捨てなので、生成に使うLinkClickContextはダミーで問題ない
LinkClickContext lcc = new LinkClickContext() {
};
this.ta = TootAccount.parse( log, lcc, result.object );
}
return result;
}
@Override
protected void onPostExecute( TootApiResult result ){
progress.dismiss();
// /noinspection StatementWithEmptyBody
if( result == null ){
// cancelled.
}else if( result.error != null ){
Utils.showToast( ActMain.this, true, result.error );
}else if( ta == null ){
// 自分のユーザネームを取れなかった
// …普通はエラーメッセージが設定されてるはずだが
Utils.showToast( ActMain.this, true, "missing TootAccount" );
}else if( this.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 : pager_adapter.column_list ){
if( c.access_info.acct.equals( sa.acct ) ){
c.startLoading();
}
}
}
}else{
// アカウント追加時
String user = ta.username + "@" + host;
this.row_id = SavedAccount.insert( host, user, result.object, result.token_info );
SavedAccount account = SavedAccount.loadAccount( log, row_id );
if( account != null ){
AlarmService.startCheck( ActMain.this );
onAccountUpdated( account );
}
}
}
};
progress.setIndeterminate( true );
progress.setCancelable( true );
progress.setOnCancelListener( new DialogInterface.OnCancelListener() {
@Override
public void onCancel( DialogInterface dialog ){
task.cancel( true );
}
} );
progress.show();
AsyncTaskCompat.executeParallel( task );
}
void reloadAccountSetting(){
ArrayList< SavedAccount > done_list = new ArrayList<>();
for( Column column : pager_adapter.column_list ){
SavedAccount a = column.access_info;
if( a == null || done_list.contains( a ) ) continue;
done_list.add( a );
a.reloadSetting();
}
}
public void performColumnClose( 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 ){
performColumnClose( true, column );
}
} )
.show();
return;
}
int page_showing = pager.getCurrentItem();
int page_delete = pager_adapter.column_list.indexOf( column );
pager_adapter.removeColumn( pager, column );
saveColumnList();
if( pager_adapter.getCount() == 0 ){
llEmpty.setVisibility( View.VISIBLE );
}else if( page_showing > 0 && page_showing == page_delete ){
pager.setCurrentItem( page_showing - 1, true );
}
}
//////////////////////////////////////////////////////////////
// カラム追加系
public Column addColumn( SavedAccount ai, int type, Object... params ){
// 既に同じカラムがあればそこに移動する
for( Column column : pager_adapter.column_list ){
if( column.isSameSpec( ai, type, params ) ){
pager.setCurrentItem( pager_adapter.column_list.indexOf( column ), true );
return column;
}
}
//
llEmpty.setVisibility( View.GONE );
//
Column col = new Column( ActMain.this, ai, type, params );
int idx = pager_adapter.addColumn( pager, col );
saveColumnList();
pager.setCurrentItem( idx, true );
return col;
}
private void onAccountUpdated( SavedAccount data ){
Utils.showToast( this, false, R.string.account_confirmed );
addColumn( data, Column.TYPE_HOME );
}
void performOpenUser( SavedAccount access_info, TootAccount user ){
addColumn( access_info, Column.TYPE_PROFILE, user.id );
}
public void performConversation( SavedAccount access_info, TootStatus status ){
addColumn( access_info, Column.TYPE_CONVERSATION, status.id );
}
private void performAddTimeline( final int type, final Object... args ){
AccountPicker.pick( this, true, new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( SavedAccount ai ){
switch( type ){
default:
addColumn( ai, type, args );
break;
case Column.TYPE_PROFILE:
addColumn( ai, type, ai.id );
break;
}
}
} );
}
public void openHashTag( SavedAccount access_info, String tag ){
addColumn( access_info, Column.TYPE_HASHTAG, tag );
}
//////////////////////////////////////////////////////////////
interface GetAccountCallback {
// return account information
// if failed, account is null.
void onGetAccount( TootAccount account );
}
void startGetAccount( final SavedAccount access_info, final String host, final String user, final GetAccountCallback callback ){
final ProgressDialog progress = new ProgressDialog( this );
final AsyncTask< Void, Void, TootAccount > task = new AsyncTask< Void, Void, TootAccount >() {
@Override
protected TootAccount doInBackground( Void... params ){
TootApiClient client = new TootApiClient( ActMain.this, new TootApiClient.Callback() {
@Override
public boolean isApiCancelled(){
return isCancelled();
}
@Override
public void publishApiProgress( final String s ){
Utils.runOnMainThread( new Runnable() {
@Override
public void run(){
progress.setMessage( s );
}
} );
}
} );
client.setAccount( access_info );
String path = "/api/v1/accounts/search" + "?q=" + Uri.encode( user );
TootApiResult result = client.request( path );
if( result.array != null ){
for( int i = 0, ie = result.array.length() ; i < ie ; ++ i ){
TootAccount item = TootAccount.parse( log, access_info, result.array.optJSONObject( i ) );
if( ! item.username.equals( user ) ) continue;
if( ! item.acct.contains( "@" )
|| item.acct.equalsIgnoreCase( user + "@" + host ) )
return item;
}
}
return null;
}
@Override
protected void onCancelled( TootAccount result ){
super.onPostExecute( result );
}
@Override
protected void onPostExecute( TootAccount result ){
progress.dismiss();
callback.onGetAccount( result );
}
};
progress.setIndeterminate( true );
progress.setCancelable( true );
progress.setOnCancelListener( new DialogInterface.OnCancelListener() {
@Override
public void onCancel( DialogInterface dialog ){
task.cancel( true );
}
} );
progress.show();
AsyncTaskCompat.executeParallel( task );
}
public void openBrowser( SavedAccount account, String url ){
openChromeTab( account, url, false );
}
Pattern reHashTag = Pattern.compile( "\\Ahttps://([^/]+)/tags/([^?#]+)\\z" );
Pattern reUserPage = Pattern.compile( "\\Ahttps://([^/]+)/@([^?#]+)\\z" );
public void openChromeTab( final SavedAccount access_info, final String url, boolean noIntercept ){
try{
log.d( "openChromeTab url=%s", url );
if( ! noIntercept ){
// ハッシュタグをアプリ内で開く
Matcher m = reHashTag.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 = Uri.decode( m.group( 2 ) );
if( host.equalsIgnoreCase( access_info.host ) ){
openHashTag( access_info, tag );
return;
}else{
openHashTagOtherInstance( access_info, url, host, tag );
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 ) );
startGetAccount( access_info, host, user, new GetAccountCallback() {
@Override
public void onGetAccount( TootAccount who ){
if( who != null ){
performOpenUser( access_info, who );
return;
}
openChromeTab( access_info, url, true );
}
} );
return;
}
}
// ビルダーを使って表示方法を指定する
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
builder.setToolbarColor( Styler.getAttributeColor( this,R.attr.colorPrimary ) ).setShowTitle( true );
// CustomTabsでURLをひらくIntentを発行
CustomTabsIntent customTabsIntent = builder.build();
customTabsIntent.launchUrl( this, Uri.parse( url ) );
}catch( Throwable ex ){
// ex.printStackTrace();
log.e( ex, "openChromeTab failed. url=%s", url );
}
}
// 他インスタンスのハッシュタグの表示
private void openHashTagOtherInstance( final SavedAccount access_info, final String url, String host, final String tag ){
ActionsDialog dialog = new ActionsDialog();
ArrayList< SavedAccount > account_list = new ArrayList<>();
for( SavedAccount a : SavedAccount.loadAccountList( log ) ){
if( a.host.equalsIgnoreCase( host ) ){
account_list.add( a );
}
}
Collections.sort( account_list, new Comparator< SavedAccount >() {
@Override
public int compare( SavedAccount a, SavedAccount b ){
return String.CASE_INSENSITIVE_ORDER.compare( a.getFullAcct( a ), b.getFullAcct( b ) );
}
} );
for( SavedAccount a : account_list ){
final SavedAccount _a = a;
dialog.addAction(
getString( R.string.open_in_account, a.acct )
, new Runnable() {
@Override
public void run(){
openHashTag( _a, tag );
}
}
);
}
if( account_list.isEmpty() ){
// TODO ログインなしアカウントで開く選択肢
}
// カラムのアカウントで開く
{
final SavedAccount _a = access_info;
dialog.addAction(
getString( R.string.open_in_account, access_info.acct )
, new Runnable() {
@Override
public void run(){
openHashTag( _a, tag );
}
}
);
}
// ブラウザで表示する
{
dialog.addAction(
getString( R.string.open_web_on_host, host )
, new Runnable() {
@Override
public void run(){
openChromeTab( access_info, url, true );
}
}
);
}
dialog.show( this, "#" + tag );
}
final HTMLDecoder.LinkClickCallback link_click_listener = new HTMLDecoder.LinkClickCallback() {
@Override
public void onClickLink( LinkClickContext lcc, String url ){
openChromeTab( (SavedAccount) lcc, url, false );
}
};
private void performTootButton(){
Column c = pager_adapter.getColumn( pager.getCurrentItem() );
if( c != null ){
ActPost.open( this, c.access_info.db_id, "" );
}
}
public void performReply( SavedAccount account, TootStatus status ){
ActPost.open( this, account.db_id, status );
}
public void performMention( SavedAccount account, TootAccount who ){
ActPost.open( this, account.db_id, "@" + account.getFullAcct( who ) + " " );
}
/////////////////////////////////////////////////////////////////////////
private void showColumnMatchAccount( SavedAccount account ){
for( Column column : pager_adapter.column_list ){
if( account.acct.equals( column.access_info.acct ) ){
column.fireVisualCallback();
}
}
}
/////////////////////////////////////////////////////////////////////////
// favourite
final HashSet< String > map_busy_fav = new HashSet<>();
boolean isBusyFav( SavedAccount account, TootStatus status ){
String busy_key = account.host + ":" + status.id;
return map_busy_fav.contains( busy_key );
}
public void performFavourite( final SavedAccount account, final TootStatus status ){
//
final String busy_key = account.host + ":" + status.id;
//
if( map_busy_fav.contains( busy_key ) ){
Utils.showToast( this, false, R.string.wait_previous_operation );
return;
}
//
map_busy_fav.add( busy_key );
//
new AsyncTask< Void, Void, TootApiResult >() {
final boolean bSet = ! status.favourited;
TootStatus new_status;
@Override
protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( ActMain.this, new TootApiClient.Callback() {
@Override
public boolean isApiCancelled(){
return isCancelled();
}
@Override
public void publishApiProgress( final String s ){
}
} );
client.setAccount( account );
Request.Builder request_builder = new Request.Builder()
.post( RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, ""
) );
TootApiResult result = client.request(
( bSet
? "/api/v1/statuses/" + status.id + "/favourite"
: "/api/v1/statuses/" + status.id + "/unfavourite"
)
, request_builder );
if( result.object != null ){
new_status = TootStatus.parse( log, account, result.object );
}
return result;
}
@Override
protected void onCancelled( TootApiResult result ){
super.onPostExecute( result );
}
@Override
protected void onPostExecute( TootApiResult result ){
map_busy_fav.remove( busy_key );
if( new_status != null ){
// カウント数は遅延があるみたい
if( bSet && new_status.favourites_count <= status.favourites_count ){
// 星つけたのにカウントが上がらないのは違和感あるので、表示をいじる
new_status.favourites_count = status.favourites_count + 1;
}else if( ! bSet && new_status.favourites_count >= status.favourites_count ){
// 星外したのにカウントが下がらないのは違和感あるので、表示をいじる
new_status.favourites_count = status.favourites_count - 1;
if( new_status.favourites_count < 0 ){
new_status.favourites_count = 0;
}
}
for( Column column : pager_adapter.column_list ){
column.findStatus( account, new_status.id, new Column.StatusEntryCallback() {
@Override
public void onIterate( TootStatus status ){
status.favourited = new_status.favourited;
status.favourites_count = new_status.favourites_count;
}
} );
}
}else{
if( result != null ) Utils.showToast( ActMain.this, true, result.error );
}
showColumnMatchAccount( account );
}
}.execute();
showColumnMatchAccount( account );
}
/////////////////////////////////////////////////////////////////////////
// boost
final HashSet< String > map_busy_boost = new HashSet<>();
boolean isBusyBoost( SavedAccount account, TootStatus status ){
String busy_key = account.host + ":" + status.id;
return map_busy_boost.contains( busy_key );
}
public void performBoost( final SavedAccount account, final TootStatus status, boolean bConfirmed ){
//
final String busy_key = account.host + ":" + status.id;
//
if( map_busy_boost.contains( busy_key ) ){
Utils.showToast( this, false, R.string.wait_previous_operation );
return;
}
if( status.reblogged ){
// FAVがついているか、FAV操作中はBoostを外せない
if( isBusyFav( account, status ) || status.favourited ){
Utils.showToast( this, false, R.string.cant_remove_boost_while_favourited );
return;
}
}else{
if( ! bConfirmed && account.confirm_boost ){
// TODO: アカウント設定でスキップさせたい
new AlertDialog.Builder( this )
.setTitle( R.string.confirm )
.setMessage( R.string.confirm_boost )
.setPositiveButton( R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick( DialogInterface dialog, int which ){
performBoost( account, status, true );
}
} )
.setNegativeButton( R.string.cancel, null )
.show();
return;
}
}
//
map_busy_boost.add( busy_key );
//
new AsyncTask< Void, Void, TootApiResult >() {
final boolean new_state = ! status.reblogged;
TootStatus new_status;
@Override
protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( ActMain.this, new TootApiClient.Callback() {
@Override
public boolean isApiCancelled(){
return isCancelled();
}
@Override
public void publishApiProgress( final String s ){
}
} );
client.setAccount( account );
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 + ( new_state ? "/reblog" : "/unreblog" )
, request_builder );
if( result.object != null ){
// reblog,unreblog のレスポンスは信用ならんのでステータスを再取得する
result = client.request( "/api/v1/statuses/" + status.id );
if( result.object != null ){
new_status = TootStatus.parse( log, account, result.object );
}
}
return result;
}
@Override
protected void onCancelled( TootApiResult result ){
super.onPostExecute( result );
}
@Override
protected void onPostExecute( TootApiResult result ){
map_busy_boost.remove( busy_key );
if( new_status != null ){
// カウント数は遅延があるみたい
if( new_status.reblogged && new_status.reblogs_count <= status.reblogs_count ){
// 星つけたのにカウントが上がらないのは違和感あるので、表示をいじる
new_status.reblogs_count = status.reblogs_count + 1;
}else if( ! new_status.reblogged && new_status.reblogs_count >= status.reblogs_count ){
// 星外したのにカウントが下がらないのは違和感あるので、表示をいじる
new_status.reblogs_count = status.reblogs_count - 1;
if( new_status.reblogs_count < 0 ){
new_status.reblogs_count = 0;
}
}
for( Column column : pager_adapter.column_list ){
column.findStatus( account, new_status.id, new Column.StatusEntryCallback() {
@Override
public void onIterate( TootStatus status ){
status.reblogged = new_status.reblogged;
status.reblogs_count = new_status.reblogs_count;
}
} );
}
}else{
if( result != null ) Utils.showToast( ActMain.this, true, result.error );
}
showColumnMatchAccount( account );
}
}.execute();
showColumnMatchAccount( account );
}
////////////////////////////////////////
private void performAccountSetting(){
AccountPicker.pick( this, true, new AccountPicker.AccountPickerCallback() {
@Override
public void onAccountPicked( SavedAccount ai ){
ActAccountSetting.open( ActMain.this, ai, REQUEST_CODE_ACCOUNT_SETTING );
}
} );
}
private void performAppSetting(){
ActAppSetting.open( ActMain.this );
}
////////////////////////////////////////////////////////
// column list
JSONArray encodeColumnList(){
JSONArray array = new JSONArray();
for( int i = 0, ie = pager_adapter.column_list.size() ; i < ie ; ++ i ){
Column column = pager_adapter.column_list.get( i );
try{
JSONObject dst = new JSONObject();
column.encodeJSON( dst, i );
array.put( dst );
}catch( JSONException ex ){
ex.printStackTrace();
}
}
return array;
}
private void performColumnList(){
Intent intent = new Intent( this, ActColumnList.class );
intent.putExtra( ActColumnList.EXTRA_ORDER, encodeColumnList().toString() );
intent.putExtra( ActColumnList.EXTRA_SELECTION, pager.getCurrentItem() );
startActivityForResult( intent, REQUEST_CODE_COLUMN_LIST );
}
private void dumpColumnList(){
for( int i = 0, ie = pager_adapter.column_list.size() ; i < ie ; ++ i ){
Column column = pager_adapter.column_list.get( i );
log.d( "dumpColumnList [%s]%s %s", i, column.access_info.acct, column.getColumnName( true ) );
}
}
static final String FILE_COLUMN_LIST = "column_list";
void saveColumnList(){
JSONArray array = encodeColumnList();
try{
OutputStream os = openFileOutput( FILE_COLUMN_LIST, MODE_PRIVATE );
try{
os.write( Utils.encodeUTF8( array.toString() ) );
}finally{
os.close();
}
}catch( Throwable ex ){
ex.printStackTrace();
Utils.showToast( this, ex, "saveColumnList failed." );
}
}
private void loadColumnList(){
try{
InputStream is = openFileInput( FILE_COLUMN_LIST );
try{
ByteArrayOutputStream bao = new ByteArrayOutputStream( is.available() );
byte[] tmp = new byte[ 4096 ];
for( ; ; ){
int r = is.read( tmp, 0, tmp.length );
if( r <= 0 ) break;
bao.write( tmp, 0, r );
}
JSONArray array = new JSONArray( Utils.decodeUTF8( bao.toByteArray() ) );
for( int i = 0, ie = array.length() ; i < ie ; ++ i ){
try{
JSONObject src = array.optJSONObject( i );
Column col = new Column( ActMain.this, src );
pager_adapter.addColumn( pager, col, pager_adapter.getCount() );
}catch( Throwable ex ){
ex.printStackTrace();
}
}
}finally{
is.close();
}
}catch( FileNotFoundException ignored ){
}catch( Throwable ex ){
ex.printStackTrace();
Utils.showToast( this, ex, "loadColumnList failed." );
}
if( pager_adapter.column_list.size() > 0 ){
llEmpty.setVisibility( View.GONE );
}
}
////////////////////////////////////////////////////////////////////////////
interface RelationChangedCallback {
// void onRelationChanged( TootRelationShip relationship );
void onRelationChanged();
}
private void callFollow( final SavedAccount access_info, final TootAccount who
, final boolean bFollow, final RelationChangedCallback callback ){
new AsyncTask< Void, Void, TootApiResult >() {
@Override protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( ActMain.this, new TootApiClient.Callback() {
@Override public boolean isApiCancelled(){
return isCancelled();
}
@Override public void publishApiProgress( String s ){
}
} );
client.setAccount( access_info );
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 ){
remote_who = TootAccount.parse( log, access_info, result.object );
}
}
}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 ){
if( result.object != null ){
relation = TootRelationShip.parse( log, result.object );
}
}
}
return result;
}
TootRelationShip relation;
TootAccount remote_who;
@Override
protected void onCancelled( TootApiResult result ){
onPostExecute( null );
}
@Override
protected void onPostExecute( TootApiResult result ){
// if( relation != null ){
// App1.relationship_map.put( access_info, relation );
// if( callback != null ) callback.onRelationChanged( relation );
// }else if( remote_who != null ){
// App1.relationship_map.addFollowing( access_info, remote_who.id );
// if( callback != null )
// callback.onRelationChanged( App1.relationship_map.get( access_info, remote_who.id ) );
// }
//noinspection StatementWithEmptyBody
if( result == null ){
// cancelled.
}else if( relation != null || remote_who != null ){
// ローカル操作成功、もしくはリモートフォロー成功
if( callback != null ) callback.onRelationChanged();
}else if( bFollow && who.locked && result.response.code() == 422 ){
Utils.showToast( ActMain.this, false, R.string.cant_follow_locked_user );
}else{
Utils.showToast( ActMain.this, false, result.error );
}
}
}.execute();
}
// acct で指定したユーザをリモートフォローする
void callRemoteFollow( final SavedAccount access_info
, final String acct, final boolean locked, final RelationChangedCallback callback
){
new AsyncTask< Void, Void, TootApiResult >() {
@Override protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( ActMain.this, new TootApiClient.Callback() {
@Override public boolean isApiCancelled(){
return isCancelled();
}
@Override public void publishApiProgress( String s ){
}
} );
client.setAccount( access_info );
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 ){
who = TootAccount.parse( log, access_info, result.object );
}
}
return result;
}
TootAccount who;
@Override
protected void onCancelled( TootApiResult result ){
onPostExecute( null );
}
@Override
protected void onPostExecute( TootApiResult result ){
//noinspection StatementWithEmptyBody
if( result == null ){
// cancelled.
}else if( who != null ){
// App1.relationship_map.addFollowing( access_info, who.id );
// if( callback != null )
// callback.onRelationChanged( App1.relationship_map.get( access_info, who.id ) );
if( callback != null )
callback.onRelationChanged();
}else if( locked && result.response.code() == 422 ){
Utils.showToast( ActMain.this, false, R.string.cant_follow_locked_user );
}else{
Utils.showToast( ActMain.this, false, result.error );
}
}
}.execute();
}
// アカウントを選択してからユーザをフォローする
void followFromAccount( final SavedAccount access_info, final TootAccount who, final RelationChangedCallback callback ){
AccountPicker.pick( ActMain.this, true, new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( SavedAccount ai ){
String acct = who.acct;
if( ! acct.contains( "@" ) ){
acct = acct + "@" + access_info.host;
}
callRemoteFollow( ai, acct, who.locked, callback );
}
} );
}
////////////////////////////////////////
private void callMute( final SavedAccount access_info, final TootAccount who, final boolean bMute, final RelationChangedCallback callback ){
new AsyncTask< Void, Void, TootApiResult >() {
@Override
protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( ActMain.this, new TootApiClient.Callback() {
@Override
public boolean isApiCancelled(){
return isCancelled();
}
@Override
public void publishApiProgress( String s ){
}
} );
client.setAccount( access_info );
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 + ( bMute ? "/mute" : "/unmute" )
, request_builder );
if( result != null ){
if( result.object != null ){
relation = TootRelationShip.parse( log, result.object );
Utils.showToast( ActMain.this, false, bMute ? R.string.mute_succeeded : R.string.unmute_succeeded );
}else{
Utils.showToast( ActMain.this, false, result.error );
}
}
return result;
}
TootRelationShip relation;
@Override
protected void onCancelled( TootApiResult result ){
onPostExecute( null );
}
@Override
protected void onPostExecute( TootApiResult result ){
if( relation != null ){
// App1.relationship_map.put( access_info, relation );
// if( callback != null ) callback.onRelationChanged( relation );
if( callback != null ) callback.onRelationChanged();
if( bMute ){
for( Column column : pager_adapter.column_list ){
column.removeStatusByAccount( access_info, who.id );
}
}else{
for( Column column : pager_adapter.column_list ){
column.removeFromMuteList( access_info, who.id );
}
}
showColumnMatchAccount( access_info );
}
}
}.execute();
}
private void callBlock( final SavedAccount access_info, final TootAccount who, final boolean bBlock, final RelationChangedCallback callback ){
new AsyncTask< Void, Void, TootApiResult >() {
@Override
protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( ActMain.this, new TootApiClient.Callback() {
@Override
public boolean isApiCancelled(){
return isCancelled();
}
@Override
public void publishApiProgress( String s ){
}
} );
client.setAccount( access_info );
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 = TootRelationShip.parse( log, result.object );
Utils.showToast( ActMain.this, false, bBlock ? R.string.block_succeeded : R.string.unblock_succeeded );
}else{
Utils.showToast( ActMain.this, false, result.error );
}
}
return result;
}
TootRelationShip relation;
TootAccount remote_who;
@Override
protected void onCancelled( TootApiResult result ){
onPostExecute( null );
}
@Override
protected void onPostExecute( TootApiResult result ){
boolean bOK = false;
if( relation != null ){
bOK = true;
// App1.relationship_map.put( access_info, relation );
// if( callback != null ) callback.onRelationChanged( relation );
if( callback != null ) callback.onRelationChanged();
}else if( remote_who != null ){
bOK = true;
// App1.relationship_map.addFollowing( access_info, remote_who.id );
// if( callback != null )
// callback.onRelationChanged( App1.relationship_map.get( access_info, remote_who.id ) );
if( callback != null )
callback.onRelationChanged();
}
if( bOK ){
if( bBlock ){
for( Column column : pager_adapter.column_list ){
column.removeStatusByAccount( access_info, who.id );
}
}else{
for( Column column : pager_adapter.column_list ){
column.removeFromBlockList( access_info, who.id );
}
}
showColumnMatchAccount( access_info );
}
}
}.execute();
}
private void deleteStatus( final SavedAccount access_info, final long status_id ){
new AsyncTask< Void, Void, TootApiResult >() {
@Override
protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( ActMain.this, new TootApiClient.Callback() {
@Override
public boolean isApiCancelled(){
return isCancelled();
}
@Override
public void publishApiProgress( String s ){
}
} );
client.setAccount( access_info );
Request.Builder request_builder = new Request.Builder().delete();
TootApiResult result = client.request( "/api/v1/statuses/" + status_id, request_builder );
return result;
}
@Override
protected void onCancelled( TootApiResult result ){
onPostExecute( null );
}
@Override
protected void onPostExecute( TootApiResult result ){
boolean bOK = false;
if( result != null ){
if( result.object != null ){
Utils.showToast( ActMain.this, false, R.string.delete_succeeded );
for( Column column : pager_adapter.column_list ){
column.removeStatus( access_info, status_id );
}
showColumnMatchAccount( access_info );
}else{
Utils.showToast( ActMain.this, false, result.error );
}
}
}
}.execute();
}
interface ReportCompleteCallback {
void onReportComplete( TootApiResult result );
}
private void callReport( final SavedAccount account, final TootAccount who, final TootStatus status
, final String comment, final ReportCompleteCallback callback
){
new AsyncTask< Void, Void, TootApiResult >() {
@Override
protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( ActMain.this, new TootApiClient.Callback() {
@Override
public boolean isApiCancelled(){
return isCancelled();
}
@Override
public void publishApiProgress( String s ){
}
} );
client.setAccount( account );
StringBuilder sb = new StringBuilder();
sb.append( "account_id=" )
.append( Long.toString( status.account.id ) )
.append( "&comment=" )
.append( Uri.encode( comment ) )
;
if( status != null ){
sb.append( "&status_ids[]=" )
.append( Long.toString( status.id ) )
;
}
Request.Builder request_builder = new Request.Builder().post(
RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, sb.toString()
) );
TootApiResult result = client.request( "/api/v1/reports", request_builder );
if( result != null ){
callback.onReportComplete( result );
}
return result;
}
}.execute();
}
private void openReportForm( final SavedAccount account, final TootAccount who, 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 ){
//noinspection StatementWithEmptyBody
if( result == null ){
// cancelled
}else if( result.object != null ){
Utils.showToast( ActMain.this, false, R.string.report_completed );
dialog.dismiss();
}else{
// error
Utils.showToast( ActMain.this, false, result.error );
}
}
} );
}
} );
}
////////////////////////////////////////////////
private void sendStatus( SavedAccount access_info, TootStatus status ){
try{
StringBuilder sb = new StringBuilder();
sb.append( getString( R.string.send_header_url ) );
sb.append( ": " );
sb.append( status.url );
sb.append( "\n" );
sb.append( getString( R.string.send_header_date ) );
sb.append( ": " );
sb.append( TootStatus.formatTime( status.time_created_at ) );
sb.append( "\n" );
sb.append( getString( R.string.send_header_from_acct ) );
sb.append( ": " );
sb.append( access_info.getFullAcct( status.account ) );
sb.append( "\n" );
sb.append( getString( R.string.send_header_from_name ) );
sb.append( ": " );
sb.append( status.account.display_name );
sb.append( "\n" );
if( ! TextUtils.isEmpty( status.spoiler_text ) ){
sb.append( getString( R.string.send_header_content_warning ) );
sb.append( ": " );
sb.append( HTMLDecoder.decodeHTMLForClipboard( access_info, status.spoiler_text ) );
sb.append( "\n" );
}
sb.append( "\n" );
sb.append( HTMLDecoder.decodeHTMLForClipboard( access_info, status.content ) );
Intent intent = new Intent();
intent.setAction( Intent.ACTION_SEND );
intent.setType( "text/plain" );
intent.putExtra( Intent.EXTRA_TEXT, sb.toString() );
startActivity( intent );
}catch( Throwable ex ){
log.e( ex, "sendStatus failed." );
ex.printStackTrace();
Utils.showToast( this, ex, "sendStatus failed." );
}
}
////////////////////////////////////////////////
final RelationChangedCallback follow_comolete_callback = new RelationChangedCallback() {
// @Override public void onRelationChanged( TootRelationShip relationship ){
// Utils.showToast( ActMain.this,false,R.string.follow_succeeded );
// }
@Override public void onRelationChanged(){
Utils.showToast( ActMain.this, false, R.string.follow_succeeded );
}
};
final RelationChangedCallback unfollow_comolete_callback = new RelationChangedCallback() {
// @Override public void onRelationChanged( TootRelationShip relationship ){
// Utils.showToast( ActMain.this,false,R.string.follow_succeeded );
// }
@Override public void onRelationChanged(){
Utils.showToast( ActMain.this, false, R.string.unfollow_succeeded );
}
};
// ステータスのmoreメニュー
public void openStatusMoreMenu( final SavedAccount access_info, final TootStatus status ){
ActionsDialog dialog = new ActionsDialog();
dialog.addAction( getString( R.string.open_web_page ), new Runnable() {
@Override public void run(){
// 強制的にブラウザで開く
openChromeTab( access_info, status.url, true );
}
} );
dialog.addAction( getString( R.string.send_text ), new Runnable() {
@Override public void run(){
sendStatus( access_info, status );
}
} );
final ArrayList< SavedAccount > tmp_list = new ArrayList<>();
for( SavedAccount a : SavedAccount.loadAccountList( log ) ){
if( a.host.equalsIgnoreCase( access_info.host ) ){
// 同じホストを収集
tmp_list.add( a );
}
}
if( ! tmp_list.isEmpty() ){
dialog.addAction( getString( R.string.favourite_from_another_account ), new Runnable() {
@Override public void run(){
AccountPicker.pick( ActMain.this, false, tmp_list, new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( SavedAccount ai ){
if( ai != null ) performFavourite( ai, status );
}
} );
}
} );
dialog.addAction( getString( R.string.boost_from_another_account ), new Runnable() {
@Override public void run(){
AccountPicker.pick( ActMain.this, false, tmp_list, new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( SavedAccount ai ){
if( ai != null ) performBoost( ai, status, false );
}
} );
}
} );
}
dialog.addAction( getString( R.string.follow_from_another_account ), new Runnable() {
@Override public void run(){
followFromAccount( access_info, status.account, follow_comolete_callback );
}
} );
dialog.addAction( getString( R.string.follow ), new Runnable() {
@Override public void run(){
callFollow( access_info, status.account, true, follow_comolete_callback );
}
} );
dialog.addAction( getString( R.string.unfollow ), new Runnable() {
@Override public void run(){
callFollow( access_info, status.account, false, unfollow_comolete_callback );
}
} );
dialog.addAction( getString( R.string.mute ), new Runnable() {
@Override public void run(){
callMute( access_info, status.account, true, null );
}
} );
dialog.addAction( getString( R.string.unmute ), new Runnable() {
@Override public void run(){
callMute( access_info, status.account, false, null );
}
} );
dialog.addAction( getString( R.string.block ), new Runnable() {
@Override public void run(){
callBlock( access_info, status.account, true, null );
}
} );
dialog.addAction( getString( R.string.unblock ), new Runnable() {
@Override public void run(){
callBlock( access_info, status.account, false, null );
}
} );
dialog.addAction( getString( R.string.report ), new Runnable() {
@Override public void run(){
openReportForm( access_info, status.account, status );
}
} );
if( access_info.isMe( status.account ) ){
dialog.addAction( getString( R.string.delete ), new Runnable() {
@Override public void run(){
deleteStatus( access_info, status.id );
}
} );
}
dialog.show( this, null );
}
public void openAccountMoreMenu( final SavedAccount access_info, final TootAccount who ){
ActionsDialog dialog = new ActionsDialog();
dialog.addAction( getString( R.string.mention ), new Runnable() {
@Override public void run(){
performMention( access_info, who );
}
} );
dialog.addAction( getString( R.string.follow ), new Runnable() {
@Override public void run(){
callFollow( access_info, who, true, follow_comolete_callback );
}
} );
dialog.addAction( getString( R.string.follow_from_another_account ), new Runnable() {
@Override public void run(){
followFromAccount( access_info, who, follow_comolete_callback );
}
} );
dialog.addAction( getString( R.string.unfollow ), new Runnable() {
@Override
public void run(){
callFollow( access_info, who, false, unfollow_comolete_callback );
}
} );
dialog.addAction( getString( R.string.mute ), new Runnable() {
@Override public void run(){
callMute( access_info, who, true, null );
}
} );
dialog.addAction( getString( R.string.unmute ), new Runnable() {
@Override
public void run(){
callMute( access_info, who, false, null );
}
} );
dialog.addAction( getString( R.string.block ), new Runnable() {
@Override public void run(){
callBlock( access_info, who, true, null );
}
} );
dialog.addAction( getString( R.string.unblock ), new Runnable() {
@Override public void run(){
callBlock( access_info, who, false, null );
}
} );
dialog.addAction( getString( R.string.report ), new Runnable() {
@Override public void run(){
openReportForm( access_info, who, null );
}
} );
dialog.show( this, null );
}
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 AsyncTask< Void, Void, TootApiResult >() {
@Override
protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( ActMain.this, new TootApiClient.Callback() {
@Override
public boolean isApiCancelled(){
return isCancelled();
}
@Override
public void publishApiProgress( String s ){
}
} );
client.setAccount( target_account );
Request.Builder request_builder = new Request.Builder().post(
RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, "" // 空データ
) );
TootApiResult result = client.request( "/api/v1/notifications/clear" , request_builder );
return result;
}
@Override
protected void onCancelled( TootApiResult result ){
onPostExecute( null );
}
@Override
protected void onPostExecute( TootApiResult result ){
if( result == null ){
//cancelled.
}else if( result.object != null ){
// ok. empty object will be returned.
for( Column column : pager_adapter.column_list ){
if( 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 );
}
}
}.execute();
}
}