アカウント設定をつけた。アカウント削除をつけた

This commit is contained in:
tateisu 2017-04-22 23:45:26 +09:00
parent 924b464f59
commit ba568642e8
70 changed files with 18550 additions and 3660 deletions

View File

@ -1,13 +1,18 @@
<component name="ProjectDictionaryState">
<dictionary name="tateisu">
<words>
<w>dont</w>
<w>emojione</w>
<w>favourited</w>
<w>noto</w>
<w>nsfw</w>
<w>reblog</w>
<w>reblogged</w>
<w>reblogs</w>
<w>subwaytooter</w>
<w>timelines</w>
<w>unfavourite</w>
<w>unreblog</w>
</words>
</dictionary>
</component>

View File

@ -30,8 +30,10 @@ dependencies {
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
compile 'com.github.bumptech.glide:glide:3.7.0'
compile 'com.android.volley:volley:1.0.0'
compile 'com.android.support:customtabs:24.2.0'
compile 'com.squareup.okhttp3:okhttp:3.7.0'
compile 'commons-io:commons-io:2.4'
compile 'uk.co.chrisjenx:calligraphy:2.2.0'
}

View File

@ -18,20 +18,43 @@
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:windowSoftInputMode="adjustPan"
>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".ActPost"
android:label="@string/act_post"
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="adjustResize"
>
</activity>
/>
<activity
android:name=".ActAccountSetting"
android:label="@string/account_setting"
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="adjustResize"
/>
<activity
android:name=".ActAppSetting"
android:label="@string/app_setting"
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="adjustResize"
/>
<activity
android:name=".ActColumnList"
android:label="@string/column_list"
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="adjustResize"
/>
</application>
</manifest>

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,184 @@
package jp.juggler.subwaytooter;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.Switch;
import android.widget.TextView;
import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.LogCategory;
public class ActAccountSetting extends AppCompatActivity implements View.OnClickListener, CompoundButton.OnCheckedChangeListener {
static final LogCategory log = new LogCategory( "ActAccountSetting" );
static final String KEY_ACCOUNT_DB_ID = "account_db_id";
public static void open( Context context, SavedAccount ai ){
Intent intent = new Intent( context, ActAccountSetting.class );
intent.putExtra( KEY_ACCOUNT_DB_ID, ai.db_id );
context.startActivity( intent );
}
SavedAccount account;
@Override
protected void onCreate( @Nullable Bundle savedInstanceState ){
super.onCreate( savedInstanceState );
initUI();
account = SavedAccount.loadAccount( log, getIntent().getLongExtra( KEY_ACCOUNT_DB_ID, - 1L ) );
if( account == null ) finish();
loadUIFromData( account );
}
TextView tvInstance;
TextView tvUser;
View btnAccessToken;
View btnAccountRemove;
Button btnVisibility;
Switch swConfirmBeforeBoost;
Switch swNSFWOpen;
private void initUI(){
setContentView( R.layout.act_account_setting );
tvInstance = (TextView) findViewById( R.id.tvInstance );
tvUser = (TextView) findViewById( R.id.tvUser );
btnAccessToken = findViewById( R.id.btnAccessToken );
btnAccountRemove = findViewById( R.id.btnAccountRemove );
btnVisibility = (Button) findViewById( R.id.btnVisibility );
swConfirmBeforeBoost = (Switch) findViewById( R.id.swConfirmBeforeBoost );
swNSFWOpen = (Switch) findViewById( R.id.swNSFWOpen );
btnAccessToken.setOnClickListener( this );
btnAccountRemove.setOnClickListener( this );
btnVisibility.setOnClickListener( this );
swNSFWOpen.setOnCheckedChangeListener( this );
swConfirmBeforeBoost.setOnCheckedChangeListener( this );
}
private void loadUIFromData( SavedAccount a ){
tvInstance.setText( a.host );
tvUser.setText( a.user );
String sv = a.visibility;
if( sv != null ){
visibility = sv;
}
swConfirmBeforeBoost.setChecked( a.confirm_boost );
swNSFWOpen.setChecked( a.dont_hide_nsfw );
updateVisibility();
}
private void saveUIToData(){
account.visibility = visibility;
account.confirm_boost = swConfirmBeforeBoost.isChecked();
account.dont_hide_nsfw = swNSFWOpen.isChecked();
account.saveSetting();
}
@Override
public void onCheckedChanged( CompoundButton buttonView, boolean isChecked ){
saveUIToData();
}
@Override
public void onClick( View v ){
switch( v.getId() ){
case R.id.btnAccessToken:
performAccessToken();
break;
case R.id.btnAccountRemove:
performAccountRemove();
break;
case R.id.btnVisibility:
performVisibility();
break;
}
}
///////////////////////////////////////////////////
String visibility = TootStatus.VISIBILITY_PUBLIC;
private void updateVisibility(){
btnVisibility.setText( Styler.getVisibilityString( this, visibility ) );
}
private void performVisibility(){
final String[] caption_list = new String[]{
getString( R.string.visibility_public ),
getString( R.string.visibility_unlisted ),
getString( R.string.visibility_private ),
getString( R.string.visibility_direct ),
};
// public static final String VISIBILITY_PUBLIC ="public";
// public static final String VISIBILITY_UNLISTED ="unlisted";
// public static final String VISIBILITY_PRIVATE ="private";
// public static final String VISIBILITY_DIRECT ="direct";
new AlertDialog.Builder( this )
.setTitle( R.string.choose_visibility )
.setItems( caption_list, new DialogInterface.OnClickListener() {
@Override
public void onClick( DialogInterface dialog, int which ){
switch( which ){
case 0:
visibility = TootStatus.VISIBILITY_PUBLIC;
break;
case 1:
visibility = TootStatus.VISIBILITY_UNLISTED;
break;
case 2:
visibility = TootStatus.VISIBILITY_PRIVATE;
break;
case 3:
visibility = TootStatus.VISIBILITY_DIRECT;
break;
}
updateVisibility();
saveUIToData();
}
} )
.setNegativeButton( R.string.cancel, null )
.show();
}
///////////////////////////////////////////////////
private void performAccountRemove(){
new AlertDialog.Builder( this )
.setTitle( R.string.confirm )
.setMessage( R.string.confirm_account_remove )
.setNegativeButton( R.string.cancel, null )
.setPositiveButton( R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick( DialogInterface dialog, int which ){
account.delete();
finish();
}
} )
.show();
}
///////////////////////////////////////////////////
private void performAccessToken(){
}
///////////////////////////////////////////////////
}

View File

@ -0,0 +1,10 @@
package jp.juggler.subwaytooter;
import android.support.v7.app.AppCompatActivity;
/**
* Created by tateisu on 2017/04/22.
*/
public class ActAppSetting extends AppCompatActivity {
}

View File

@ -0,0 +1,10 @@
package jp.juggler.subwaytooter;
import android.support.v7.app.AppCompatActivity;
/**
* Created by tateisu on 2017/04/22.
*/
public class ActColumnList extends AppCompatActivity {
}

View File

@ -2,6 +2,7 @@ package jp.juggler.subwaytooter;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.net.Uri;
import android.os.AsyncTask;
@ -11,6 +12,7 @@ import android.support.design.widget.FloatingActionButton;
import android.support.v4.content.ContextCompat;
import android.support.v4.os.AsyncTaskCompat;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AlertDialog;
import android.view.Gravity;
import android.view.View;
import android.support.design.widget.NavigationView;
@ -29,21 +31,34 @@ import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringBufferInputStream;
import java.util.HashMap;
import java.util.HashSet;
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.TootStatus;
import jp.juggler.subwaytooter.dialog.AccountPicker;
import jp.juggler.subwaytooter.dialog.LoginForm;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.HTMLDecoder;
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));
// }
@Override
protected void onCreate( Bundle savedInstanceState ){
super.onCreate( savedInstanceState );
@ -62,6 +77,26 @@ public class ActMain extends AppCompatActivity
protected void onResume(){
super.onResume();
HTMLDecoder.link_callback = link_click_listener;
// アカウント設定から戻ってきたらカラムを消す必要があるかもしれない
int size =pager_adapter.getCount();
for(int i=size-1;i>=0;--i){
Column column = pager_adapter.getColumn( i );
SavedAccount sa = SavedAccount.loadAccount( log, column.access_info.db_id );
if( sa == null ){
pager_adapter.removeColumn( pager,column );
}
}
if(update_at_resume){
update_at_resume = false;
// TODO: 各カラムを更新する
}
if( pager_adapter.getCount() == 0){
llEmpty.setVisibility( View.VISIBLE );
}
}
@Override
@ -76,6 +111,8 @@ public class ActMain extends AppCompatActivity
DrawerLayout drawer = (DrawerLayout) findViewById( R.id.drawer_layout );
if( drawer.isDrawerOpen( GravityCompat.START ) ){
drawer.closeDrawer( GravityCompat.START );
}else if( ! pager_adapter.column_list.isEmpty() ){
performColumnClose( false,pager_adapter.getColumn( pager.getCurrentItem() ) );
}else{
super.onBackPressed();
}
@ -127,6 +164,13 @@ public class ActMain extends AppCompatActivity
}else if( id == R.id.nav_add_notifications ){
performAddTimeline( Column.TYPE_TL_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();
// Handle the camera action
// }else if( id == R.id.nav_gallery ){
//
@ -180,8 +224,8 @@ public class ActMain extends AppCompatActivity
fabMenu.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick( View view ){
if( ! drawer.isDrawerOpen( Gravity.LEFT ) ){
drawer.openDrawer( Gravity.LEFT );
if( ! drawer.isDrawerOpen( Gravity.START ) ){
drawer.openDrawer( Gravity.START );
}
}
} );
@ -202,22 +246,18 @@ public class ActMain extends AppCompatActivity
final AsyncTask< Void, String, TootApiResult > task = new AsyncTask< Void, String, TootApiResult >() {
boolean __isCancelled(){
return isCancelled();
}
long row_id;
@Override
protected TootApiResult doInBackground( Void... params ){
TootApiClient api_client = new TootApiClient( ActMain.this, new TootApiClient.Callback() {
@Override
public boolean isCancelled(){
return __isCancelled();
public boolean isApiCancelled(){
return isCancelled();
}
@Override
public void publishProgress( final String s ){
public void publishApiProgress( final String s ){
Utils.runOnMainThread( new Runnable() {
@Override
public void run(){
@ -229,11 +269,11 @@ public class ActMain extends AppCompatActivity
api_client.setUserInfo( instance, user_mail, password );
TootApiResult result = api_client.get( "/api/v1/accounts/verify_credentials" );
TootApiResult result = api_client.request( "/api/v1/accounts/verify_credentials" );
if( result != null && result.object != null ){
TootAccount ta = TootAccount.parse( log, result.object );
String user = ta.username +"@" + instance;
this.row_id = SavedAccount.insert( log, instance, user, result.object ,result.token_info );
this.row_id = SavedAccount.insert( instance, user, result.object ,result.token_info );
}
return result;
}
@ -281,13 +321,39 @@ public class ActMain extends AppCompatActivity
pager.setCurrentItem( idx );
}
public void performColumnClose( Column column ){
public void performColumnClose( boolean bConfirm,final Column column ){
if(! bConfirm ){
new AlertDialog.Builder( this )
.setTitle( R.string.confirm )
.setMessage( R.string.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 );
if( pager_adapter.getCount() == 0 ){
llEmpty.setVisibility( View.VISIBLE );
}else if( page_showing > 0 && page_showing == page_delete ){
pager.setCurrentItem( page_showing-1 ,true);
}
}
void performOpenUser(SavedAccount access_info,TootAccount user){
llEmpty.setVisibility( View.GONE );
//
Column col = new Column( ActMain.this, access_info, Column.TYPE_TL_STATUSES, user.id );
pager.setCurrentItem( pager_adapter.addColumn( pager, col ) ,true);
}
private void performAddTimeline( final int type, final Object... params ){
AccountPicker.pick( this, new AccountPicker.AccountPickerCallback() {
@Override
@ -296,7 +362,7 @@ public class ActMain extends AppCompatActivity
//
Column col = new Column( ActMain.this, ai, type, ai.id, params );
int idx = pager_adapter.addColumn( pager, col );
pager.setCurrentItem( idx );
pager.setCurrentItem( idx ,true);
}
} );
}
@ -316,8 +382,8 @@ public class ActMain extends AppCompatActivity
customTabsIntent.launchUrl( this, Uri.parse( url ) );
}catch( Throwable ex ){
ex.printStackTrace();
log.e( ex, "openChromeTab failed." );
// ex.printStackTrace();
log.e( ex, "openChromeTab failed. url=%s",url );
}
}
@ -393,8 +459,281 @@ public class ActMain extends AppCompatActivity
Column c = pager_adapter.getColumn( pager.getCurrentItem() );
if( c != null && c.access_info != null ){
ActPost.open( this, c.access_info.db_id );
ActPost.open( this, c.access_info.db_id ,null );
}
}
/////////////////////////////////////////////////////////////////////////
private void showColumnMatchAccount( SavedAccount account ){
for( Column column : pager_adapter.column_list ){
if( account.user.equals( column.access_info.user ) ){
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 new_state = ! 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(
(new_state
? "/api/v1/statuses/"+status.id+"/favourite"
: "/api/v1/statuses/"+status.id+"/unfavourite"
)
, request_builder );
if( result.object != null ){
new_status = TootStatus.parse( log,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( new_state && new_status.favourites_count <= status.favourites_count ){
// 星つけたのにカウントが上がらないのは違和感あるので表示をいじる
new_status.favourites_count = status.favourites_count +1;
}else if( !new_state && 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, 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);
}
////////////////////////////////////////
public void performMore( SavedAccount account, TootStatus status ){
// open menu
// Expand this status
// Mute user
// Block user
// report user
Utils.showToast( this,false,"not implemented. toot="+status.decoded_content );
}
public void performReply( SavedAccount account, TootStatus status ){
Utils.showToast( this,false,"not implemented. toot="+status.decoded_content );
}
////////////////////////////////////////
private void performColumnList(){
Utils.showToast( this,false,"not implemented." );
}
private void performAccountSetting(){
AccountPicker.pick( this, new AccountPicker.AccountPickerCallback() {
@Override
public void onAccountPicked( SavedAccount ai ){
ActAccountSetting.open( ActMain.this, ai);
}
} );
}
private void performAppSetting(){
Utils.showToast( this,false,"not implemented." );
}
}

View File

@ -1,44 +1,122 @@
package jp.juggler.subwaytooter;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.OpenableColumns;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v4.os.AsyncTaskCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.TextView;
import com.android.volley.toolbox.NetworkImageView;
import org.apache.commons.io.IOUtils;
import org.json.JSONArray;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import jp.juggler.subwaytooter.api.TootApiClient;
import jp.juggler.subwaytooter.api.TootApiResult;
import jp.juggler.subwaytooter.api.entity.TootAttachment;
import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okio.BufferedSink;
public class ActPost extends AppCompatActivity implements View.OnClickListener {
static final LogCategory log = new LogCategory( "ActPost" );
static final String KEY_ACCOUNT_DB_ID = "account_db_id";
static final String KEY_VISIBILITY = "visibility";
static final String KEY_ATTACHMENT_LIST = "attachment_list";
public static void open( Context context, long account_db_id ){
public static void open( Context context, long account_db_id, String visibility ){
Intent intent = new Intent( context, ActPost.class );
intent.putExtra( KEY_ACCOUNT_DB_ID, account_db_id );
if( visibility != null ) intent.putExtra( KEY_VISIBILITY, visibility );
context.startActivity( intent );
}
@Override
public void onClick( View v ){
switch( v.getId() ){
case R.id.btnAccount:
performAccountChooser();
break;
case R.id.btnVisibility:
performVisibility();
break;
case R.id.btnAttachment:
performAttachment();
break;
case R.id.ivMedia1:
performAttachmentDelete( 0 );
break;
case R.id.ivMedia2:
performAttachmentDelete( 1 );
break;
case R.id.ivMedia3:
performAttachmentDelete( 2 );
break;
case R.id.ivMedia4:
performAttachmentDelete( 3 );
break;
case R.id.btnPost:
performPost();
break;
}
}
static final int REQUEST_CODE_ATTACHMENT = 1;
@Override
protected void onActivityResult( int requestCode, int resultCode, Intent data ){
if( resultCode == RESULT_OK ){
if( requestCode == REQUEST_CODE_ATTACHMENT ){
if( data != null ){
Uri uri = data.getData();
if( uri != null ){
String type = data.getType();
if( TextUtils.isEmpty( type ) ){
type = getContentResolver().getType( uri );
}
addAttachment( uri, type );
}
}
}
}
super.onActivityResult( requestCode, resultCode, data );
}
@Override
@ -54,34 +132,7 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener {
}
if( savedInstanceState != null ){
}else{
Intent intent = getIntent();
long account_db_id = intent.getLongExtra( KEY_ACCOUNT_DB_ID, SavedAccount.INVALID_ID );
if( account_db_id != SavedAccount.INVALID_ID ){
for( int i = 0, ie = account_list.size() ; i < ie ; ++ i ){
SavedAccount a = account_list.get( i );
if( a.db_id == account_db_id ){
setAccount( a );
break;
}
}
}
}
if( this.account == null ){
setAccount( null );
}
updateContentWarning();
updateMediaAttachment();
}
@Override
protected void onRestoreInstanceState( Bundle savedInstanceState ){
super.onRestoreInstanceState( savedInstanceState );
if( savedInstanceState != null ){
long account_db_id = savedInstanceState.getLong(KEY_ACCOUNT_DB_ID,SavedAccount.INVALID_ID);
long account_db_id = savedInstanceState.getLong( KEY_ACCOUNT_DB_ID, SavedAccount.INVALID_ID );
if( account_db_id != SavedAccount.INVALID_ID ){
for( int i = 0, ie = account_list.size() ; i < ie ; ++ i ){
SavedAccount a = account_list.get( i );
@ -92,22 +143,81 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener {
}
}
String sv = savedInstanceState.getString( KEY_VISIBILITY );
if( TextUtils.isEmpty( sv ) ) sv = account.visibility;
this.visibility = sv;
sv = savedInstanceState.getString( KEY_ATTACHMENT_LIST );
if( ! TextUtils.isEmpty( sv ) ){
try{
attachment_list.clear();
JSONArray array = new JSONArray( sv );
for( int i = 0, ie = array.length() ; i < ie ; ++ i ){
try{
TootAttachment a = TootAttachment.parse( log, array.optJSONObject( i ) );
if( a != null ){
PostAttachment pa = new PostAttachment();
pa.status = ATTACHMENT_UPLOADED;
pa.attachment = a;
}
}catch( Throwable ex2 ){
ex2.printStackTrace();
}
}
}catch( Throwable ex ){
ex.printStackTrace();
}
}
}else{
Intent intent = getIntent();
long account_db_id = intent.getLongExtra( KEY_ACCOUNT_DB_ID, SavedAccount.INVALID_ID );
if( account_db_id != SavedAccount.INVALID_ID ){
for( int i = 0, ie = account_list.size() ; i < ie ; ++ i ){
SavedAccount a = account_list.get( i );
if( a.db_id == account_db_id ){
setAccount( a );
break;
}
}
}
String sv = intent.getStringExtra( KEY_VISIBILITY );
if( TextUtils.isEmpty( sv ) ) sv = account.visibility;
this.visibility = sv;
}
if( this.account == null ){
setAccount( null );
}
updateContentWarning();
updateMediaAttachment();
showMediaAttachment();
updateVisibility();
updateTextCount();
}
@Override
protected void onSaveInstanceState( Bundle outState ){
if( account != null ){
outState.putLong( KEY_ACCOUNT_DB_ID,account.db_id );
outState.putLong( KEY_ACCOUNT_DB_ID, account.db_id );
}
if( visibility != null ){
outState.putString( KEY_VISIBILITY, visibility );
}
if( ! attachment_list.isEmpty() ){
JSONArray array = new JSONArray();
for( PostAttachment pa : attachment_list ){
if( pa.status == ATTACHMENT_UPLOADED ){
// アップロード完了したものだけ保持する
array.put( pa.attachment.json );
}
}
outState.putString( KEY_ATTACHMENT_LIST, array.toString() );
}
}
Button btnAccount;
ImageButton btnVisibility;
View btnAttachment;
View btnPost;
View llAttachment;
@ -115,17 +225,18 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener {
NetworkImageView ivMedia2;
NetworkImageView ivMedia3;
NetworkImageView ivMedia4;
CheckBox cbNSFW;
CheckBox cbContentWarning;
EditText etContentWarning;
EditText etContent;
TextView tvCharCount;
ArrayList< SavedAccount > account_list;
SavedAccount account;
private void initUI(){
setContentView( R.layout.act_post );
btnAccount = (Button) findViewById( R.id.btnAccount );
btnVisibility = (ImageButton) findViewById( R.id.btnVisibility );
btnAttachment = findViewById( R.id.btnAttachment );
btnPost = findViewById( R.id.btnPost );
llAttachment = findViewById( R.id.llAttachment );
@ -133,6 +244,7 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener {
ivMedia2 = (NetworkImageView) findViewById( R.id.ivMedia2 );
ivMedia3 = (NetworkImageView) findViewById( R.id.ivMedia3 );
ivMedia4 = (NetworkImageView) findViewById( R.id.ivMedia4 );
cbNSFW = (CheckBox) findViewById( R.id.cbNSFW );
cbContentWarning = (CheckBox) findViewById( R.id.cbContentWarning );
etContentWarning = (EditText) findViewById( R.id.etContentWarning );
etContent = (EditText) findViewById( R.id.etContent );
@ -147,13 +259,14 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener {
} );
btnAccount.setOnClickListener( this );
btnVisibility.setOnClickListener( this );
btnAttachment.setOnClickListener( this );
btnPost.setOnClickListener( this );
llAttachment = findViewById( R.id.llAttachment );
ivMedia1.setOnClickListener( this );
ivMedia2.setOnClickListener( this );
ivMedia3.setOnClickListener( this );
ivMedia4.setOnClickListener( this );
cbContentWarning.setOnCheckedChangeListener( new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged( CompoundButton buttonView, boolean isChecked ){
@ -180,53 +293,451 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener {
}
private void updateTextCount(){
tvCharCount.setText( 500 - etContent.getText().length() );
}
void setAccount( SavedAccount a ){
this.account = a;
btnAccount.setText( a == null ? getString( R.string.not_selected ) : a.getFullAcct( a ) );
tvCharCount.setText( Integer.toString( 500 - etContent.getText().length() ) );
}
private void updateContentWarning(){
etContentWarning.setVisibility( cbContentWarning.isChecked() ? View.VISIBLE : View.GONE );
}
//////////////////////////////////////////////////////////
// Account
private void updateMediaAttachment(){
if( attachment_list.isEmpty() ){
llAttachment.setVisibility( View.GONE );
}else{
llAttachment.setVisibility( View.VISIBLE );
showAttachment( ivMedia1, 0 );
showAttachment( ivMedia2, 0 );
showAttachment( ivMedia3, 0 );
showAttachment( ivMedia4, 0 );
}
SavedAccount account;
void setAccount( SavedAccount a ){
this.account = a;
btnAccount.setText(
( a == null ? getString( R.string.not_selected ) : a.getFullAcct( a ) )
);
}
private void showAttachment( NetworkImageView iv, int idx ){
if( idx >= attachment_list.size() ){
iv.setVisibility( View.GONE );
}else{
iv.setVisibility( View.VISIBLE );
PostAttachment a = attachment_list.get( idx );
if( a.status == ATTACHMENT_UPLOADING ){
iv.setImageResource( R.drawable.ic_loading );
}else{
iv.setImageBitmap( a.bitmap );
}
private void performAccountChooser(){
// TODO: mention の状況によっては別サーバを選べないかもしれない
// TODO: 添付ファイルがあったら確認の上添付ファイルを捨てないと切り替えられない
final ArrayList< SavedAccount > tmp_account_list = new ArrayList<>();
tmp_account_list.addAll( account_list );
String[] caption_list = new String[ tmp_account_list.size() ];
for( int i = 0, ie = tmp_account_list.size() ; i < ie ; ++ i ){
caption_list[ i ] = tmp_account_list.get( i ).user;
}
new AlertDialog.Builder( this )
.setTitle( R.string.choose_account )
.setItems( caption_list, new DialogInterface.OnClickListener() {
@Override
public void onClick( DialogInterface dialog, int which ){
if( which >= 0 && which < tmp_account_list.size() ){
setAccount( tmp_account_list.get( which ) );
}
}
} )
.setNegativeButton( R.string.cancel, null )
.show();
}
//////////////////////////////////////////////////////////
// Attachment
static final int ATTACHMENT_UPLOADING = 1;
static final int ATTACHMENT_UPLOADED = 2;
static class PostAttachment {
int status;
Bitmap bitmap;
String url;
TootAttachment attachment;
}
final ArrayList< PostAttachment > attachment_list = new ArrayList<>();
private void showMediaAttachment(){
if( attachment_list.isEmpty() ){
llAttachment.setVisibility( View.GONE );
cbNSFW.setVisibility( View.GONE );
}else{
llAttachment.setVisibility( View.VISIBLE );
cbNSFW.setVisibility( View.VISIBLE );
showAttachment_sub( ivMedia1, 0 );
showAttachment_sub( ivMedia2, 1 );
showAttachment_sub( ivMedia3, 2 );
showAttachment_sub( ivMedia4, 3 );
}
}
private void showAttachment_sub( NetworkImageView iv, int idx ){
if( idx >= attachment_list.size() ){
iv.setVisibility( View.GONE );
}else{
iv.setVisibility( View.VISIBLE );
PostAttachment a = attachment_list.get( idx );
if( a.status == ATTACHMENT_UPLOADING ){
iv.setImageDrawable( ContextCompat.getDrawable(this,R.drawable.ic_loading ));
}else if( a.attachment != null ){
iv.setImageUrl( a.attachment.preview_url, App1.getImageLoader() );
}else{
iv.setImageDrawable( ContextCompat.getDrawable(this,R.drawable.ic_unknown ));
}
}
}
// 添付した画像をタップ
void performAttachmentDelete( int idx ){
final PostAttachment pa = attachment_list.get( idx );
new AlertDialog.Builder( this )
.setTitle( R.string.confirm_delete_attachment )
.setPositiveButton( R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick( DialogInterface dialog, int which ){
try{
attachment_list.remove( pa );
}catch( Throwable ignored ){
}
showMediaAttachment();
}
} )
.setNegativeButton( R.string.cancel, null )
.show();
}
private void performAttachment(){
if( attachment_list.size() >= 4 ){
Utils.showToast( this, false, R.string.attachment_too_many );
return;
}
if( account == null ){
Utils.showToast( this, false, R.string.account_select_please );
return;
}
// SAFのIntentで開く
try{
Intent intent = new Intent( Intent.ACTION_OPEN_DOCUMENT );
intent.addCategory( Intent.CATEGORY_OPENABLE );
intent.setType( "*/*" );
intent.putExtra( Intent.EXTRA_MIME_TYPES, new String[]{ "image/*", "video/*" } );
startActivityForResult( intent, REQUEST_CODE_ATTACHMENT );
}catch( Throwable ex ){
ex.printStackTrace();
Utils.showToast( this, ex, "ACTION_OPEN_DOCUMENT failed." );
}
}
static final byte[] hex = Utils.encodeUTF8( "0123456789abcdef" );
void addAttachment( final Uri uri, final String mime_type ){
if( attachment_list.size() >= 4 ){
Utils.showToast( this, false, R.string.attachment_too_many );
return;
}
if( account == null ){
Utils.showToast( this, false, R.string.account_select_please );
return;
}
final PostAttachment pa = new PostAttachment();
pa.status = ATTACHMENT_UPLOADING;
attachment_list.add( pa );
showMediaAttachment();
new AsyncTask< Void, Void, TootApiResult >() {
final SavedAccount target_account = account;
@Override
protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( ActPost.this, new TootApiClient.Callback() {
@Override
public boolean isApiCancelled(){
return isCancelled();
}
@Override
public void publishApiProgress( String s ){
}
} );
client.setAccount( target_account );
if( TextUtils.isEmpty( mime_type ) ){
return new TootApiResult( "mime_type is null." );
}
try{
final long content_length = getStreamSize( true, getContentResolver().openInputStream( uri ) );
if( content_length > 8000000 ){
return new TootApiResult( getString( R.string.file_size_too_big ) );
}
RequestBody multipart_body = new MultipartBody.Builder()
.setType( MultipartBody.FORM )
.addFormDataPart(
"file"
, getDocumentName( uri )
, new RequestBody() {
@Override
public MediaType contentType(){
return MediaType.parse( mime_type );
}
@Override
public long contentLength() throws IOException{
return content_length;
}
@Override
public void writeTo( BufferedSink sink ) throws IOException{
InputStream is = getContentResolver().openInputStream( uri );
try{
byte[] tmp = new byte[ 4096 ];
for( ; ; ){
int r = is.read( tmp, 0, tmp.length );
if( r <= 0 ) break;
sink.write( tmp, 0, r );
}
}finally{
IOUtils.closeQuietly( is );
}
}
}
)
.build();
Request.Builder request_builder = new Request.Builder()
.post( multipart_body );
TootApiResult result = client.request( "/api/v1/media", request_builder );
if( result.object != null ){
pa.attachment = TootAttachment.parse( log, result.object );
if( pa.attachment == null ){
result.error = "TootAttachment.parse failed";
}
}
return result;
}catch( Throwable ex ){
return new TootApiResult( Utils.formatError( ex, "read failed." ) );
}
}
@Override
protected void onCancelled(){
onPostExecute( null );
}
@Override
protected void onPostExecute( TootApiResult result ){
pa.status = ATTACHMENT_UPLOADED;
if( pa.attachment == null ){
if( result != null ){
Utils.showToast( ActPost.this, true, result.error );
}
attachment_list.remove( pa );
}else{
String sv = etContent.getText().toString();
sv = sv + pa.attachment.text_url+" ";
etContent.setText(sv);
}
showMediaAttachment();
}
}.execute();
}
public String getDocumentName( Uri uri ){
Cursor cursor = getContentResolver().query( uri, null, null, null, null, null );
try{
if( cursor != null && cursor.moveToFirst() ){
return cursor.getString( cursor.getColumnIndex( OpenableColumns.DISPLAY_NAME ) );
}
}finally{
cursor.close();
}
return null;
}
long getStreamSize( boolean bClose, InputStream is ) throws IOException{
try{
long size = 0L;
for( ; ; ){
long r = IOUtils.skip( is, 16384 );
if( r <= 0 ) break;
size += r;
}
return size;
}finally{
if( bClose ) IOUtils.closeQuietly( is );
}
}
//////////////////////////////////////////////////////////////////////
// visibility
String visibility = TootStatus.VISIBILITY_PUBLIC;
private void updateVisibility(){
btnVisibility.setImageResource( Styler.getVisibilityIcon(visibility) );
}
private void performVisibility(){
final String[] caption_list = new String[]{
getString( R.string.visibility_public ),
getString( R.string.visibility_unlisted ),
getString( R.string.visibility_private ),
getString( R.string.visibility_direct ),
};
// public static final String VISIBILITY_PUBLIC ="public";
// public static final String VISIBILITY_UNLISTED ="unlisted";
// public static final String VISIBILITY_PRIVATE ="private";
// public static final String VISIBILITY_DIRECT ="direct";
new AlertDialog.Builder( this )
.setTitle( R.string.choose_visibility )
.setItems( caption_list, new DialogInterface.OnClickListener() {
@Override
public void onClick( DialogInterface dialog, int which ){
switch( which ){
case 0:
visibility = TootStatus.VISIBILITY_PUBLIC;
break;
case 1:
visibility = TootStatus.VISIBILITY_UNLISTED;
break;
case 2:
visibility = TootStatus.VISIBILITY_PRIVATE;
break;
case 3:
visibility = TootStatus.VISIBILITY_DIRECT;
break;
}
updateVisibility();
}
} )
.setNegativeButton( R.string.cancel, null )
.show();
}
///////////////////////////////////////////////////////////////////////////////////////
// post
private void performPost(){
final String content = etContent.getText().toString().trim();
if(TextUtils.isEmpty( content ) ){
Utils.showToast( this,true,R.string.post_error_contents_empty );
return;
}
final String spoiler_text;
if( !cbContentWarning.isChecked() ){
spoiler_text = null;
}else{
spoiler_text = etContentWarning.getText().toString().trim();
if( TextUtils.isEmpty( spoiler_text ) ){
Utils.showToast( this, true, R.string.post_error_contents_warning_empty );
return;
}
}
final StringBuilder sb = new StringBuilder( );
sb.append("status=");
sb.append(Uri.encode( content ));
sb.append("&visibility=");
sb.append(Uri.encode( visibility ));
if( cbNSFW.isChecked() ){
sb.append("&sensitive=1");
}
if( spoiler_text != null ){
sb.append("&spoiler_text=");
sb.append(Uri.encode( spoiler_text ));
}
for(PostAttachment pa : attachment_list){
if( pa.attachment != null ){
sb.append("&media_ids[]="+pa.attachment.id);
}
}
// TODO: in_reply_to_id (optional): local ID of the status you want to reply to
final ProgressDialog progress = new ProgressDialog( this );
final AsyncTask< Void, Void, TootApiResult > task = new AsyncTask< Void, Void, TootApiResult >() {
final SavedAccount target_account = account;
TootStatus status;
@Override
protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( ActPost.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( target_account );
Request.Builder request_builder = new Request.Builder()
.post( RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
,sb.toString()
));
TootApiResult result = client.request( "/api/v1/statuses", request_builder );
if( result.object != null ){
status = TootStatus.parse( log,result.object );
}
return result;
}
@Override
protected void onCancelled(){
onPostExecute( null );
}
@Override
protected void onPostExecute( TootApiResult result ){
progress.dismiss();
if( status != null ){
ActMain.update_at_resume = true;
ActPost.this.finish();
}else{
if( result != null ){
Utils.showToast( ActPost.this, true, result.error );
}
}
}
};
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 );
}
}

View File

@ -5,6 +5,7 @@ import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.graphics.Bitmap;
import android.graphics.Typeface;
import android.support.v4.util.LruCache;
import android.widget.ImageView;
@ -19,12 +20,30 @@ import jp.juggler.subwaytooter.table.ContentWarning;
import jp.juggler.subwaytooter.table.LogData;
import jp.juggler.subwaytooter.table.MediaShown;
import jp.juggler.subwaytooter.table.SavedAccount;
import okhttp3.OkHttpClient;
import uk.co.chrisjenx.calligraphy.CalligraphyConfig;
import uk.co.chrisjenx.calligraphy.TypefaceUtils;
public class App1 extends Application {
@Override
public void onCreate(){
super.onCreate();
CalligraphyConfig.initDefault(new CalligraphyConfig.Builder()
.setDefaultFontPath("NotoSansCJKjp-Regular.otf")
.setFontAttrId(R.attr.fontPath)
.build()
);
if( typeface_emoji == null ){
typeface_emoji = TypefaceUtils.load(getAssets(), "emojione_android.ttf");
}
if( typeface_normal == null ){
typeface_normal = TypefaceUtils.load(getAssets(), "NotoSansCJKjp-Regular.otf");
}
if( db_open_helper == null ){
db_open_helper = new DBOpenHelper( getApplicationContext() );
db_open_helper.onCreate( getDB() );
@ -134,4 +153,8 @@ public class App1 extends Application {
}
public static final OkHttpClient ok_http_client = new OkHttpClient();
public static Typeface typeface_emoji ;
public static Typeface typeface_normal ;
}

View File

@ -101,6 +101,27 @@ public class Column {
}
}
public interface StatusEntryCallback{
void onIterate(TootStatus status);
}
public void findStatus( SavedAccount target_account,long target_status_id ,StatusEntryCallback callback){
if( target_account.user.equals( access_info.user ) ){
for( int i = 0, ie = status_list.size() ; i < ie ; ++ i ){
TootStatus status = status_list.get( i );
if( target_status_id == status.id ){
callback.onIterate( status );
}
TootStatus reblog = status.reblog;
if( reblog!= null ){
if( target_status_id == reblog.id ){
callback.onIterate( status );
}
}
}
}
}
public interface VisualCallback {
void onVisualColumn();
}
@ -126,7 +147,7 @@ public class Column {
}
}
private void fireVisualCallback(){
public void fireVisualCallback(){
Iterator< VisualCallback > it = visual_callback.iterator();
while( it.hasNext() ){
it.next().onVisualColumn();
@ -160,9 +181,6 @@ public class Column {
cancelLastTask();
AsyncTask< Void, Void, TootApiResult > task = this.last_task = new AsyncTask< Void, Void, TootApiResult >() {
boolean __isCancelled(){
return isCancelled();
}
TootStatus.List tmp_list_status;
TootReport.List tmp_list_report;
@ -200,12 +218,12 @@ public class Column {
protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( activity, new TootApiClient.Callback() {
@Override
public boolean isCancelled(){
return __isCancelled() || is_dispose.get();
public boolean isApiCancelled(){
return isCancelled() || is_dispose.get();
}
@Override
public void publishProgress( final String s ){
public void publishApiProgress( final String s ){
Utils.runOnMainThread( new Runnable() {
@Override
public void run(){
@ -222,30 +240,30 @@ public class Column {
switch( type ){
default:
case TYPE_TL_HOME:
return parseStatuses( client.get( "/api/v1/timelines/home" ) );
return parseStatuses( client.request( "/api/v1/timelines/home" ) );
case TYPE_TL_LOCAL:
return parseStatuses( client.get( "/api/v1/timelines/public?local=1" ) );
return parseStatuses( client.request( "/api/v1/timelines/public?local=1" ) );
case TYPE_TL_FEDERATE:
return parseStatuses( client.get( "/api/v1/timelines/public" ) );
return parseStatuses( client.request( "/api/v1/timelines/public" ) );
case TYPE_TL_STATUSES:
if( who_account == null ){
parseAccount( client.get( "/api/v1/accounts/" + who_id ) );
client.callback.publishProgress( "" );
parseAccount( client.request( "/api/v1/accounts/" + who_id ) );
client.callback.publishApiProgress( "" );
}
return parseStatuses( client.get( "/api/v1/accounts/" + who_id + "/statuses" ) );
return parseStatuses( client.request( "/api/v1/accounts/" + who_id + "/statuses" ) );
case TYPE_TL_FAVOURITES:
return parseStatuses( client.get( "/api/v1/favourites" ) );
return parseStatuses( client.request( "/api/v1/favourites" ) );
case TYPE_TL_REPORTS:
return parseReports( client.get( "/api/v1/reports" ) );
return parseReports( client.request( "/api/v1/reports" ) );
case TYPE_TL_NOTIFICATIONS:
return parseNotifications( client.get( "/api/v1/notifications" ) );
return parseNotifications( client.request( "/api/v1/notifications" ) );
}
}

View File

@ -27,10 +27,17 @@ public class ColumnPagerAdapter extends PagerAdapter{
final SparseArray<ColumnViewHolder> holder_list = new SparseArray<>();
int addColumn( ViewPager pager, Column column ){
return addColumn( pager,column,pager.getCurrentItem()+1 );
}
int addColumn( ViewPager pager, Column column,int index ){
int size = column_list.size();
column_list.add( column );
if( index > size ) index = size;
pager.setAdapter( null );
column_list.add( index,column );
pager.setAdapter( this );
notifyDataSetChanged();
return size;
return index;
}
public void removeColumn( ViewPager pager,Column column ){
@ -40,7 +47,7 @@ public class ColumnPagerAdapter extends PagerAdapter{
pager.setAdapter( null );
column_list.remove( idx_column );
pager.setAdapter( this );
pager.setCurrentItem( idx_showing >= column_list.size() ? idx_showing -1 : idx_showing );
}

View File

@ -27,6 +27,7 @@ import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.table.ContentWarning;
import jp.juggler.subwaytooter.table.MediaShown;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.HTMLDecoder;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
@ -90,7 +91,7 @@ public class ColumnViewHolder implements View.OnClickListener, Column.VisualCall
public void onClick( View v ){
switch( v.getId() ){
case R.id.btnColumnClose:
activity.performColumnClose( column );
activity.performColumnClose( false, column );
break;
case R.id.btnColumnReload:
column.reload();
@ -99,10 +100,10 @@ public class ColumnViewHolder implements View.OnClickListener, Column.VisualCall
}
void showError(String message){
void showError( String message ){
tvLoading.setVisibility( View.VISIBLE );
listView.setVisibility( View.GONE );
tvLoading.setText( message);
tvLoading.setText( message );
}
@Override
@ -112,18 +113,18 @@ public class ColumnViewHolder implements View.OnClickListener, Column.VisualCall
tvColumnName.setText( column.getColumnName() );
if( column.is_dispose.get() ){
showError("column was disposed.");
showError( "column was disposed." );
return;
}
if( vh_header != null ){
vh_header.bind( activity, column.access_info, column.who_account );
}
if( column.is_loading ){
String message = column.task_progress;
if( message == null ) message = "loading?";
showError( message);
showError( message );
return;
}
@ -135,7 +136,7 @@ public class ColumnViewHolder implements View.OnClickListener, Column.VisualCall
case Column.TYPE_TL_FAVOURITES:
case Column.TYPE_TL_STATUSES:
if( column.status_list.isEmpty() && vh_header == null ){
showError(activity.getString(R.string.list_empty));
showError( activity.getString( R.string.list_empty ) );
}else{
tvLoading.setVisibility( View.GONE );
listView.setVisibility( View.VISIBLE );
@ -144,7 +145,7 @@ public class ColumnViewHolder implements View.OnClickListener, Column.VisualCall
break;
case Column.TYPE_TL_REPORTS:
if( column.report_list.isEmpty() ){
showError(activity.getString(R.string.list_empty));
showError( activity.getString( R.string.list_empty ) );
}else{
tvLoading.setVisibility( View.GONE );
listView.setVisibility( View.VISIBLE );
@ -153,7 +154,7 @@ public class ColumnViewHolder implements View.OnClickListener, Column.VisualCall
break;
case Column.TYPE_TL_NOTIFICATIONS:
if( column.notification_list.isEmpty() ){
showError(activity.getString(R.string.list_empty));
showError( activity.getString( R.string.list_empty ) );
}else{
tvLoading.setVisibility( View.GONE );
listView.setVisibility( View.VISIBLE );
@ -231,6 +232,7 @@ public class ColumnViewHolder implements View.OnClickListener, Column.VisualCall
final NetworkImageView ivFollow;
final TextView tvFollowerName;
final TextView tvFollowerAcct;
final ImageButton btnFollow;
final View llStatus;
final NetworkImageView ivThumbnail;
@ -240,6 +242,10 @@ public class ColumnViewHolder implements View.OnClickListener, Column.VisualCall
final View llContentWarning;
final TextView tvContentWarning;
final Button btnContentWarning;
final View llContents;
final TextView tvTags;
final TextView tvMentions;
final TextView tvContent;
final View flMedia;
@ -256,8 +262,12 @@ public class ColumnViewHolder implements View.OnClickListener, Column.VisualCall
final Button btnFavourite;
final ImageButton btnMore;
TootStatus status;
SavedAccount account;
TootAccount account_thumbnail;
TootAccount account_boost;
TootAccount account_follow;
public StatusViewHolder( View view ){
this.llBoosted = view.findViewById( R.id.llBoosted );
@ -269,6 +279,7 @@ public class ColumnViewHolder implements View.OnClickListener, Column.VisualCall
this.ivFollow = (NetworkImageView) view.findViewById( R.id.ivFollow );
this.tvFollowerName = (TextView) view.findViewById( R.id.tvFollowerName );
this.tvFollowerAcct = (TextView) view.findViewById( R.id.tvFollowerAcct );
this.btnFollow = (ImageButton)view.findViewById( R.id.btnFollow );
this.llStatus = view.findViewById( R.id.llStatus );
@ -279,7 +290,12 @@ public class ColumnViewHolder implements View.OnClickListener, Column.VisualCall
this.llContentWarning = view.findViewById( R.id.llContentWarning );
this.tvContentWarning = (TextView) view.findViewById( R.id.tvContentWarning );
this.btnContentWarning = (Button) view.findViewById( R.id.btnContentWarning );
this.llContents = view.findViewById( R.id.llContents );
this.tvContent = (TextView) view.findViewById( R.id.tvContent );
this.tvTags = (TextView) view.findViewById( R.id.tvTags );
this.tvMentions = (TextView) view.findViewById( R.id.tvMentions );
this.btnReply = (ImageButton) view.findViewById( R.id.btnReply );
this.btnBoost = (Button) view.findViewById( R.id.btnBoost );
this.btnFavourite = (Button) view.findViewById( R.id.btnFavourite );
@ -301,11 +317,24 @@ public class ColumnViewHolder implements View.OnClickListener, Column.VisualCall
ivMedia3.setOnClickListener( this );
ivMedia4.setOnClickListener( this );
btnReply.setOnClickListener( this );
btnBoost.setOnClickListener( this );
btnFavourite.setOnClickListener( this );
btnMore.setOnClickListener( this );
ivThumbnail.setOnClickListener( this );
tvName.setOnClickListener( this );
llBoosted.setOnClickListener( this );
llFollow.setOnClickListener( this );
btnFollow.setOnClickListener( this );
tvContent.setMovementMethod( LinkMovementMethod.getInstance() );
tvTags.setMovementMethod( LinkMovementMethod.getInstance() );
tvMentions.setMovementMethod( LinkMovementMethod.getInstance() );
}
public void bind( ActMain activity, View view, Object item, SavedAccount account ){
this.account = account;
public void bind( ActMain activity, View view, Object item, SavedAccount access_info ){
this.account = access_info;
llBoosted.setVisibility( View.GONE );
llFollow.setVisibility( View.GONE );
@ -316,36 +345,50 @@ public class ColumnViewHolder implements View.OnClickListener, Column.VisualCall
if( item instanceof TootNotification ){
TootNotification n = (TootNotification) item;
if( TootNotification.TYPE_FAVOURITE.equals( n.type ) ){
account_boost = n.account;
llBoosted.setVisibility( View.VISIBLE );
ivBoosted.setImageResource( R.drawable.btn_favourite );
tvBoostedTime.setText( TootStatus.formatTime( n.time_created_at )
+ "\n" + account.getFullAcct( n.account )
+ "\n" + access_info.getFullAcct(account_boost )
);
tvBoosted.setText( activity.getString( R.string.favourited_by, n.account.display_name ) );
if( n.status != null ) bindSub( activity, view, n.status, account );
tvBoosted.setText( Utils.formatSpannable1( activity,R.string.display_name_favourited_by, account_boost.display_name));
if( n.status != null ) bindSub( activity, view, n.status, access_info );
}else if( TootNotification.TYPE_REBLOG.equals( n.type ) ){
account_boost = n.account;
llBoosted.setVisibility( View.VISIBLE );
ivBoosted.setImageResource( R.drawable.btn_boost );
tvBoostedTime.setText( TootStatus.formatTime( n.time_created_at )
+ "\n" + account.getFullAcct( n.account )
+ "\n" + access_info.getFullAcct(account_boost )
);
tvBoosted.setText( activity.getString( R.string.boosted_by, n.account.display_name ) );
if( n.status != null ) bindSub( activity, view, n.status, account );
tvBoosted.setText( Utils.formatSpannable1( activity, R.string.display_name_boosted_by, account_boost.display_name ) );
account_boost = n.account;
if( n.status != null ) bindSub( activity, view, n.status, access_info );
}else if( TootNotification.TYPE_FOLLOW.equals( n.type ) ){
account_boost = n.account;
llBoosted.setVisibility( View.VISIBLE );
ivBoosted.setImageResource( R.drawable.btn_boost );
tvBoostedTime.setText( TootStatus.formatTime( n.time_created_at )
+ "\n" + account.getFullAcct( n.account )
+ "\n" + access_info.getFullAcct( account_boost )
);
tvBoosted.setText( activity.getString( R.string.boosted_by, n.account.display_name ) );
tvBoosted.setText( Utils.formatSpannable1( activity, R.string.display_name_followed_by, account_boost.display_name ) );
//
account_follow = n.account;
llFollow.setVisibility( View.VISIBLE );
ivFollow.setImageUrl( n.account.avatar_static, App1.getImageLoader() );
tvFollowerName.setText( n.account.display_name );
tvFollowerAcct.setText( account.getFullAcct( n.account ) );
ivFollow.setImageUrl( account_follow.avatar_static, App1.getImageLoader() );
tvFollowerName.setText(account_follow.display_name );
tvFollowerAcct.setText( access_info.getFullAcct( account_follow ) );
}else if( TootNotification.TYPE_MENTION.equals( n.type ) ){
if( n.status != null ) bindSub( activity, view, n.status, account );
account_boost = n.account;
llBoosted.setVisibility( View.VISIBLE );
ivBoosted.setImageResource( R.drawable.btn_reply );
tvBoostedTime.setText( TootStatus.formatTime( n.time_created_at )
+ "\n" + access_info.getFullAcct(account_boost )
);
tvBoosted.setText( Utils.formatSpannable1( activity, R.string.display_name_replied_by, account_boost.display_name ) );
if( n.status != null ) bindSub( activity, view, n.status, access_info );
}
return;
}
@ -353,22 +396,23 @@ public class ColumnViewHolder implements View.OnClickListener, Column.VisualCall
if( item instanceof TootStatus ){
TootStatus status = (TootStatus) item;
if( status.reblog != null ){
account_boost = status.account;
llBoosted.setVisibility( View.VISIBLE );
ivBoosted.setImageResource( R.drawable.btn_boost );
tvBoostedTime.setText( TootStatus.formatTime( status.time_created_at )
+ "\n" + account.getFullAcct( status.account )
+ "\n" + access_info.getFullAcct( account_boost )
);
tvBoosted.setText( activity.getString( R.string.boosted_by, status.account.display_name ) );
bindSub( activity, view, status.reblog, account );
tvBoosted.setText( Utils.formatSpannable1( activity, R.string.display_name_boosted_by, status.account.display_name ) );
bindSub( activity, view, status.reblog, access_info );
}else{
bindSub( activity, view, status, account );
bindSub( activity, view, status, access_info );
}
}
}
private void bindSub( ActMain activity, View view, TootStatus status, SavedAccount account ){
this.status = status;
account_thumbnail = status.account;
llStatus.setVisibility( View.VISIBLE );
tvTime.setText( TootStatus.formatTime( status.time_created_at )
+ "\n" + account.getFullAcct( status.account )
@ -377,14 +421,29 @@ public class ColumnViewHolder implements View.OnClickListener, Column.VisualCall
ivThumbnail.setImageUrl( status.account.avatar_static, App1.getImageLoader() );
tvContent.setText( status.decoded_content );
if( status.decoded_tags == null ){
tvTags.setVisibility( View.GONE );
}else{
tvTags.setVisibility( View.VISIBLE );
tvTags.setText( status.decoded_tags);
}
if( status.decoded_mentions == null ){
tvMentions.setVisibility( View.GONE );
}else{
tvMentions.setVisibility( View.VISIBLE );
tvMentions.setText( status.decoded_mentions);
}
// Content warning
if( TextUtils.isEmpty( status.spoiler_text ) ){
llContentWarning.setVisibility( View.GONE );
tvContent.setVisibility( View.VISIBLE );
llContents.setVisibility( View.VISIBLE );
}else{
llContentWarning.setVisibility( View.VISIBLE );
tvContentWarning.setText( status.spoiler_text );
showContent( ContentWarning.isShown( account.host, status.id, false ));
boolean cw_shown = ContentWarning.isShown( account.host, status.id, false );
showContent( cw_shown );
}
if( status.media_attachments == null || status.media_attachments.isEmpty() ){
@ -397,32 +456,52 @@ public class ColumnViewHolder implements View.OnClickListener, Column.VisualCall
setMedia( ivMedia4, status, 3 );
// hide sensitive media
boolean is_shown = MediaShown.isShown( account.host,status.id, ! status.sensitive );
boolean is_shown = MediaShown.isShown( account.host, status.id, account.dont_hide_nsfw || ! status.sensitive );
btnShowMedia.setVisibility( ! is_shown ? View.VISIBLE : View.GONE );
}
Drawable d;
int color;
color = ( status.reblogged ? 0xff0088ff : 0xff000000 );
d = ContextCompat.getDrawable( activity, R.drawable.btn_boost ).mutate();
d.setColorFilter( color, PorterDuff.Mode.SRC_ATOP );
btnBoost.setCompoundDrawablesRelativeWithIntrinsicBounds( d, null, null, null );
btnBoost.setText( Long.toString( status.reblogs_count ) );
btnBoost.setTextColor( color );
if( activity.isBusyBoost( account,status )){
color = 0xff000000;
d = ContextCompat.getDrawable( activity, R.drawable.btn_boost ).mutate();
d.setColorFilter( color, PorterDuff.Mode.SRC_ATOP );
btnBoost.setCompoundDrawablesRelativeWithIntrinsicBounds( d, null, null, null );
btnBoost.setText( "?" );
btnBoost.setTextColor( color );
color = ( status.favourited ? 0xff0088ff : 0xff000000 );
d = ContextCompat.getDrawable( activity, R.drawable.btn_favourite ).mutate();
d.setColorFilter( color, PorterDuff.Mode.SRC_ATOP );
btnFavourite.setCompoundDrawablesRelativeWithIntrinsicBounds( d, null, null, null );
btnFavourite.setText( Long.toString( status.favourites_count ) );
btnFavourite.setTextColor( color );
}else{
color = ( status.reblogged ? 0xff0088ff : 0xff000000 );
d = ContextCompat.getDrawable( activity, R.drawable.btn_boost ).mutate();
d.setColorFilter( color, PorterDuff.Mode.SRC_ATOP );
btnBoost.setCompoundDrawablesRelativeWithIntrinsicBounds( d, null, null, null );
btnBoost.setText( Long.toString( status.reblogs_count ) );
btnBoost.setTextColor( color );
}
if( activity.isBusyFav( account,status )){
color = 0xff000000;
d = ContextCompat.getDrawable( activity, R.drawable.btn_refresh ).mutate();
d.setColorFilter( color, PorterDuff.Mode.SRC_ATOP );
btnFavourite.setCompoundDrawablesRelativeWithIntrinsicBounds( d, null, null, null );
btnFavourite.setText( "?" );
btnFavourite.setTextColor( color );
}else{
color = ( status.favourited ? 0xff0088ff : 0xff000000 );
d = ContextCompat.getDrawable( activity, R.drawable.btn_favourite ).mutate();
d.setColorFilter( color, PorterDuff.Mode.SRC_ATOP );
btnFavourite.setCompoundDrawablesRelativeWithIntrinsicBounds( d, null, null, null );
btnFavourite.setText( Long.toString( status.favourites_count ) );
btnFavourite.setTextColor( color );
}
}
private void showContent( boolean shown ){
btnContentWarning.setText( shown ? R.string.hide : R.string.show );
tvContent.setVisibility( shown ? View.VISIBLE : View.GONE );
llContents.setVisibility( shown ? View.VISIBLE : View.GONE );
}
private void setMedia( NetworkImageView iv, TootStatus status, int idx ){
@ -441,11 +520,11 @@ public class ColumnViewHolder implements View.OnClickListener, Column.VisualCall
public void onClick( View v ){
switch( v.getId() ){
case R.id.btnHideMedia:
MediaShown.save( account.host,status.id, false );
MediaShown.save( account.host, status.id, false );
btnShowMedia.setVisibility( View.VISIBLE );
break;
case R.id.btnShowMedia:
MediaShown.save( account.host,status.id, true );
MediaShown.save( account.host, status.id, true );
btnShowMedia.setVisibility( View.GONE );
break;
case R.id.ivMedia1:
@ -460,25 +539,55 @@ public class ColumnViewHolder implements View.OnClickListener, Column.VisualCall
case R.id.ivMedia4:
clickMedia( 3 );
break;
case R.id.btnContentWarning:
{
boolean new_shown = (tvContent.getVisibility()==View.GONE);
ContentWarning.save( account.host,status.id , new_shown);
showContent( new_shown );
break;
}
case R.id.btnContentWarning:{
boolean new_shown = ( llContents.getVisibility() == View.GONE );
ContentWarning.save( account.host, status.id, new_shown );
showContent( new_shown );
break;
}
case R.id.btnReply:
activity.performReply( account,status);
break;
case R.id.btnBoost:
activity.performBoost( account,status.reblog != null ? status.reblog : status ,false);
break;
case R.id.btnFavourite:
activity.performFavourite( account,status.reblog != null ? status.reblog : status);
break;
case R.id.btnMore:
activity.performMore( account,status);
break;
case R.id.ivThumbnail:
case R.id.tvName:
activity.performOpenUser(account,account_thumbnail);
break;
case R.id.llBoosted:
activity.performOpenUser(account,account_boost);
break;
case R.id.llFollow:
activity.performOpenUser(account,account_follow);
break;
}
}
private void clickMedia( int i ){
try{
activity.openChromeTab( status.media_attachments.get( i ).remote_url );
TootAttachment a = status.media_attachments.get( i );
String sv = a.remote_url;
if( TextUtils.isEmpty( sv ) ){
sv = a.url;
}
activity.openChromeTab( sv );
}catch( Throwable ex ){
ex.printStackTrace();
}
}
}
class HeaderViewHolder implements View.OnClickListener {
final View viewRoot;
final NetworkImageView ivBackground;
@ -509,6 +618,7 @@ public class ColumnViewHolder implements View.OnClickListener, Column.VisualCall
btnFollowers.setOnClickListener( this );
btnStatusCount.setOnClickListener( this );
tvNote.setMovementMethod( LinkMovementMethod.getInstance() );
}
public void bind( ActMain activity, SavedAccount access_info, TootAccount who ){

View File

@ -0,0 +1,31 @@
package jp.juggler.subwaytooter;
import android.content.Context;
import jp.juggler.subwaytooter.api.entity.TootStatus;
/**
* Created by tateisu on 2017/04/22.
*/
public class Styler {
public static int getVisibilityIcon( String visibility ){
return
TootStatus.VISIBILITY_PUBLIC.equals( visibility ) ? R.drawable.ic_public
: TootStatus.VISIBILITY_UNLISTED.equals( visibility ) ? R.drawable.ic_lock_open
: TootStatus.VISIBILITY_PRIVATE.equals( visibility ) ? R.drawable.ic_lock
: TootStatus.VISIBILITY_DIRECT.equals( visibility ) ? R.drawable.ic_mail
: R.drawable.ic_public;
}
public static String getVisibilityString( Context context ,String visibility){
return
TootStatus.VISIBILITY_PUBLIC.equals( visibility ) ? context.getString( R.string.visibility_public )
: TootStatus.VISIBILITY_UNLISTED.equals( visibility ) ? context.getString( R.string.visibility_unlisted )
: TootStatus.VISIBILITY_PRIVATE.equals( visibility ) ? context.getString( R.string.visibility_private )
: TootStatus.VISIBILITY_DIRECT.equals( visibility ) ? context.getString( R.string.visibility_direct )
: "?";
}
}

View File

@ -9,23 +9,32 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.util.UUID;
import jp.juggler.subwaytooter.App1;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.CancelChecker;
import jp.juggler.subwaytooter.util.HTTPClient;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.R;
import jp.juggler.subwaytooter.util.Utils;
import jp.juggler.subwaytooter.table.ClientInfo;
import okhttp3.Call;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class TootApiClient {
private static final LogCategory log = new LogCategory( "TootApiClient" );
static final OkHttpClient ok_http_client = App1.ok_http_client;
public interface Callback {
boolean isCancelled();
boolean isApiCancelled();
void publishProgress( String s );
void publishApiProgress( String s );
}
private final Context context;
@ -53,52 +62,65 @@ public class TootApiClient {
this.account = account;
}
public TootApiResult get( String path ){
final HTTPClient client = new HTTPClient( 60000, 10, "account", new CancelChecker() {
@Override
public boolean isCancelled(){
return callback.isCancelled();
}
} );
public static final MediaType MEDIA_TYPE_FORM_URL_ENCODED = MediaType.parse( "application/x-www-form-urlencoded" );
public TootApiResult request( String path ){
return request(path,new Request.Builder() );
}
public TootApiResult request( String path, Request.Builder request_builder ){
JSONObject client_info = null;
JSONObject token_info = ( account == null ? null : account.token_info );
for( ; ; ){
if( callback.isCancelled() ) return null;
if( callback.isApiCancelled() ) return null;
if( token_info == null ){
if( client_info == null ){
// DBにあるならそれを使う
client_info = ClientInfo.load( instance );
if( client_info != null ) continue;
callback.publishProgress( context.getString( R.string.register_app_to_server, instance ) );
callback.publishApiProgress( context.getString( R.string.register_app_to_server, instance ) );
// OAuth2 クライアント登録
String client_name = "jp.juggler.subwaytooter." + UUID.randomUUID().toString();
client.post_content = Utils.encodeUTF8(
"client_name=" + Uri.encode( client_name )
+ "&redirect_uris=urn:ietf:wg:oauth:2.0:oob"
+ "&scopes=read write follow"
);
byte[] data = client.getHTTP( log, "https://" + instance + "/api/v1/apps" );
if( callback.isCancelled() ) return null;
if( data == null ){
return new TootApiResult( context.getString( R.string.network_error, client.last_error ) );
Request request = new Request.Builder()
.url( "https://" + instance + "/api/v1/apps" )
.post( RequestBody.create( MEDIA_TYPE_FORM_URL_ENCODED
, "client_name=" + Uri.encode( client_name )
+ "&redirect_uris=urn:ietf:wg:oauth:2.0:oob"
+ "&scopes=read write follow"
) )
.build();
Call call = ok_http_client.newCall( request );
Response response;
try{
response = call.execute();
}catch( Throwable ex ){
return new TootApiResult( Utils.formatError( ex, context.getResources(), R.string.network_error ) );
}
if( callback.isApiCancelled() ) return null;
if( ! response.isSuccessful() ){
return new TootApiResult( context.getString( R.string.network_error_arg, response ) );
}
try{
String result = Utils.decodeUTF8( data );
String json = response.body().string();
if( TextUtils.isEmpty( json ) || json.startsWith( "<" ) ){
return new TootApiResult( context.getString( R.string.response_not_json ) + "\n" + json );
}
// {"id":999,"redirect_uri":"urn:ietf:wg:oauth:2.0:oob","client_id":"******","client_secret":"******"}
client_info = new JSONObject( result );
client_info = new JSONObject( json );
String error = Utils.optStringX( client_info, "error" );
if( ! TextUtils.isEmpty( error ) ){
return new TootApiResult( context.getString( R.string.api_error, error ) );
}
ClientInfo.save( instance, result );
ClientInfo.save( instance, json );
continue;
}catch( JSONException ex ){
}catch( Throwable ex ){
ex.printStackTrace();
return new TootApiResult( Utils.formatError( ex, "API data error" ) );
}
@ -109,30 +131,46 @@ public class TootApiClient {
return new TootApiResult( context.getString( R.string.login_required ) );
}
callback.publishProgress( context.getString( R.string.request_access_token ) );
callback.publishApiProgress( context.getString( R.string.request_access_token ) );
// アクセストークンの取得
//
client.post_content = Utils.encodeUTF8(
"client_id=" + Uri.encode( Utils.optStringX( client_info, "client_id" ) )
+ "&client_secret=" + Uri.encode( Utils.optStringX( client_info, "client_secret" ) )
+ "&grant_type=password"
+ "&username=" + Uri.encode( user_mail )
+ "&password=" + Uri.encode( password )
);
byte[] data = client.getHTTP( log, "https://" + instance + "/oauth/token" );
if( callback.isCancelled() ) return null;
Request request = new Request.Builder()
.url( "https://" + instance + "/oauth/token" )
.post( RequestBody.create(
MEDIA_TYPE_FORM_URL_ENCODED
,"client_id=" + Uri.encode( Utils.optStringX( client_info, "client_id" ) )
+ "&client_secret=" + Uri.encode( Utils.optStringX( client_info, "client_secret" ) )
+ "&grant_type=password"
+ "&username=" + Uri.encode( user_mail )
+ "&password=" + Uri.encode( password )
+ "&scope=read write follow"
+ "&scopes=read write follow"
))
.build();
Call call = ok_http_client.newCall( request );
Response response;
try{
response = call.execute();
}catch( Throwable ex ){
return new TootApiResult( Utils.formatError( ex, context.getResources(), R.string.network_error ) );
}
if( callback.isApiCancelled() ) return null;
// TODO: アプリIDが無効な場合はどんなエラーが出る
if( data == null ){
return new TootApiResult( context.getString( R.string.network_error, client.last_error ) );
if( ! response.isSuccessful() ){
return new TootApiResult( context.getString( R.string.network_error_arg, response ) );
}
try{
String result = Utils.decodeUTF8( data );
String json = response.body().string();
// {"access_token":"******","token_type":"bearer","scope":"read","created_at":1492334641}
token_info = new JSONObject( result );
if( TextUtils.isEmpty( json ) || json.charAt( 0 ) == '<' ){
return new TootApiResult( context.getString( R.string.login_failed ) );
}
token_info = new JSONObject( json );
String error = Utils.optStringX( client_info, "error" );
if( ! TextUtils.isEmpty( error ) ){
return new TootApiResult( context.getString( R.string.api_error, error ) );
@ -141,51 +179,60 @@ public class TootApiClient {
account.updateTokenInfo( token_info );
}
continue;
}catch( JSONException ex ){
}catch( Throwable ex ){
ex.printStackTrace();
return new TootApiResult( Utils.formatError( ex, "API data error" ) );
}
}
// アクセストークンを使ってAPIを呼び出す
{
callback.publishProgress( context.getString( R.string.request_api, path ) );
client.post_content = null;
client.extra_header = new String[]{
"Authorization", "Bearer " + Utils.optStringX( token_info, "access_token" )
};
byte[] data = client.getHTTP( log, "https://" + instance + path );
if( callback.isCancelled() ) return null;
// TODO: アクセストークンが無効な場合はどうなる
// TODO: アプリIDが無効な場合はどうなる
if( data == null ){
return new TootApiResult( context.getString( R.string.network_error, client.last_error ) );
}
try{
String result = Utils.decodeUTF8( data );
if( result.startsWith( "[" ) ){
JSONArray array = new JSONArray( result );
return new TootApiResult( token_info, result, array );
}else{
JSONObject json = new JSONObject( result );
String error = Utils.optStringX( json, "error" );
if( ! TextUtils.isEmpty( error ) ){
return new TootApiResult( context.getString( R.string.api_error, error ) );
}
return new TootApiResult( token_info, result, json );
// アクセストークンを使ってAPIを呼び出す
{
callback.publishApiProgress( context.getString( R.string.request_api, path ) );
Request request = request_builder
.url("https://" + instance + path)
.header( "Authorization", "Bearer " + Utils.optStringX( token_info, "access_token" ) )
.build();
Call call = ok_http_client.newCall( request );
Response response;
try{
response = call.execute();
}catch( Throwable ex ){
return new TootApiResult( Utils.formatError( ex, context.getResources(), R.string.network_error ) );
}
if( callback.isApiCancelled() ) return null;
// TODO: アクセストークンが無効な場合はどうなる
// TODO: アプリIDが無効な場合はどうなる
if( ! response.isSuccessful() ){
return new TootApiResult( context.getString( R.string.network_error_arg, response ) );
}
try{
String json = response.body().string();
if( TextUtils.isEmpty( json ) || json.startsWith( "<" ) ){
return new TootApiResult( context.getString( R.string.response_not_json ) + "\n" + json );
}else if( json.startsWith( "[" ) ){
JSONArray array = new JSONArray( json );
return new TootApiResult( token_info, json, array );
}else{
JSONObject object = new JSONObject( json );
String error = Utils.optStringX( object, "error" );
if( ! TextUtils.isEmpty( error ) ){
return new TootApiResult( context.getString( R.string.api_error, error ) );
}
return new TootApiResult( token_info, json, object );
}
}catch( Throwable ex ){
ex.printStackTrace();
return new TootApiResult( Utils.formatError( ex, "API data error" ) );
}
}catch( JSONException ex ){
ex.printStackTrace();
return new TootApiResult( Utils.formatError( ex, "API data error" ) );
}
}
}
}
}

View File

@ -1,10 +1,16 @@
package jp.juggler.subwaytooter.api.entity;
import android.text.Spannable;
import android.text.TextUtils;
import jp.juggler.subwaytooter.util.Emojione;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.ArrayList;
import jp.juggler.subwaytooter.util.HTMLDecoder;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
@ -24,7 +30,7 @@ public class TootAccount {
public String acct;
// The account's display name
public String display_name;
public CharSequence display_name;
//Boolean for when the account cannot be followed without waiting for approval first
public boolean locked;
@ -44,7 +50,7 @@ public class TootAccount {
// Biography of user
// 説明文改行は\r\nリンクなどはHTMLタグで書かれている
public String note;
public Spannable note;
//URL of the user's profile page (can be remote)
// https://mastodon.juggler.jp/@tateisu
@ -70,21 +76,30 @@ public class TootAccount {
dst.id = src.optLong( "id" );
dst.username = Utils.optStringX( src, "username" );
dst.acct = Utils.optStringX( src, "acct" );
dst.display_name = Utils.optStringX( src, "display_name" );
String sv = Utils.optStringX( src, "display_name" );
if( TextUtils.isEmpty( sv ) ){
dst.display_name = dst.username;
}else{
dst.display_name = Emojione.decodeEmoji( HTMLDecoder.decodeEntity(sv ) );
}
dst.locked = src.optBoolean( "locked" );
dst.created_at = Utils.optStringX( src, "created_at" );
dst.followers_count = src.optLong( "followers_count" );
dst.following_count = src.optLong( "following_count" );
dst.statuses_count = src.optLong( "statuses_count" );
dst.note = Utils.optStringX( src, "note" );
dst.note = HTMLDecoder.decodeHTML( Utils.optStringX( src, "note" ) );
dst.url = Utils.optStringX( src, "url" );
dst.avatar = Utils.optStringX( src, "avatar" ); // "https:\/\/mastodon.juggler.jp\/system\/accounts\/avatars\/000\/000\/148\/original\/0a468974fac5a448.PNG?1492081886",
dst.avatar_static = Utils.optStringX( src, "avatar_static" ); // "https:\/\/mastodon.juggler.jp\/system\/accounts\/avatars\/000\/000\/148\/original\/0a468974fac5a448.PNG?1492081886",
dst.header = Utils.optStringX( src, "header" ); // "https:\/\/mastodon.juggler.jp\/headers\/original\/missing.png"
dst.header_static = Utils.optStringX( src, "header_static" ); // "https:\/\/mastodon.juggler.jp\/headers\/original\/missing.png"}
dst.time_created_at = TootStatus.parseTime( log,dst.created_at );
dst.time_created_at = TootStatus.parseTime( log, dst.created_at );
return dst;
}catch( Throwable ex ){
ex.printStackTrace();
log.e( ex, "TootAccount.parse failed." );
@ -109,5 +124,4 @@ public class TootAccount {
return result;
}
}

View File

@ -35,10 +35,13 @@ public class TootAttachment {
// Shorter URL for the image, for insertion into text (only present on local images)
public String text_url;
public JSONObject json;
public static TootAttachment parse( LogCategory log, JSONObject src ){
if( src == null ) return null;
try{
TootAttachment dst = new TootAttachment();
dst.json = src;
dst.id = src.optLong( "id" );
dst.type = Utils.optStringX( src, "type" );
dst.url = Utils.optStringX( src, "url" );

View File

@ -19,6 +19,7 @@ import jp.juggler.subwaytooter.util.Utils;
public class TootStatus {
public static class List extends ArrayList< TootStatus > {
}
@ -70,6 +71,10 @@ public class TootStatus {
//One of: public, unlisted, private, direct
public String visibility;
public static final String VISIBILITY_PUBLIC ="public";
public static final String VISIBILITY_UNLISTED ="unlisted";
public static final String VISIBILITY_PRIVATE ="private";
public static final String VISIBILITY_DIRECT ="direct";
// An array of Attachments
public TootAttachment.List media_attachments;
@ -78,7 +83,7 @@ public class TootStatus {
public TootMention.List mentions;
//An array of Tags
public ArrayList<String> tags;
public TootTag.List tags;
//Application from which the status was posted
public String application;
@ -86,6 +91,8 @@ public class TootStatus {
public long time_created_at;
public Spannable decoded_content;
public Spannable decoded_tags;
public Spannable decoded_mentions;
public static TootStatus parse( LogCategory log, JSONObject src ){
@ -112,12 +119,14 @@ public class TootStatus {
status.visibility = Utils.optStringX( src, "visibility" );
status.media_attachments = TootAttachment.parseList( log, src.optJSONArray( "media_attachments" ) );
status.mentions = TootMention.parseList( log, src.optJSONArray( "mentions" ));
status.tags = Utils.parseStringArray( log, src.optJSONArray( "tags" ));
status.tags = TootTag.parseList( log, src.optJSONArray( "tags" ));
status.application = Utils.optStringX( src, "application" ); // null
status.time_created_at = parseTime( log, status.created_at );
status.decoded_content = HTMLDecoder.decodeHTML(status.content);
status.decoded_tags = HTMLDecoder.decodeTags( status.tags);
status.decoded_mentions = HTMLDecoder.decodeMentions( status.mentions);
return status;
}catch( Throwable ex ){
ex.printStackTrace();

View File

@ -0,0 +1,51 @@
package jp.juggler.subwaytooter.api.entity;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.ArrayList;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
public class TootTag {
// The hashtag, not including the preceding #
public String name;
// The URL of the hashtag
public String url;
public static TootTag parse( LogCategory log, JSONObject src ){
if( src == null ) return null;
try{
TootTag dst = new TootTag();
dst.name = Utils.optStringX(src, "name" );
dst.url = Utils.optStringX(src, "url" );
return dst;
}catch( Throwable ex ){
ex.printStackTrace();
log.e(ex,"TootTag.parse failed.");
return null;
}
}
public static class List extends ArrayList<TootTag>{
}
public static TootTag.List parseList( LogCategory log, JSONArray array ){
TootTag.List result = new TootTag.List();
if( array != null ){
for( int i = array.length() - 1 ; i >= 0 ; -- i ){
JSONObject src = array.optJSONObject( i );
if( src == null ) continue;
TootTag item = parse( log, src );
if( item != null ) result.add( 0, item );
}
}
return result;
}
}

View File

@ -34,7 +34,7 @@ public class AccountPicker {
for(int i=0,ie=account_list.size();i<ie;++i){
SavedAccount ai = account_list.get(i);
caption_list[i] = ai.acct + ( ai.login_required ? " "+ activity.getString( R.string.login_required) : "");
caption_list[i] = ai.acct;
}
new AlertDialog.Builder(activity)

View File

@ -15,15 +15,19 @@ import jp.juggler.subwaytooter.api.entity.TootAccount;
import jp.juggler.subwaytooter.util.LogCategory;
public class SavedAccount extends TootAccount{
private static final LogCategory log = new LogCategory( "SavedAccount" );
static final String table = "access_info";
private static final String table = "access_info";
static final String COL_ID = BaseColumns._ID;
static final String COL_HOST = "h";
static final String COL_USER = "u";
static final String COL_ACCOUNT = "a";
static final String COL_TOKEN = "t";
static final String COL_LOGIN_REQUIRED = "lr";
private static final String COL_ID = BaseColumns._ID;
private static final String COL_HOST = "h";
private static final String COL_USER = "u";
private static final String COL_ACCOUNT = "a";
private static final String COL_TOKEN = "t";
private static final String COL_VISIBILITY = "visibility";
private static final String COL_CONFIRM_BOOST = "confirm_boost";
private static final String COL_DONT_HIDE_NSFW = "dont_hide_nsfw";
public static final long INVALID_ID = -1L;
@ -31,8 +35,10 @@ public class SavedAccount extends TootAccount{
public long db_id = INVALID_ID;
public String host;
public String user;
public boolean login_required;
public JSONObject token_info;
public String visibility;
public boolean confirm_boost;
public boolean dont_hide_nsfw;
public static void onDBCreate( SQLiteDatabase db ){
db.execSQL(
@ -42,7 +48,9 @@ public class SavedAccount extends TootAccount{
+ ",h text not null"
+ ",a text not null"
+ ",t text not null"
+ ",lr integer default 0"
+ ",visibility text"
+ ",confirm_boost integer default 1"
+ ",dont_hide_nsfw integer default 0"
+ ")"
);
db.execSQL("create index if not exists " + table + "_user on " + table + "(u)" );
@ -53,21 +61,27 @@ public class SavedAccount extends TootAccount{
}
private static SavedAccount parse( LogCategory log, Cursor cursor ) throws JSONException{
private static SavedAccount parse( Cursor cursor ) throws JSONException{
JSONObject src = new JSONObject( cursor.getString( cursor.getColumnIndex( COL_ACCOUNT ) ) );
SavedAccount dst = (SavedAccount)parse(log,src,new SavedAccount());
if( dst != null){
dst.db_id = cursor.getLong( cursor.getColumnIndex( COL_ID ) );
dst.host = cursor.getString( cursor.getColumnIndex( COL_HOST ) );
dst.user = cursor.getString( cursor.getColumnIndex( COL_USER ) );
dst.login_required = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_LOGIN_REQUIRED ) ) );
int colIdx_visibility = cursor.getColumnIndex( COL_VISIBILITY );
dst.visibility = cursor.isNull( colIdx_visibility )? null : cursor.getString( colIdx_visibility );
dst.confirm_boost = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_CONFIRM_BOOST ) ) );
dst.dont_hide_nsfw = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_DONT_HIDE_NSFW ) ) );
dst.token_info = new JSONObject( cursor.getString( cursor.getColumnIndex( COL_TOKEN ) ) );
}
return dst;
}
public static long insert( LogCategory log,String host, String user, JSONObject account,JSONObject token ){
public static long insert( String host, String user, JSONObject account,JSONObject token ){
try{
ContentValues cv = new ContentValues();
cv.put( COL_HOST, host );
@ -81,6 +95,14 @@ public class SavedAccount extends TootAccount{
return INVALID_ID;
}
public void delete(){
try{
App1.getDB().delete( table, COL_ID + "=?", new String[]{ Long.toString(db_id) } );
}catch( Throwable ex ){
log.e( ex, "saveAccount failed." );
}
}
public void updateTokenInfo( JSONObject token_info ){
if( db_id != INVALID_ID ){
ContentValues cv = new ContentValues();
@ -88,13 +110,22 @@ public class SavedAccount extends TootAccount{
App1.getDB().update( table, cv, COL_ID + "=?", new String[]{ Long.toString(db_id) } );
}
}
public void saveSetting(){
if( db_id != INVALID_ID ){
ContentValues cv = new ContentValues();
cv.put( COL_VISIBILITY, visibility );
cv.put( COL_CONFIRM_BOOST, confirm_boost? 1:0 );
cv.put( COL_DONT_HIDE_NSFW, dont_hide_nsfw ? 1: 0 );
App1.getDB().update( table, cv, COL_ID + "=?", new String[]{ Long.toString(db_id) } );
}
}
public static SavedAccount loadAccount( LogCategory log, long id ){
try{
Cursor cursor = App1.getDB().query( table, null, COL_ID+"=?", new String[]{ Long.toString(id) }, null, null, null );
try{
if( cursor.moveToFirst() ){
return parse( log,cursor );
return parse( cursor );
}
}finally{
cursor.close();
@ -112,7 +143,7 @@ public class SavedAccount extends TootAccount{
Cursor cursor = App1.getDB().query( table, null, null, null, null, null, null );
try{
while( cursor.moveToNext() ){
result.add( parse( log,cursor ) );
result.add( parse( cursor ) );
}
return result;
}finally{

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,113 @@
package jp.juggler.subwaytooter.util;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jp.juggler.subwaytooter.App1;
import uk.co.chrisjenx.calligraphy.CalligraphyTypefaceSpan;
public abstract class Emojione
{
private static final Pattern SHORTNAME_PATTERN = Pattern.compile(":([-+\\w]+):");
private static final HashMap<String,String> map_name2unicode = EmojiMap._shortNameToUnicode;
private static final HashMap<String,String> map_unicode2name = EmojiMap._unicodeToShortName;
static class DecodeEnv{
SpannableStringBuilder sb = new SpannableStringBuilder();
int last_span_start = -1;
int last_span_end = -1;
void closeSpan(){
if( last_span_start >= 0 ){
CalligraphyTypefaceSpan typefaceSpan = new CalligraphyTypefaceSpan( App1.typeface_emoji );
sb.setSpan(typefaceSpan, last_span_start,last_span_end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
last_span_start = -1;
}
}
void addEmoji(String s){
if( last_span_start < 0 ){
last_span_start = sb.length();
}
sb.append(s);
last_span_end = sb.length();
}
void addUnicodeString(String s){
int i = 0;
int end = s.length();
while( i < end ){
int remain = end - i;
if( remain >= 4 ){
String check = s.substring( i, i + 4 );
if( map_unicode2name.containsKey( check ) ){
addEmoji( check );
i += 4;
continue;
}
}
if( remain >= 3 ){
String check = s.substring( i, i + 3 );
if( map_unicode2name.containsKey( check ) ){
addEmoji( check );
i += 3;
continue;
}
}
if( remain >= 2 ){
String check = s.substring( i, i + 2 );
if( map_unicode2name.containsKey( check ) ){
addEmoji( check );
i += 2;
continue;
}
}
if( remain >= 1 ){
String check = s.substring( i, i + 1 );
if( map_unicode2name.containsKey( check ) ){
addEmoji( check );
i += 1;
continue;
}
}
closeSpan();
sb.append( s.charAt( i ) );
++ i;
}
}
}
public static CharSequence decodeEmoji( String s ){
DecodeEnv decode_env = new DecodeEnv();
Matcher matcher = SHORTNAME_PATTERN.matcher(s);
int last_end = 0;
while( matcher.find() ){
int start = matcher.start();
int end = matcher.end();
if( start > last_end ){
decode_env.addUnicodeString(s.substring( last_end,start ));
}
last_end = end;
//
String unicode = map_name2unicode.get(matcher.group(1));
if( unicode == null ){
decode_env.addUnicodeString(s.substring( start, end ));
}else{
decode_env.addEmoji( unicode );
}
}
// close span
decode_env.closeSpan();
// copy remain
int end = s.length();
if( end > last_end ){
decode_env.addUnicodeString(s.substring( last_end, end ));
}
return decode_env.sb;
}
}

View File

@ -1,19 +1,19 @@
package jp.juggler.subwaytooter.util;
import android.text.Html;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ClickableSpan;
import android.view.View;
import com.emojione.Emojione;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jp.juggler.subwaytooter.api.entity.TootMention;
import jp.juggler.subwaytooter.api.entity.TootTag;
public class HTMLDecoder {
static final LogCategory log = new LogCategory( "HTMLDecoder" );
@ -27,7 +27,9 @@ public class HTMLDecoder {
static final Pattern reTag = Pattern.compile( "<(/?)(\\w+)" );
static final Pattern reTagEnd = Pattern.compile( "(/?)>$" );
static final Pattern reHref = Pattern.compile( "\\bhref=\"([^\"]*)\"" );
static class TokenParser {
final String src;
@ -133,15 +135,13 @@ public class HTMLDecoder {
if( DEBUG_HTML_PARSER ) log.d( "parseChild: %s)%s", indent, tag );
}
String decodeEmoji( String s ){
return Emojione.shortnameToUnicode( s, false );
}
public void encodeSpan( SpannableStringBuilder sb ){
if( TAG_TEXT.equals( tag ) ){
sb.append( decodeEntity( decodeEmoji( text ) ) );
sb.append( Emojione.decodeEmoji( decodeEntity( text ) ) );
return;
}
if( DEBUG_HTML_PARSER ) sb.append( "(start " + tag + ")" );
@ -203,6 +203,49 @@ public class HTMLDecoder {
return null;
}
public static Spannable decodeTags( TootTag.List src_list ){
if( src_list == null || src_list.isEmpty()) return null;
SpannableStringBuilder sb = new SpannableStringBuilder();
for(TootTag item : src_list){
if(sb.length() > 0) sb.append(" ");
int start = sb.length();
sb.append('#');
sb.append(item.name);
final String item_url = item.url;
sb.setSpan( new ClickableSpan() {
@Override public void onClick( View widget ){
if( link_callback != null ){
link_callback.onClickLink( item_url );
}
}
}, start, sb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE );
}
return sb;
}
public static Spannable decodeMentions( TootMention.List src_list ){
if( src_list == null || src_list.isEmpty()) return null;
SpannableStringBuilder sb = new SpannableStringBuilder();
for(TootMention item : src_list){
if(sb.length() > 0) sb.append(" ");
int start = sb.length();
sb.append('@');
sb.append( item.acct );
final String item_url = item.url;
sb.setSpan( new ClickableSpan() {
@Override public void onClick( View widget ){
if( link_callback != null ){
link_callback.onClickLink( item_url );
}
}
}, start, sb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE );
}
return sb;
}
//////////////////////////////////////////////////////////////////////////////////////
private static final HashMap< String, Character > entity_map = new HashMap<>();
private static void _addEntity( String s, char c ){
@ -215,7 +258,7 @@ public class HTMLDecoder {
static final Pattern reEntity = Pattern.compile( "&(#?)(\\w+);" );
static CharSequence decodeEntity( String src ){
public static String decodeEntity( String src ){
StringBuilder sb = null;
Matcher m = reEntity.matcher( src );
int last_end = 0;
@ -262,7 +305,7 @@ public class HTMLDecoder {
if( end > last_end ){
sb.append( src.substring( last_end, end ) );
}
return sb;
return sb.toString();
}
private static void init1(){

View File

@ -1,692 +0,0 @@
package jp.juggler.subwaytooter.util;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.SSLHandshakeException;
import android.os.SystemClock;
//! リトライつきHTTPクライアント
public class HTTPClient {
static final boolean debug_http = false;
public String[] extra_header;
public int rcode;
public boolean allow_error = false;
public Map< String, List< String > > response_header;
public HashMap< String, String > cookie_pot;
public int max_try;
@SuppressWarnings("unused")
public int timeout_dns = 1000 * 3;
public int timeout_connect;
public int timeout_read;
public String caption;
public boolean silent_error = false;
public long time_expect_connect = 3000;
public boolean bDisableKeepAlive = false;
@SuppressWarnings("unused")
public HTTPClient( int timeout, int max_try, String caption, CancelChecker cancel_checker ){
this.cancel_checker = cancel_checker;
this.timeout_connect = this.timeout_read = timeout;
this.max_try = max_try;
this.caption = caption;
}
@SuppressWarnings("unused")
public HTTPClient( int timeout, int max_try, String caption, final AtomicBoolean _cancel_checker ){
this.cancel_checker = new CancelChecker() {
@Override
public boolean isCancelled(){
return _cancel_checker.get();
}
};
this.timeout_connect = this.timeout_read = timeout;
this.max_try = max_try;
this.caption = caption;
}
@SuppressWarnings("unused")
public void setCookiePot( boolean enabled ){
if( enabled == ( cookie_pot != null ) ) return;
cookie_pot = ( enabled ? new HashMap< String, String >() : null );
}
///////////////////////////////
// デフォルトの入力ストリームハンドラ
HTTPClientReceiver default_receiver = new HTTPClientReceiver() {
byte[] buf = new byte[ 2048 ];
ByteArrayOutputStream bao = new ByteArrayOutputStream( 0 );
public byte[] onHTTPClientStream( LogCategory log, CancelChecker cancel_checker, InputStream in, int content_length ){
try{
bao.reset();
for( ; ; ){
if( cancel_checker.isCancelled() ){
if( debug_http ) log.w(
"[%s,read]cancelled!"
, caption
);
return null;
}
int delta = in.read( buf );
if( delta <= 0 ) break;
bao.write( buf, 0, delta );
}
return bao.toByteArray();
}catch( Throwable ex ){
log.e(
"[%s,read] %s:%s"
, caption
, ex.getClass().getSimpleName()
, ex.getMessage()
);
}
return null;
}
};
///////////////////////////////
// 別スレッドからのキャンセル処理
public CancelChecker cancel_checker;
volatile Thread io_thread;
@SuppressWarnings("unused")
public boolean isCancelled(){
return cancel_checker.isCancelled();
}
@SuppressWarnings("unused")
public synchronized void cancel( LogCategory log ){
Thread t = io_thread;
if( t == null ) return;
log.i(
"[%s,cancel] %s"
, caption
, t
);
try{
t.interrupt();
}catch( Throwable ex ){
ex.printStackTrace();
}
}
public byte[] post_content = null;
public String post_content_type = null;
public boolean quit_network_error = false;
public String last_error = null;
public long mtime;
public static String user_agent = null;
///////////////////////////////
// HTTPリクエスト処理
@SuppressWarnings("unused")
public byte[] getHTTP( LogCategory log, String url ){
return getHTTP( log, url, default_receiver );
}
@SuppressWarnings("ConstantConditions")
public byte[] getHTTP( LogCategory log, String url, HTTPClientReceiver receiver ){
// // http://android-developers.blogspot.jp/2011/09/androids-http-clients.html
// // HTTP connection reuse which was buggy pre-froyo
// if( Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO ){
// System.setProperty( "http.keepAlive", "false" );
// }
try{
synchronized( this ){
this.io_thread = Thread.currentThread();
}
URL urlObject;
try{
urlObject = new URL( url );
}catch( MalformedURLException ex ){
log.d( "[%s,init] bad url %s %s", caption, url, ex.getMessage() );
return null;
}
/*
// desire だとどうもリソースリークしているようなので行わないことにした
// DNSを引けるか確認する
if(debug_http) Log.d(logcat,"check hostname "+url);
if( !checkDNSResolver(urlObject) ){
Log.w(logcat,"broken name resolver");
return null;
}
*/
long timeStart = SystemClock.elapsedRealtime();
for( int nTry = 0 ; nTry < max_try ; ++ nTry ){
long t1, t2, lap;
try{
this.rcode = 0;
// キャンセルされたか確認
if( cancel_checker.isCancelled() ) return null;
// http connection
HttpURLConnection conn = (HttpURLConnection) urlObject.openConnection();
if( user_agent != null ) conn.setRequestProperty( "User-Agent", user_agent );
// 追加ヘッダがあれば記録する
if( extra_header != null ){
for( int i = 0 ; i < extra_header.length ; i += 2 ){
conn.addRequestProperty( extra_header[ i ], extra_header[ i + 1 ] );
if( debug_http )
log.d( "%s: %s", extra_header[ i ], extra_header[ i + 1 ] );
}
}
if( bDisableKeepAlive ){
conn.setRequestProperty( "Connection", "close" );
}
// クッキーがあれば指定する
if( cookie_pot != null ){
StringBuilder sb = new StringBuilder();
for( Map.Entry< String, String > pair : cookie_pot.entrySet() ){
if( sb.length() > 0 ) sb.append( "; " );
sb.append( pair.getKey() );
sb.append( '=' );
sb.append( pair.getValue() );
}
conn.addRequestProperty( "Cookie", sb.toString() );
}
// リクエストを送ってレスポンスの頭を読む
try{
t1 = SystemClock.elapsedRealtime();
if( debug_http )
log.d( "[%s,connect] start %s", caption, toHostName( url ) );
conn.setDoInput( true );
conn.setConnectTimeout( this.timeout_connect );
conn.setReadTimeout( this.timeout_read );
if( post_content == null ){
conn.setDoOutput( false );
conn.connect();
}else{
conn.setDoOutput( true );
// if( Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB ){
// conn.setRequestProperty( "Content-Length", Integer.toString( post_content.length ) );
// }
if( post_content_type != null ){
conn.setRequestProperty( "Content-Type", post_content_type );
}
OutputStream out = conn.getOutputStream();
out.write( post_content );
out.flush();
out.close();
}
// http://stackoverflow.com/questions/12931791/java-io-ioexception-received-authentication-challenge-is-null-in-ics-4-0-3
int rcode;
try{
// Will throw IOException if server responds with 401.
rcode = this.rcode = conn.getResponseCode();
}catch( IOException ex ){
String sv = ex.getMessage();
if( sv != null && sv.contains( "authentication challenge" ) ){
log.d( "retry getResponseCode!" );
// Will return 401, because now connection has the correct internal state.
rcode = this.rcode = conn.getResponseCode();
}else{
throw ex;
}
}
mtime = conn.getLastModified();
t2 = SystemClock.elapsedRealtime();
lap = t2 - t1;
if( lap > time_expect_connect )
log.d( "[%s,connect] time=%sms %s", caption, lap, toHostName( url ) );
// ヘッダを覚えておく
response_header = conn.getHeaderFields();
// クッキーが来ていたら覚える
if( cookie_pot != null ){
String v = conn.getHeaderField( "set-cookie" );
if( v != null ){
int pos = v.indexOf( '=' );
cookie_pot.put( v.substring( 0, pos ), v.substring( pos + 1 ) );
}
}
if( rcode >= 500 ){
if( ! silent_error )
log.e( "[%s,connect] temporary error %d", caption, rcode );
last_error = String.format( "(HTTP error %d)", rcode );
continue;
}else if( ! allow_error && rcode >= 300 ){
if( ! silent_error )
log.e( "[%s,connect] permanent error %d", caption, rcode );
last_error = String.format( "(HTTP error %d)", rcode );
return null;
}
}catch( UnknownHostException ex ){
rcode = 0;
last_error = ex.getClass().getSimpleName();
// このエラーはリトライしてもムリ
conn.disconnect();
return null;
}catch( SSLHandshakeException ex ){
last_error = String.format( "SSL handshake error. Please check device's date and time. (%s %s)", ex.getClass().getSimpleName(), ex.getMessage() );
if( ! silent_error ){
log.e( "[%s,connect] %s"
, caption
, last_error
);
if( ex.getMessage() == null ){
ex.printStackTrace();
}
}
this.rcode = - 1;
return null;
}catch( Throwable ex ){
last_error = String.format( "%s %s", ex.getClass().getSimpleName(), ex.getMessage() );
if( ! silent_error ){
log.e( "[%s,connect] %s"
, caption
, last_error
);
if( ex.getMessage() == null ){
ex.printStackTrace();
}
}
// 時計が合ってない場合は Received authentication challenge is null なエラーが出るらしい
// getting a 401 Unauthorized error, due to a malformed Authorization header.
if( ex instanceof IOException
&& ex.getMessage() != null
&& ex.getMessage().contains( "authentication challenge" )
){
ex.printStackTrace();
log.d( "Please check device's date and time." );
this.rcode = 401;
return null;
}else if( ex instanceof ConnectException
&& ex.getMessage() != null
&& ex.getMessage().contains( "ENETUNREACH" )
){
// このアプリの場合は network unreachable はリトライしない
return null;
}
if( quit_network_error ) return null;
// 他のエラーはリトライしてみようキャンセルされたなら次のループの頭で抜けるはず
conn.disconnect();
continue;
}
InputStream in = null;
try{
if( debug_http ) if( rcode != 200 )
log.d( "[%s,read] start status=%d", caption, this.rcode );
try{
in = conn.getInputStream();
}catch( FileNotFoundException ex ){
in = conn.getErrorStream();
}
if( in == null ){
log.d( "[%s,read] missing input stream. rcode=%d", caption, rcode );
return null;
}
int content_length = conn.getContentLength();
byte[] data = receiver.onHTTPClientStream( log, cancel_checker, in, content_length );
if( data == null ) continue;
if( data.length > 0 ){
if( nTry > 0 ) log.w( "[%s] OK. retry=%d,time=%dms"
, caption
, nTry
, SystemClock.elapsedRealtime() - timeStart
);
return data;
}
if( ! cancel_checker.isCancelled()
&& ! silent_error
){
log.w(
"[%s,read] empty data."
, caption
);
}
}finally{
try{
if( in != null ) in.close();
}catch( Throwable ignored ){
}
conn.disconnect();
}
}catch( Throwable ex ){
last_error = String.format( "%s %s", ex.getClass().getSimpleName(), ex.getMessage() );
ex.printStackTrace();
}
}
if( ! silent_error ) log.e( "[%s] fail. try=%d. rcode=%d", caption, max_try, rcode );
}catch( Throwable ex ){
ex.printStackTrace();
last_error = String.format( "%s %s", ex.getClass().getSimpleName(), ex.getMessage() );
}finally{
synchronized( this ){
io_thread = null;
}
}
return null;
}
//! HTTPレスポンスのヘッダを読む
@SuppressWarnings("unused")
public void dump_res_header( LogCategory log ){
log.d( "HTTP code %d", rcode );
if( response_header != null ){
for( Map.Entry< String, List< String > > entry : response_header.entrySet() ){
String k = entry.getKey();
for( String v : entry.getValue() ){
log.d( "%s: %s", k, v );
}
}
}
}
@SuppressWarnings({ "unused", "ConstantConditions" })
public String get_cache( LogCategory log, File file, String url ){
String last_error = null;
for( int nTry = 0 ; nTry < 10 ; ++ nTry ){
if( cancel_checker.isCancelled() ) return "cancelled";
long now = System.currentTimeMillis();
try{
HttpURLConnection conn = (HttpURLConnection) new URL( url ).openConnection();
try{
conn.setConnectTimeout( 1000 * 10 );
conn.setReadTimeout( 1000 * 10 );
if( file.exists() ) conn.setIfModifiedSince( file.lastModified() );
conn.connect();
this.rcode = conn.getResponseCode();
if( rcode == 304 ){
if( file.exists() ){
//noinspection ResultOfMethodCallIgnored
file.setLastModified( now );
}
return null;
}
if( rcode == 200 ){
InputStream in = conn.getInputStream();
try{
ByteArrayOutputStream bao = new ByteArrayOutputStream();
try{
byte[] tmp = new byte[ 4096 ];
for( ; ; ){
if( cancel_checker.isCancelled() ) return "cancelled";
int delta = in.read( tmp, 0, tmp.length );
if( delta <= 0 ) break;
bao.write( tmp, 0, delta );
}
byte[] data = bao.toByteArray();
if( data != null ){
FileOutputStream out = new FileOutputStream( file );
try{
out.write( data );
return null;
}finally{
try{
out.close();
}catch( Throwable ignored ){
}
}
}
}finally{
try{
bao.close();
}catch( Throwable ignored ){
}
}
}catch( Throwable ex ){
ex.printStackTrace();
if( file.exists() ){
//noinspection ResultOfMethodCallIgnored
file.delete();
}
last_error = String.format( "%s %s", ex.getClass().getSimpleName(), ex.getMessage() );
}finally{
try{
in.close();
}catch( Throwable ignored ){
}
}
break;
}
log.e( "http error: %d %s", rcode, url );
if( rcode >= 400 && rcode < 500 ){
last_error = String.format( "HTTP error %d", rcode );
break;
}
}finally{
conn.disconnect();
}
// retry ?
}catch( MalformedURLException ex ){
ex.printStackTrace();
last_error = String.format( "bad URL:%s", ex.getMessage() );
break;
}catch( IOException ex ){
ex.printStackTrace();
last_error = String.format( "%s %s", ex.getClass().getSimpleName(), ex.getMessage() );
}
}
return last_error;
}
/////////////////////////////////////////////////////////
// 複数URLに対応したリクエスト処理
public boolean no_cache = false;
@SuppressWarnings({ "unused", "ConstantConditions" })
public File getFile( LogCategory log, File cache_dir, String[] url_list, File _file ){
//
if( url_list == null || url_list.length < 1 ){
setError( 0, "missing url argument." );
return null;
}
// make cache_dir
if( cache_dir != null ){
if( ! cache_dir.mkdirs() && ! cache_dir.isDirectory() ){
setError( 0, "can not create cache_dir" );
return null;
}
}
for( int nTry = 0 ; nTry < 10 ; ++ nTry ){
if( cancel_checker.isCancelled() ){
setError( 0, "cancelled." );
return null;
}
//
String url = url_list[ nTry % url_list.length ];
File file = ( _file != null ? _file : new File( cache_dir, Utils.url2name( url ) ) );
//
//noinspection TryWithIdenticalCatches
try{
HttpURLConnection conn = (HttpURLConnection) new URL( url ).openConnection();
if( user_agent != null ) conn.setRequestProperty( "User-Agent", user_agent );
try{
conn.setConnectTimeout( 1000 * 10 );
conn.setReadTimeout( 1000 * 10 );
if( ! no_cache && file.exists() )
conn.setIfModifiedSince( file.lastModified() );
conn.connect();
this.rcode = conn.getResponseCode();
if( debug_http ) if( rcode != 200 ) log.d( "getFile %s %s", rcode, url );
// 変更なしの場合
if( rcode == 304 ){
/// log.d("304: %s",file);
return file;
}
// 変更があった場合
if( rcode == 200 ){
// メッセージボディをファイルに保存する
InputStream in = null;
FileOutputStream out = null;
try{
byte[] tmp = new byte[ 4096 ];
in = conn.getInputStream();
out = new FileOutputStream( file );
for( ; ; ){
if( cancel_checker.isCancelled() ){
setError( 0, "cancelled" );
if( file.exists() ){
//noinspection ResultOfMethodCallIgnored
file.delete();
}
return null;
}
int delta = in.read( tmp, 0, tmp.length );
if( delta <= 0 ) break;
out.write( tmp, 0, delta );
}
out.close();
out = null;
//
long mtime = conn.getLastModified();
if( mtime >= 1000 ){
//noinspection ResultOfMethodCallIgnored
file.setLastModified( mtime );
}
//
/// log.d("200: %s",file);
return file;
}catch( Throwable ex ){
setError( ex );
}finally{
try{
if( in != null ) in.close();
}catch( Throwable ignored ){
}
try{
if( out != null ) out.close();
}catch( Throwable ignored ){
}
}
// エラーがあったらリトライ
if( file.exists() ){
//noinspection ResultOfMethodCallIgnored
file.delete();
}
continue;
}
// その他よく分からないケース
log.e( "http error: %d %s", rcode, url );
// URLが複数提供されている場合404エラーはリトライ対象
if( rcode == 404 && url_list.length > 1 ){
last_error = String.format( "(HTTP error %d)", rcode );
continue;
}
// それ以外の永続エラーはリトライしない
if( rcode >= 400 && rcode < 500 ){
last_error = String.format( "(HTTP error %d)", rcode );
break;
}
}finally{
conn.disconnect();
}
// retry ?
}catch( UnknownHostException ex ){
rcode = 0;
last_error = ex.getClass().getSimpleName();
// このエラーはリトライしてもムリ
break;
}catch( MalformedURLException ex ){
setError( ex );
break;
}catch( SocketTimeoutException ex ){
setError_silent( log, ex );
}catch( ConnectException ex ){
setError_silent( log, ex );
}catch( IOException ex ){
setError( ex );
}
}
return null;
}
///////////////////////////////////////////////////////////////////
public boolean setError( int i, String string ){
rcode = i;
last_error = string;
return false;
}
public boolean setError( Throwable ex ){
ex.printStackTrace();
rcode = 0;
last_error = String.format( "%s %s", ex.getClass().getSimpleName(), ex.getMessage() );
return false;
}
public boolean setError_silent( LogCategory log, Throwable ex ){
log.d( "ERROR: %s %s", ex.getClass().getName(), ex.getMessage() );
rcode = 0;
last_error = String.format( "%s %s", ex.getClass().getSimpleName(), ex.getMessage() );
return false;
}
//! HTTPレスポンスのヘッダを読む
public String getHeaderString( String key, String defval ){
List< String > list = response_header.get( key );
if( list != null && list.size() > 0 ){
String v = list.get( 0 );
if( v != null ) return v;
}
return defval;
}
//! HTTPレスポンスのヘッダを読む
@SuppressWarnings("unused")
public int getHeaderInt( String key, int defval ){
String v = getHeaderString( key, null );
try{
return Integer.parseInt( v, 10 );
}catch( Throwable ex ){
return defval;
}
}
static Pattern reHostName = Pattern.compile( "//([^/]+)/" );
static String toHostName( String url ){
Matcher m = reHostName.matcher( url );
if( m.find() ) return m.group( 1 );
return url;
}
}

View File

@ -1,9 +0,0 @@
package jp.juggler.subwaytooter.util;
import java.io.InputStream;
//! HTTPClientのバッファ管理を独自に行いたい場合に使用する.
//! このインタフェースを実装したものをHTTPClient.getHTTP()の第二引数に指定する
public interface HTTPClientReceiver {
byte[] onHTTPClientStream( LogCategory log,CancelChecker cancel_checker, InputStream in, int content_length);
}

View File

@ -23,6 +23,7 @@ import android.os.Looper;
import android.os.storage.StorageManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.util.Base64;
import android.util.SparseBooleanArray;
@ -45,6 +46,8 @@ import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import jp.juggler.subwaytooter.ActMain;
public class Utils {
@SuppressLint("DefaultLocale")
@ -531,7 +534,17 @@ public class Utils {
return MIME_TYPE_APPLICATION_OCTET_STREAM;
}
public static CharSequence formatSpannable1( Context context, int string_id, CharSequence display_name ){
String s = context.getString( string_id );
int end = s.length();
int pos = s.indexOf( "%1$s" );
if( pos == -1 ) return s;
SpannableStringBuilder sb = new SpannableStringBuilder( );
if( pos > 0 ) sb.append(s.substring( 0,pos ));
sb.append( display_name);
if( pos +4 < end ) sb.append(s.substring( pos+4,end ));
return sb;
}
static class FileInfo {

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 997 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#FF808080">
<item android:id="@android:id/mask" android:drawable="@android:color/white" />
</ripple>

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 B

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#FF808080" />
</shape>
</item>
<item android:state_selected="true">
<shape android:shape="rectangle">
<solid android:color="#00000080" />
</shape>
</item>
<item android:state_focused="true">
<shape android:shape="rectangle">
<solid android:color="#00000080" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="#00000000" />
</shape>
</item>
</selector>

View File

@ -0,0 +1,400 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fillViewport="true"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingEnd="@dimen/activity_horizontal_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingStart="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:scrollbarStyle="outsideOverlay"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/instance"
/>
<LinearLayout style="@style/setting_row_form">
<TextView
android:id="@+id/tvInstance"
style="@style/setting_horizontal_stretch"
android:ellipsize="start"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/user"
/>
<LinearLayout style="@style/setting_row_form">
<TextView
android:id="@+id/tvUser"
style="@style/setting_horizontal_stretch"
android:ellipsize="start"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/actions"
/>
<LinearLayout style="@style/setting_row_form">
<Button
android:id="@+id/btnAccessToken"
style="@style/setting_horizontal_stretch"
android:ellipsize="start"
android:text="@string/get_access_token"
/>
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<Button
android:id="@+id/btnAccountRemove"
style="@style/setting_horizontal_stretch"
android:ellipsize="start"
android:text="@string/account_remove"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/default_status_visibility"
/>
<LinearLayout style="@style/setting_row_form">
<Button
android:id="@+id/btnVisibility"
style="@style/setting_horizontal_stretch"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/confirm_before_boost"
/>
<LinearLayout style="@style/setting_row_form">
<Switch
android:id="@+id/swConfirmBeforeBoost"
style="@style/setting_horizontal_stretch"
android:gravity="center"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/sensitive_content_default_open"
/>
<LinearLayout style="@style/setting_row_form">
<Switch
android:id="@+id/swNSFWOpen"
style="@style/setting_horizontal_stretch"
android:gravity="center"
/>
</LinearLayout>
<!--<View style="@style/setting_divider"/>-->
<!--<TextView-->
<!--style="@style/setting_row_label"-->
<!--android:labelFor="@+id/etTargetUrl"-->
<!--android:text="@string/target_url"-->
<!--/>-->
<!--<LinearLayout style="@style/setting_row_form">-->
<!--<EditText-->
<!--android:id="@+id/etTargetUrl"-->
<!--style="@style/setting_edit_text"-->
<!--android:inputType="textUri"-->
<!--/>-->
<!--<Button-->
<!--android:id="@+id/btnHelpTargetUrl"-->
<!--style="@style/setting_row_help"-->
<!--/>-->
<!--</LinearLayout>-->
<!--<View style="@style/setting_divider"/>-->
<!--<TextView-->
<!--style="@style/setting_row_label"-->
<!--android:text="@string/local_folder"-->
<!--/>-->
<!--<LinearLayout style="@style/setting_row_form">-->
<!--<TextView-->
<!--android:id="@+id/tvFolder"-->
<!--style="@style/setting_horizontal_stretch"-->
<!--android:ellipsize="start"-->
<!--/>-->
<!--<Button-->
<!--android:id="@+id/btnFolderPicker"-->
<!--android:layout_width="wrap_content"-->
<!--android:layout_height="wrap_content"-->
<!--android:minWidth="32dp"-->
<!--android:text="@string/dot_dot"-->
<!--/>-->
<!--<Button-->
<!--android:id="@+id/btnHelpFolderPicker"-->
<!--style="@style/setting_row_help"-->
<!--/>-->
<!--</LinearLayout>-->
<!--<View style="@style/setting_divider"/>-->
<!--<TextView-->
<!--style="@style/setting_row_label"-->
<!--android:labelFor="@+id/etFileType"-->
<!--android:text="@string/file_type"-->
<!--/>-->
<!--<LinearLayout style="@style/setting_row_form">-->
<!--<EditText-->
<!--android:id="@+id/etFileType"-->
<!--style="@style/setting_edit_text"-->
<!--android:inputType="text"-->
<!--/>-->
<!--<Button-->
<!--android:id="@+id/btnFileTypeHelp"-->
<!--style="@style/setting_row_help"-->
<!--/>-->
<!--</LinearLayout>-->
<!--<View style="@style/setting_divider"/>-->
<!--<TextView-->
<!--style="@style/setting_row_label"-->
<!--android:labelFor="@+id/etRepeatInterval"-->
<!--android:text="@string/repeat_interval"-->
<!--/>-->
<!--<LinearLayout style="@style/setting_row_form">-->
<!--<EditText-->
<!--android:id="@+id/etRepeatInterval"-->
<!--style="@style/setting_edit_text"-->
<!--android:inputType="number"-->
<!--/>-->
<!--<Button-->
<!--android:id="@+id/btnIntervalHelp"-->
<!--style="@style/setting_row_help"-->
<!--/>-->
<!--</LinearLayout>-->
<!--<View style="@style/setting_divider"/>-->
<!--<TextView-->
<!--style="@style/setting_row_label"-->
<!--android:text="@string/geo_tagging_mode"-->
<!--/>-->
<!--<LinearLayout style="@style/setting_row_form">-->
<!--<Spinner-->
<!--android:id="@+id/spLocationMode"-->
<!--style="@style/setting_horizontal_stretch"-->
<!--/>-->
<!--<Button-->
<!--android:id="@+id/btnLocationModeHelp"-->
<!--style="@style/setting_row_help"-->
<!--/>-->
<!--</LinearLayout>-->
<!--<View style="@style/setting_divider"/>-->
<!--<TextView-->
<!--style="@style/setting_row_label"-->
<!--android:labelFor="@+id/etLocationIntervalDesired"-->
<!--android:text="@string/location_interval_desired"-->
<!--/>-->
<!--<LinearLayout style="@style/setting_row_form">-->
<!--<EditText-->
<!--android:id="@+id/etLocationIntervalDesired"-->
<!--style="@style/setting_edit_text"-->
<!--android:inputType="number"-->
<!--/>-->
<!--<Button-->
<!--android:id="@+id/btnLocationIntervalDesiredHelp"-->
<!--style="@style/setting_row_help"-->
<!--/>-->
<!--</LinearLayout>-->
<!--<View style="@style/setting_divider"/>-->
<!--<TextView-->
<!--style="@style/setting_row_label"-->
<!--android:labelFor="@+id/etLocationIntervalMin"-->
<!--android:text="@string/location_interval_min"-->
<!--/>-->
<!--<LinearLayout style="@style/setting_row_form">-->
<!--<EditText-->
<!--android:id="@+id/etLocationIntervalMin"-->
<!--style="@style/setting_edit_text"-->
<!--android:inputType="number"-->
<!--/>-->
<!--<Button-->
<!--android:id="@+id/btnLocationIntervalMinHelp"-->
<!--style="@style/setting_row_help"-->
<!--/>-->
<!--</LinearLayout>-->
<!--<View style="@style/setting_divider"/>-->
<!--<TextView-->
<!--style="@style/setting_row_label"-->
<!--android:text="@string/force_wifi_ap"-->
<!--/>-->
<!--<LinearLayout style="@style/setting_row_form">-->
<!--<Switch-->
<!--android:id="@+id/swForceWifi"-->
<!--style="@style/setting_horizontal_stretch"-->
<!--android:gravity="center"-->
<!--/>-->
<!--<Button-->
<!--android:id="@+id/btnForceWifiHelp"-->
<!--style="@style/setting_row_help"-->
<!--/>-->
<!--</LinearLayout>-->
<!--<View style="@style/setting_divider"/>-->
<!--<TextView-->
<!--style="@style/setting_row_label"-->
<!--android:labelFor="@+id/etSSID"-->
<!--android:text="@string/wifi_ap_ssid"-->
<!--/>-->
<!--<LinearLayout style="@style/setting_row_form">-->
<!--<EditText-->
<!--android:id="@+id/etSSID"-->
<!--style="@style/setting_edit_text"-->
<!--android:inputType="text"-->
<!--/>-->
<!--<Button-->
<!--android:id="@+id/btnSSIDPicker"-->
<!--android:layout_width="wrap_content"-->
<!--android:layout_height="wrap_content"-->
<!--android:minWidth="32dp"-->
<!--android:text="@string/dot_dot"-->
<!--/>-->
<!--<Button-->
<!--android:id="@+id/btnSSIDHelp"-->
<!--style="@style/setting_row_help"-->
<!--/>-->
<!--</LinearLayout>-->
<!--<View style="@style/setting_divider"/>-->
<!--<TextView-->
<!--style="@style/setting_row_label"-->
<!--android:text="@string/thumbnail_auto_rotate"-->
<!--/>-->
<!--<LinearLayout style="@style/setting_row_form">-->
<!--<Switch-->
<!--android:id="@+id/swThumbnailAutoRotate"-->
<!--style="@style/setting_horizontal_stretch"-->
<!--android:gravity="center"-->
<!--/>-->
<!--<Button-->
<!--android:id="@+id/btnThumbnailAutoRotateHelp"-->
<!--style="@style/setting_row_help"-->
<!--/>-->
<!--</LinearLayout>-->
<!--<View style="@style/setting_divider"/>-->
<!--<TextView-->
<!--style="@style/setting_row_label"-->
<!--android:text="@string/copy_before_view_send"-->
<!--/>-->
<!--<LinearLayout style="@style/setting_row_form">-->
<!--<Switch-->
<!--android:id="@+id/swCopyBeforeViewSend"-->
<!--style="@style/setting_horizontal_stretch"-->
<!--android:gravity="center"-->
<!--/>-->
<!--<Button-->
<!--android:id="@+id/btnCopyBeforeViewSendHelp"-->
<!--style="@style/setting_row_help"-->
<!--/>-->
<!--</LinearLayout>-->
<!--<View style="@style/setting_divider"/>-->
<!--<TextView-->
<!--android:layout_width="match_parent"-->
<!--android:layout_height="wrap_content"-->
<!--android:layout_marginBottom="20dp"-->
<!--android:layout_marginTop="20dp"-->
<!--android:gravity="center"-->
<!--android:text="@string/setting_desc"-->
<!--/>-->
</LinearLayout>
</ScrollView>

View File

@ -1,118 +1,173 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="12dp"
android:fillViewport="true"
>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="12dp"
>
<Button
android:id="@+id/btnAccount"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:textAllCaps="false"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@string/post_from"
/>
<Button
android:id="@+id/btnAccount"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/btn_bg_transparent"
android:gravity="center_vertical"
android:paddingEnd="8dp"
android:paddingStart="8dp"
android:textAllCaps="false"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<ImageButton
android:id="@+id/btnAttachment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/btn_bg_transparent"
android:minHeight="48dp"
android:minWidth="48dp"
android:src="@drawable/btn_attachment"
/>
<ImageButton
android:id="@+id/btnVisibility"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/btn_bg_transparent"
android:minHeight="48dp"
android:minWidth="48dp"
android:src="@drawable/ic_public"
/>
<TextView
android:id="@+id/tvCharCount"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:gravity="end|center_vertical"
/>
<ImageButton
android:id="@+id/btnPost"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/btn_bg_transparent"
android:minHeight="48dp"
android:minWidth="48dp"
android:src="@drawable/btn_post"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/llAttachment"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="12dp"
android:orientation="horizontal"
>
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/ivMedia1"
android:layout_width="48dp"
android:layout_height="match_parent"
android:background="@drawable/btn_bg_transparent"
android:scaleType="fitCenter"
/>
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/ivMedia2"
android:layout_width="48dp"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:background="@drawable/btn_bg_transparent"
android:scaleType="fitCenter"
/>
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/ivMedia3"
android:layout_width="48dp"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:background="@drawable/btn_bg_transparent"
android:scaleType="fitCenter"
/>
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/ivMedia4"
android:layout_width="48dp"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:background="@drawable/btn_bg_transparent"
android:scaleType="fitCenter"
/>
</LinearLayout>
<CheckBox
android:id="@+id/cbNSFW"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/nsfw"
/>
<ImageButton
android:id="@+id/btnAttachment"
android:layout_width="wrap_content"
<CheckBox
android:id="@+id/cbContentWarning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="@drawable/btn_attachment"
android:layout_marginTop="12dp"
android:text="@string/content_warning"
/>
<EditText
android:id="@+id/etContentWarning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/content_warning_hint"
/>
<TextView
android:id="@+id/tvCharCount"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/status"
/>
<ImageButton
android:id="@+id/btnPost"
android:layout_width="wrap_content"
<EditText
android:id="@+id/etContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="@drawable/btn_post"
android:hint="@string/content_hint"
android:inputType="textMultiLine"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/llAttachment"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="12dp"
android:orientation="horizontal"
>
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/ivMedia1"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:scaleType="centerCrop"
/>
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/ivMedia2"
android:layout_marginStart="8dp"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:scaleType="centerCrop"
/>
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/ivMedia3"
android:layout_marginStart="8dp"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:scaleType="centerCrop"
/>
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/ivMedia4"
android:layout_marginStart="8dp"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:scaleType="centerCrop"
/>
</LinearLayout>
<CheckBox
android:id="@+id/cbContentWarning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/content_warning"
/>
<EditText
android:id="@+id/etContentWarning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/content_warning_hint"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/status"
/>
<EditText
android:id="@+id/etContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:hint="@string/content_hint"
/>
</LinearLayout>
</ScrollView>

View File

@ -32,10 +32,10 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#40ffffff"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="12dp"
android:background="#C0FFFFFF"
>
<com.android.volley.toolbox.NetworkImageView
@ -43,7 +43,7 @@
android:layout_width="128dp"
android:layout_height="128dp"
android:layout_marginTop="20dp"
android:background="#888"
android:background="@drawable/btn_bg_transparent"
/>
<TextView
@ -89,6 +89,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="statuses\n124"
android:background="@drawable/btn_bg_transparent"
/>
<Button
@ -96,6 +97,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="following\n9999"
android:background="@drawable/btn_bg_transparent"
/>
<Button
@ -103,6 +105,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="followers\n9999"
android:background="@drawable/btn_bg_transparent"
/>
</LinearLayout>

View File

@ -4,10 +4,10 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:descendantFocusability="blocksDescendants"
android:orientation="vertical"
android:paddingBottom="12dp"
android:paddingTop="12dp"
>
<LinearLayout
@ -15,17 +15,18 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:background="@drawable/btn_bg_transparent"
android:orientation="horizontal"
>
<ImageView
android:id="@+id/ivBoosted"
android:layout_width="64dp"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginEnd="8dp"
android:scaleType="fitEnd"
android:src="@drawable/btn_boost"
android:id="@+id/ivBoosted"
/>
@ -50,6 +51,7 @@
android:id="@+id/tvBoosted"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
tools:text="~にブーストされました"
/>
@ -63,14 +65,15 @@
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:orientation="horizontal"
android:background="@drawable/btn_bg_transparent"
>
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/ivFollow"
android:layout_width="64dp"
android:layout_height="32dp"
android:layout_gravity="bottom"
android:layout_marginEnd="8dp"
android:id="@+id/ivFollow"
android:contentDescription="@string/thumbnail"
android:scaleType="fitEnd"
/>
@ -86,6 +89,7 @@
android:id="@+id/tvFollowerName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
tools:text="Follower Name"
/>
@ -99,20 +103,23 @@
/>
</LinearLayout>
<ImageButton
android:id="@+id/btnFollow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/btn_bg_transparent"
android:contentDescription="@string/follow"
android:src="@drawable/btn_follow"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/llStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:id="@+id/llStatus"
>
<com.android.volley.toolbox.NetworkImageView
@ -121,6 +128,7 @@
android:layout_height="64dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:background="@drawable/btn_bg_transparent"
android:contentDescription="@string/thumbnail"
android:scaleType="centerCrop"
/>
@ -147,111 +155,146 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/btn_bg_transparent"
android:textStyle="bold"
tools:text="Displayname @username"
/>
<LinearLayout
android:id="@+id/llContentWarning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp"
android:id="@+id/llContentWarning"
android:orientation="horizontal"
>
<Button
android:id="@+id/btnContentWarning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
tools:text="見る"
android:id="@+id/btnContentWarning"
android:minWidth="32dp"
tools:text="見る"
/>
<TextView
android:id="@+id/tvContentWarning"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/tvContentWarning"
/>
</LinearLayout>
<TextView
android:id="@+id/tvContent"
android:id="@+id/tvMentions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
tools:text="Contents\nContents"
android:gravity="end"
/>
<FrameLayout
<LinearLayout
android:id="@+id/llContents"
android:layout_width="match_parent"
android:layout_height="64dp"
android:layout_marginTop="8dp"
android:id="@+id/flMedia"
android:layout_height="match_parent"
android:orientation="vertical"
>
<LinearLayout
<TextView
android:id="@+id/tvTags"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="end"
/>
<TextView
android:id="@+id/tvContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
tools:text="Contents\nContents"
/>
<FrameLayout
android:id="@+id/flMedia"
android:layout_width="match_parent"
android:layout_height="64dp"
android:layout_marginTop="8dp"
>
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/ivMedia1"
android:layout_width="0dp"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:contentDescription="@string/thumbnail"
android:scaleType="centerCrop"
/>
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/ivMedia2"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:contentDescription="@string/thumbnail"
android:scaleType="centerCrop"
/>
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/ivMedia3"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:contentDescription="@string/thumbnail"
android:scaleType="centerCrop"
/>
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/ivMedia4"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:contentDescription="@string/thumbnail"
android:scaleType="centerCrop"
/>
android:orientation="horizontal"
>
<ImageButton
android:layout_width="32dp"
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/ivMedia1"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:contentDescription="@string/thumbnail"
android:scaleType="centerCrop"
/>
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/ivMedia2"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:contentDescription="@string/thumbnail"
android:scaleType="centerCrop"
/>
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/ivMedia3"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:contentDescription="@string/thumbnail"
android:scaleType="centerCrop"
/>
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/ivMedia4"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:contentDescription="@string/thumbnail"
android:scaleType="centerCrop"
/>
<ImageButton
android:id="@+id/btnHideMedia"
android:layout_width="32dp"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:background="@drawable/btn_bg_transparent"
android:contentDescription="@string/hide"
android:src="@drawable/btn_close"
/>
</LinearLayout>
<TextView
android:id="@+id/btnShowMedia"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:background="@null"
android:src="@drawable/btn_close"
android:contentDescription="@string/hide"
android:id="@+id/btnHideMedia"
android:background="#000"
android:gravity="center"
android:text="@string/tap_to_show"
android:textColor="#fff"
/>
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000"
android:textColor="#fff"
android:text="@string/tap_to_show"
android:id="@+id/btnShowMedia"
android:gravity="center"
/>
</FrameLayout>
</FrameLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="40dp"
android:layout_marginTop="8dp"
android:orientation="horizontal"
>
@ -259,32 +302,38 @@
<ImageButton
android:id="@+id/btnReply"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:background="@drawable/btn_bg_transparent"
android:contentDescription="@string/reply"
android:minWidth="48dp"
android:src="@drawable/btn_reply"
/>
<Button
android:id="@+id/btnBoost"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:background="@drawable/btn_bg_transparent"
android:drawablePadding="4dp"
android:minWidth="32dp"
android:minWidth="48dp"
/>
<Button
android:id="@+id/btnFavourite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:background="@drawable/btn_bg_transparent"
android:drawablePadding="4dp"
android:minWidth="32dp"
android:minWidth="48dp"
/>
<ImageButton
android:id="@+id/btnMore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:background="@drawable/btn_bg_transparent"
android:contentDescription="@string/more"
android:minWidth="48dp"
android:src="@drawable/btn_more"
/>

View File

@ -40,20 +40,22 @@
<ImageButton
android:id="@+id/btnColumnReload"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:background="@drawable/btn_bg_transparent"
android:contentDescription="@string/reload"
android:src="@drawable/btn_refresh"
/>
<ImageButton
android:id="@+id/btnColumnClose"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:background="@drawable/btn_bg_transparent"
android:contentDescription="@string/close_column"
android:src="@drawable/black_close"
/>

View File

@ -3,12 +3,19 @@
<group android:checkableBehavior="single">
<item android:title="@string/account">
<menu>
<item
android:id="@+id/nav_account_add"
android:icon="@drawable/ic_account_add"
android:title="@string/account_add"/>
<item
android:id="@+id/nav_account_setting"
android:icon="@drawable/ic_setting"
android:title="@string/account_setting"/>
</menu>
</item>
<item android:title="@string/add_column">
<menu>
<item
@ -55,7 +62,19 @@
<!--android:title="Tools"/>-->
</menu>
</item>
<item android:title="@string/setting">
<menu>
<item
android:id="@+id/nav_column_list"
android:icon="@drawable/ic_list"
android:title="@string/column_list"/>
<item
android:id="@+id/nav_app_setting"
android:icon="@drawable/ic_setting"
android:title="@string/app_setting"/>
</menu>
</item>
</group>
<!--<item android:title="Communicate">-->

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="colorPrimary">#c4c4c4</color>
<color name="colorPrimaryDark">#303030</color>
<color name="colorAccent">#5a5a5a</color>
<color name="colorLink">#00a2ff</color>

View File

@ -18,6 +18,7 @@
<string name="password_not_specified">password not specified</string>
<string name="network_error">network error. %1$s</string>
<string name="network_error_arg">network error. %1$s</string>
<string name="api_error">API error. %1$s</string>
<string name="register_app_to_server">registering this app to %1$s…</string>
@ -40,7 +41,6 @@
<string name="media_attachment">media attachment</string>
<string name="close_column">close column</string>
<string name="thumbnail">thumbnail</string>
<string name="boosted_by">boosted by %1$s</string>
<string name="reload">reload</string>
<string name="add_favourites">add your favourites</string>
<string name="federate_tl">federate timeline</string>
@ -53,7 +53,6 @@
<string name="your_notifications">your notifications</string>
<string name="statuses_of">statuses of %1$s</string>
<string name="follow">follow</string>
<string name="favourited_by">favourited by %1$s</string>
<string name="add_column">add column</string>
<string name="profile_page">profile\npage</string>
<string name="following">following</string>
@ -73,4 +72,44 @@
<string name="content_hint">please input your status</string>
<string name="status">status</string>
<string name="list_empty">no items in list</string>
<string name="post_from">post from</string>
<string name="choose_account">choose account</string>
<string name="response_not_json">API response is not JSON.</string>
<string name="attachment_too_many">max 4 files allowed.</string>
<string name="account_select_please">please select account</string>
<string name="login_failed">login failed</string>
<string name="file_size_too_big">file size too big. maximum limit is 8MB.</string>
<string name="visibility_public">public</string>
<string name="visibility_unlisted">unlisted</string>
<string name="visibility_private">private</string>
<string name="visibility_direct">direct</string>
<string name="choose_visibility">choose visibility</string>
<string name="confirm_delete_attachment">remove this attachment?</string>
<string name="post_error_contents_empty">please input your status.</string>
<string name="post_error_contents_warning_empty">please input contents warning</string>
<string name="wait_previous_operation">Please wait until last operation is over.</string>
<string name="cant_remove_boost_while_favourited">Can\'t remove boost while favourited</string>
<string name="confirm">confirm</string>
<string name="confirm_boost">Boost this status? it\'s shown by all followers and your profile page.</string>
<string name="tags">tags</string>
<string name="mentions">mentions</string>
<string name="display_name_favourited_by">favourited by %1$s</string>
<string name="display_name_boosted_by">boosted by %1$s</string>
<string name="display_name_replied_by">replied by %1$s</string>
<string name="display_name_followed_by">followed by %1$s</string>
<string name="account">account</string>
<string name="account_setting">account setting</string>
<string name="setting">setting</string>
<string name="app_setting">app setting</string>
<string name="columun_list">column list</string>
<string name="column_list">column list</string>
<string name="get_access_token">get access token</string>
<string name="account_remove">remove account from this app</string>
<string name="actions">actions</string>
<string name="default_status_visibility">default visibility of status</string>
<string name="confirm_before_boost">confirm before boost</string>
<string name="sensitive_content_default_open">sensitive content default open</string>
<string name="user">user</string>
<string name="confirm_account_remove">Account will be deleted. also all columns are remove.\nAre you sure?</string>
</resources>

View File

@ -1,4 +1,4 @@
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
@ -17,4 +17,75 @@
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light"/>
<style name="setting_group_header">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textSize">20sp</item>
<item name="android:textStyle">bold</item>
<item name="android:layout_marginTop">20dp</item>
</style>
<style name="setting_row_label">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:paddingLeft">12dp</item>
<item name="android:paddingRight">0dp</item>
<item name="android:paddingStart" tools:ignore="NewApi">12dp</item>
<item name="android:paddingEnd" tools:ignore="NewApi">0dp</item>
</style>
<style name="setting_row_form">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:orientation">horizontal</item>
<item name="android:baselineAligned">true</item>
<item name="android:paddingLeft">48dp</item>
<item name="android:paddingRight">0dp</item>
<item name="android:paddingStart" tools:ignore="NewApi">48dp</item>
<item name="android:paddingEnd" tools:ignore="NewApi">0dp</item>
</style>
<style name="setting_row_help">
<item name="android:background">@drawable/btn_bg_transparent</item>
<item name="android:textColor">@color/colorAccent</item>
<item name="android:layout_width">40dp</item>
<item name="android:layout_height">40dp</item>
<item name="android:minWidth">32dp</item>
<item name="android:minHeight">32dp</item>
<item name="android:layout_marginLeft">8dp</item>
<item name="android:layout_marginRight">0dp</item>
<item name="android:layout_marginStart" tools:ignore="NewApi">8dp</item>
<item name="android:layout_marginEnd" tools:ignore="NewApi">0dp</item>
<item name="android:text">@string/question</item>
</style>
<style name="setting_divider">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">1dp</item>
<item name="android:background">#ddd</item>
<item name="android:layout_marginTop">12dp</item>
<item name="android:layout_marginBottom">12dp</item>
</style>
<style name="setting_horizontal_stretch">
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_weight">1</item>
</style>
<style name="setting_edit_text" parent="@style/setting_horizontal_stretch">
<item name="android:imeOptions">actionDone</item>
</style>
<string name="question">\?</string>
</resources>

96
convert-emoji-codes.pl Normal file
View File

@ -0,0 +1,96 @@
#!perl --
use strict;
use warnings;
use utf8;
use LWP::Simple;
use JSON;
use Data::Dump qw(dump);
use Encode;
# perl convert-emoji-codes.pl < eac.json >converted.txt
# eac.json is here
# https://github.com/Ranks/emojione/blob/master/extras/alpha-codes/eac.json
my $data;
{
local $/ = undef;
$data = <STDIN>;
}
my $eac_map = decode_json $data;
undef $data;
my @list;
while( my($k,$v)=each %$eac_map){
{
my $t = $v->{"alpha code"};
my @a = ($t =~ /:([^\s:]+):/g );
for(@a){
push @list,[$k, $_ ];
}
}
{
my $t = $v->{"aliases"};
my @a = ($t =~ /:([^\s:]+):/g );
for(@a){
push @list,[$k, $_ ];
}
}
}
my %unicode_map;
sub putUnicodeMap{
my($map,$char,@remain)=@_;
$map->{$char} or $map->{$char} = {};
if(not @remain){
$map->{$char}->{e}=1;
}else{
putUnicodeMap( $map->{$char} ,@remain );
}
}
my $func_num = 0;
my $n = 0;
my $codepoint_max = 0;
my $length_max = 0;
sub addCode{
my($k,$name)=@_;
if( $n == 0 ){
++$func_num;
print "\tprivate static void init$func_num(){\n";
}
my @chars = split /-/,$k;
for(@chars){
my $codepoint = hex($_);
if( $codepoint > $codepoint_max ){
$codepoint_max = $codepoint;
}
}
my $char_count = 0+@chars;
if( $char_count > $length_max ){
$length_max = $char_count;
}
my $char_java = join(',',map{ "0x$_"} @chars );
print qq|\t\t_addEntry("$name", new String(new int[] {$char_java}, 0, $char_count));\n|;
if( ++$n > 100 ){
print "\t}\n";
$n = 0;
}
}
for(sort {$a->[1] cmp $b->[1]} @list){
addCode( @$_ );
}
if( $n > 0 ){
print "\t}\n";
}
print "\tstatic{\n";
for(my $i=1;$i <= $func_num;++$i){
print "\t\tinit$i();\n";
}
print "\t}\n";
printf "//codepoint_max=0x%x, length_max=$length_max\n",$codepoint_max;

12137
eac.json Normal file

File diff suppressed because it is too large Load Diff