commit d0e8e55e695cd49bbe7bf410386a03a38972828d Author: tateisu Date: Fri Apr 21 01:23:59 2017 +0900 initial import diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a4c78382 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 00000000..96cc43ef --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 00000000..c7d1c5a8 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/dictionaries/tateisu.xml b/.idea/dictionaries/tateisu.xml new file mode 100644 index 00000000..b4a54887 --- /dev/null +++ b/.idea/dictionaries/tateisu.xml @@ -0,0 +1,12 @@ + + + + favourited + reblog + reblogged + reblogs + subwaytooter + timelines + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 00000000..97626ba4 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 00000000..7ac24c77 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..b70ab773 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..6933c1ea --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..5d199810 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..452a776c --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 00000000..7f68460d --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..3543521e --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..8fd669f9 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,32 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 24 + buildToolsVersion '25.0.0' + defaultConfig { + applicationId "jp.juggler.subwaytooter" + minSdkVersion 21 + targetSdkVersion 24 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + compile 'com.android.support:appcompat-v7:24.2.0' + compile 'com.android.support:support-v4:24.2.0' + compile 'com.android.support:design:24.2.0' + compile 'com.android.support.constraint:constraint-layout:1.0.2' + testCompile 'junit:junit:4.12' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 00000000..acfee8c6 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in C:\android\sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/ExampleInstrumentedTest.java b/app/src/androidTest/java/jp/juggler/subwaytooter/ExampleInstrumentedTest.java new file mode 100644 index 00000000..89f4c663 --- /dev/null +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package jp.juggler.subwaytooter; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumentation test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception{ + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals( "jp.juggler.subwaytooter", appContext.getPackageName() ); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..543a4ead --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMain.java b/app/src/main/java/jp/juggler/subwaytooter/ActMain.java new file mode 100644 index 00000000..ff0a0c92 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMain.java @@ -0,0 +1,259 @@ +package jp.juggler.subwaytooter; + +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.design.widget.FloatingActionButton; +import android.support.design.widget.Snackbar; +import android.support.v4.os.AsyncTaskCompat; +import android.support.v4.view.ViewPager; +import android.view.View; +import android.support.design.widget.NavigationView; +import android.support.v4.view.GravityCompat; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.Menu; +import android.view.MenuItem; + +import jp.juggler.subwaytooter.api.TootApiClient; +import jp.juggler.subwaytooter.api.TootApiResult; +import jp.juggler.subwaytooter.dialog.AccountPicker; +import jp.juggler.subwaytooter.dialog.LoginForm; +import jp.juggler.subwaytooter.table.SavedAccount; +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class ActMain extends AppCompatActivity + implements NavigationView.OnNavigationItemSelectedListener { + public static final LogCategory log = new LogCategory( "ActMain" ); + + @Override + protected void onCreate( Bundle savedInstanceState ){ + super.onCreate( savedInstanceState ); + initUI(); + + } + + @Override + public void onBackPressed(){ + DrawerLayout drawer = (DrawerLayout) findViewById( R.id.drawer_layout ); + if( drawer.isDrawerOpen( GravityCompat.START ) ){ + drawer.closeDrawer( GravityCompat.START ); + }else{ + super.onBackPressed(); + } + } + + @Override + public boolean onCreateOptionsMenu( Menu menu ){ + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate( R.menu.act_main, menu ); + return true; + } + + @Override + public boolean onOptionsItemSelected( MenuItem item ){ + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + + //noinspection SimplifiableIfStatement + if( id == R.id.action_settings ){ + return true; + } + + return super.onOptionsItemSelected( item ); + } + + @SuppressWarnings("StatementWithEmptyBody") + @Override + public boolean onNavigationItemSelected( MenuItem item ){ + // Handle navigation view item clicks here. + int id = item.getItemId(); + + if( id == R.id.nav_account_add ){ + performAccountAdd(); + }else if( id == R.id.nav_add_tl_home ){ + performAddTimeline(Column.TYPE_TL_HOME ); + }else if( id == R.id.nav_add_tl_local ){ + performAddTimeline(Column.TYPE_TL_LOCAL ); + }else if( id == R.id.nav_add_tl_federate ){ + performAddTimeline(Column.TYPE_TL_FEDERATE ); + + }else if( id == R.id.nav_add_favourites ){ + performAddTimeline(Column.TYPE_TL_FAVOURITES ); +// }else if( id == R.id.nav_add_reports ){ +// performAddTimeline(Column.TYPE_TL_REPORTS ); + }else if( id == R.id.nav_add_statuses ){ + performAddTimeline(Column.TYPE_TL_STATUSES ); + }else if( id == R.id.nav_add_notifications ){ + performAddTimeline(Column.TYPE_TL_NOTIFICATIONS ); + + // Handle the camera action +// }else if( id == R.id.nav_gallery ){ +// +// }else if( id == R.id.nav_slideshow ){ +// +// }else if( id == R.id.nav_manage ){ +// +// }else if( id == R.id.nav_share ){ +// +// }else if( id == R.id.nav_send ){ + + } + + DrawerLayout drawer = (DrawerLayout) findViewById( R.id.drawer_layout ); + drawer.closeDrawer( GravityCompat.START ); + return true; + } + + ViewPager pager; + ColumnPagerAdapter pager_adapter; + View llEmpty; + + void initUI(){ + setContentView( R.layout.act_main ); + llEmpty = findViewById( R.id.llEmpty ); + + // toolbar + Toolbar toolbar = (Toolbar) findViewById( R.id.toolbar ); + setSupportActionBar( toolbar ); + + // navigation drawer + DrawerLayout drawer = (DrawerLayout) findViewById( R.id.drawer_layout ); + ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( + this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close ); + drawer.addDrawerListener( toggle ); + toggle.syncState(); + + NavigationView navigationView = (NavigationView) findViewById( R.id.nav_view ); + navigationView.setNavigationItemSelectedListener( this ); + + // floating action button + FloatingActionButton fab = (FloatingActionButton) findViewById( R.id.fab ); + fab.setOnClickListener( new View.OnClickListener() { + @Override + public void onClick( View view ){ + Snackbar.make( view, "Replace with your own action", Snackbar.LENGTH_LONG ) + .setAction( "Action", null ).show(); + } + } ); + + // ViewPager + pager = (ViewPager) findViewById( R.id.viewPager ); + pager_adapter = new ColumnPagerAdapter( this ); + pager.setAdapter( pager_adapter ); + } + + public void performAccountAdd(){ + LoginForm.showLoginForm( this, new LoginForm.LoginFormCallback() { + + @Override + public void startLogin( final Dialog dialog,final String instance, final String user_mail, final String password ){ + + final ProgressDialog progress = new ProgressDialog( ActMain.this ); + + final AsyncTask< Void, String, TootApiResult > task = new AsyncTask< Void, String, TootApiResult >() { + + boolean __isCancelled(){ + return isCancelled(); + } + + boolean is_added = false; + + @Override + protected TootApiResult doInBackground( Void... params ){ + TootApiClient api_client = new TootApiClient( ActMain.this, new TootApiClient.Callback() { + @Override + public boolean isCancelled(){ + return __isCancelled(); + } + + @Override + public void publishProgress( final String s ){ + Utils.runOnMainThread( new Runnable() { + @Override + public void run(){ + progress.setMessage( s ); + } + } ); + } + } ); + + api_client.setUserInfo( instance, user_mail, password ); + + TootApiResult result = api_client.get( "/api/v1/accounts/verify_credentials" ); + if( result != null && result.object != null ){ + is_added = ! SavedAccount.hasAccount(log,instance, user_mail); + SavedAccount.save( log,instance, user_mail, result.object ); + } + return result; + } + + @Override + protected void onPostExecute( TootApiResult result ){ + progress.dismiss(); + + if( result == null ){ + // cancelled. + }else if( result.object == null ){ + Utils.showToast( ActMain.this, true, result.error ); + log.e( result.error ); + }else{ + SavedAccount account = SavedAccount.loadAccount(log,instance,user_mail); + if( account != null ){ + ActMain.this.onAccountUpdated(account,is_added); + dialog.dismiss(); + } + } + } + }; + progress.setIndeterminate( true ); + progress.setCancelable( true ); + progress.setOnCancelListener( new DialogInterface.OnCancelListener() { + @Override + public void onCancel( DialogInterface dialog ){ + task.cancel( true ); + } + } ); + progress.show(); + AsyncTaskCompat.executeParallel( task ); + } + } ); + + } + + public void performColumnClose( Column column ){ + pager_adapter.removeColumn( pager,column ); + if( pager_adapter.getCount() == 0 ){ + llEmpty.setVisibility( View.VISIBLE ); + } + } + + private void onAccountUpdated( SavedAccount data, boolean is_added){ + Utils.showToast(this,false,R.string.accout_confirmed); + if( is_added ){ + Column col = new Column( this, data, Column.TYPE_TL_HOME ); + pager_adapter.addColumn( pager, col ); + llEmpty.setVisibility( View.GONE ); + } + } + + + private void performAddTimeline( final int type,final Object... params){ + AccountPicker.pick( this, new AccountPicker.AccountPickerCallback() { + @Override + public void onAccountPicked( SavedAccount ai ){ + Column col = new Column( ActMain.this, ai, type ,ai.id,params); + pager_adapter.addColumn( pager, col ); + pager.setCurrentItem( pager_adapter.getCount() -1 ); + llEmpty.setVisibility( View.GONE ); + } + } ); + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/App1.java b/app/src/main/java/jp/juggler/subwaytooter/App1.java new file mode 100644 index 00000000..9cac398a --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/App1.java @@ -0,0 +1,61 @@ +package jp.juggler.subwaytooter; + +import android.app.Application; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import jp.juggler.subwaytooter.table.AccessToken; +import jp.juggler.subwaytooter.table.ClientInfo; +import jp.juggler.subwaytooter.table.LogData; +import jp.juggler.subwaytooter.table.SavedAccount; + +public class App1 extends Application{ + + @Override + public void onCreate(){ + super.onCreate(); + if( db_open_helper == null ){ + db_open_helper = new DBOpenHelper( getApplicationContext() ); + } + } + + @Override + public void onTerminate(){ + super.onTerminate(); + } + + + static final String DB_NAME = "app_db"; + static final int DB_VERSION = 1; + + static DBOpenHelper db_open_helper; + + public static SQLiteDatabase getDB(){ + return db_open_helper.getWritableDatabase(); + } + + static class DBOpenHelper extends SQLiteOpenHelper { + + public DBOpenHelper( Context context ){ + super( context, DB_NAME, null , DB_VERSION ); + } + + @Override + public void onCreate( SQLiteDatabase db ){ + LogData.onDBCreate( db); + // + AccessToken.onDBCreate(db); + SavedAccount.onDBCreate(db); + ClientInfo.onDBCreate( db); + } + + @Override + public void onUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){ + LogData.onDBUpgrade( db,oldVersion,newVersion ); + AccessToken.onDBUpgrade( db,oldVersion,newVersion ); + SavedAccount.onDBUpgrade( db,oldVersion,newVersion ); + ClientInfo.onDBUpgrade( db,oldVersion,newVersion ); + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/Column.java b/app/src/main/java/jp/juggler/subwaytooter/Column.java new file mode 100644 index 00000000..a6cb231a --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/Column.java @@ -0,0 +1,282 @@ +package jp.juggler.subwaytooter; + +import android.os.AsyncTask; +import android.support.v4.os.AsyncTaskCompat; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.concurrent.atomic.AtomicBoolean; + +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.TootNotification; +import jp.juggler.subwaytooter.api.entity.TootReport; +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; + +public class Column { + static final LogCategory log = new LogCategory( "Column" ); + + final ActMain activity; + final SavedAccount access_info; + final int type; + final long who_id; + + static final int TYPE_TL_HOME = 1; + static final int TYPE_TL_LOCAL = 2; + static final int TYPE_TL_FEDERATE = 3; + static final int TYPE_TL_STATUSES = 4; + static final int TYPE_TL_FAVOURITES = 5; + static final int TYPE_TL_REPORTS = 6; + static final int TYPE_TL_NOTIFICATIONS = 7; + + public Column( ActMain activity, SavedAccount access_info, int type ){ + this( activity,access_info,type,access_info.id); + } + + public Column( ActMain activity, SavedAccount access_info, int type ,long who_id,Object... params){ + this.activity = activity; + this.access_info = access_info; + this.type = type; + this.who_id = who_id; + startLoading(); + } + + final AtomicBoolean is_dispose = new AtomicBoolean(); + + void dispose(){ + is_dispose.set( true ); + } + + public String getColumnName(){ + switch( type ){ + default: + return access_info.getFullAcct( access_info ) + "\n" + "?"; + case TYPE_TL_HOME: + return access_info.getFullAcct( access_info ) + "\n" + activity.getString( R.string.home ); + case TYPE_TL_LOCAL: + return access_info.getFullAcct( access_info ) + "\n" + activity.getString( R.string.local_timeline ); + case TYPE_TL_FEDERATE: + return access_info.getFullAcct( access_info ) + "\n" + activity.getString( R.string.federate_timeline ); + + case TYPE_TL_STATUSES: + return access_info.getFullAcct( access_info ) + "\n" + activity.getString( R.string.statuses_of + , who_account != null ? access_info.getFullAcct( who_account ) : Long.toString( who_id ) + ); + + case TYPE_TL_FAVOURITES: + return access_info.getFullAcct( access_info ) + "\n" + activity.getString( R.string.favourites ); + + case TYPE_TL_REPORTS: + return access_info.getFullAcct( access_info ) + "\n" + activity.getString( R.string.reports ); + + case TYPE_TL_NOTIFICATIONS: + return access_info.getFullAcct( access_info ) + "\n" + activity.getString( R.string.notifications ); + + } + } + + public interface VisualCallback { + void onVisualColumn(); + } + + final LinkedList< VisualCallback > visual_callback = new LinkedList<>(); + + void addVisualListener( VisualCallback listener ){ + if( listener == null ) return; + Iterator< VisualCallback > it = visual_callback.iterator(); + while( it.hasNext() ){ + VisualCallback vc = it.next(); + if( vc == listener ) return; + } + visual_callback.add( listener ); + } + + void removeVisualListener( VisualCallback listener ){ + if( listener == null ) return; + Iterator< VisualCallback > it = visual_callback.iterator(); + while( it.hasNext() ){ + VisualCallback vc = it.next(); + if( vc == listener ) it.remove(); + } + } + + private void fireVisualCallback(){ + Iterator< VisualCallback > it = visual_callback.iterator(); + while( it.hasNext() ){ + it.next().onVisualColumn(); + } + } + + AsyncTask< Void, Void, TootApiResult > last_task; + + void cancelLastTask(){ + if( last_task != null ) last_task.cancel( true ); + } + + boolean is_loading = false; + String task_progress; + String error = null; + + final TootStatus.List status_list = new TootStatus.List(); + final TootReport.List report_list = new TootReport.List(); + final TootNotification.List notification_list = new TootNotification.List(); + volatile TootAccount who_account; + + public void reload(){ + status_list.clear(); + startLoading(); + } + + void startLoading(){ + error = null; + is_loading = true; + fireVisualCallback(); + 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; + TootNotification.List tmp_list_notification; + + TootApiResult parseStatuses( TootApiResult result ){ + if( result != null ){ + tmp_list_status = TootStatus.parseList( log, result.array ); + } + return result; + } + + TootApiResult parseAccount( TootApiResult result ){ + if( result != null ){ + who_account = TootAccount.parse( log, result.object ); + } + return result; + } + + TootApiResult parseReports( TootApiResult result ){ + if( result != null ){ + tmp_list_report = TootReport.parseList( log, result.array ); + } + return result; + } + + TootApiResult parseNotifications( TootApiResult result ){ + if( result != null ){ + tmp_list_notification = TootNotification.parseList( log, result.array ); + } + return result; + } + + @Override + protected TootApiResult doInBackground( Void... params ){ + TootApiClient client = new TootApiClient( activity, new TootApiClient.Callback() { + @Override + public boolean isCancelled(){ + return __isCancelled() || is_dispose.get(); + } + + @Override + public void publishProgress( final String s ){ + Utils.runOnMainThread( new Runnable() { + @Override + public void run(){ + if( isCancelled() ) return; + task_progress = s; + fireVisualCallback(); + } + } ); + } + } ); + + client.setAccessInfo( access_info ); + + switch( type ){ + default: + case TYPE_TL_HOME: + return parseStatuses( client.get( "/api/v1/timelines/home" ) ); + + case TYPE_TL_LOCAL: + return parseStatuses( client.get( "/api/v1/timelines/public?local=1" ) ); + + case TYPE_TL_FEDERATE: + return parseStatuses( client.get( "/api/v1/timelines/public" ) ); + + case TYPE_TL_STATUSES: + if( who_account == null ){ + parseAccount( client.get( "/api/v1/accounts/" + who_id ) ); + client.callback.publishProgress( "" ); + } + + return parseStatuses( client.get( "/api/v1/accounts/"+who_id+"/statuses" ) ); + + case TYPE_TL_FAVOURITES: + return parseStatuses( client.get( "/api/v1/favourites" ) ); + + case TYPE_TL_REPORTS: + return parseReports( client.get( "/api/v1/reports" ) ); + + case TYPE_TL_NOTIFICATIONS: + return parseNotifications( client.get( "/api/v1/notifications" ) ); + } + } + + @Override + protected void onCancelled( TootApiResult result ){ + onPostExecute( null ); + } + + @Override + protected void onPostExecute( TootApiResult result ){ + is_loading = false; + if( result == null ){ + Column.this.error = activity.getString( R.string.cancelled ); + }else if( result.error != null ){ + Column.this.error = result.error; + }else{ + switch( type ){ + default: + case TYPE_TL_HOME: + case TYPE_TL_LOCAL: + case TYPE_TL_FEDERATE: + case TYPE_TL_STATUSES: + case TYPE_TL_FAVOURITES: + if( tmp_list_status != null ){ + for( int i = tmp_list_status.size() - 1 ; i >= 0 ; -- i ){ + status_list.add( 0, tmp_list_status.get( i ) ); + } + } + break; + + case TYPE_TL_REPORTS: + if( tmp_list_report != null ){ + for( int i = tmp_list_report.size() - 1 ; i >= 0 ; -- i ){ + report_list.add( 0, tmp_list_report.get( i ) ); + } + } + break; + + case TYPE_TL_NOTIFICATIONS: + if( tmp_list_notification != null ){ + for( int i = tmp_list_notification.size() - 1 ; i >= 0 ; -- i ){ + notification_list.add( 0, tmp_list_notification.get( i ) ); + } + } + break; + } + + } + fireVisualCallback(); + } + }; + + AsyncTaskCompat.executeParallel( task ); + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnPagerAdapter.java b/app/src/main/java/jp/juggler/subwaytooter/ColumnPagerAdapter.java new file mode 100644 index 00000000..a6230ca6 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnPagerAdapter.java @@ -0,0 +1,104 @@ +package jp.juggler.subwaytooter; + + +import android.app.Activity; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; + +public class ColumnPagerAdapter extends PagerAdapter{ + + final ActMain activity; + final LayoutInflater inflater; + + boolean loop_mode = false; + + ColumnPagerAdapter( ActMain activity ){ + this.activity = activity; + this.inflater = activity.getLayoutInflater(); + } + + final ArrayList column_list = new ArrayList<>(); + final SparseArray holder_list = new SparseArray<>(); + + int addColumn( ViewPager pager, Column column ){ + int size = column_list.size(); + if( size == 0 ){ + column_list.add( column ); + notifyDataSetChanged(); + return 0; + }else{ + int idx = 1+pager.getCurrentItem(); + column_list.add( idx, column ); + notifyDataSetChanged(); + pager.setCurrentItem( idx ); + return idx; + } + } + public void removeColumn( ViewPager pager,Column column ){ + int idx_column = column_list.indexOf( column ); + if( idx_column == - 1 ) return; + int idx_showing = pager.getCurrentItem(); + pager.setAdapter( null ); + column_list.remove( idx_column ); + pager.setAdapter( this ); + pager.setCurrentItem( idx_showing >= column_list.size() ? idx_showing -1 : idx_showing ); + + } + + + public Column getColumn( int idx ){ + return column_list.get( idx ); + } + + public ColumnViewHolder getColumnViewHolder( int idx ){ + return holder_list.get( idx ); + } + + + @Override public int getCount(){ + return column_list.size(); + } + + @Override public CharSequence getPageTitle( int page_idx ){ + return "page"+ page_idx; + } + + @Override + public boolean isViewFromObject( View view, Object object ){ + return view == object; + } + + @Override public Object instantiateItem( ViewGroup container, int page_idx ){ + View root = inflater.inflate( R.layout.page_column, container, false ); + container.addView( root, 0 ); + + Column column = column_list.get( page_idx ); + ColumnViewHolder holder = new ColumnViewHolder( activity,column, page_idx ); + // + holder_list.put( page_idx, holder ); + // + holder.onPageCreate( root ); + + return root; + } + + @Override public void destroyItem( ViewGroup container, int page_idx, Object object ){ + View view = (View) object; + // + container.removeView( view ); + // + ColumnViewHolder holder = holder_list.get( page_idx ); + holder_list.remove( page_idx ); + if( holder != null ){ + holder.is_destroyed.set( true ); + holder.onPageDestroy( view ); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java new file mode 100644 index 00000000..92742e67 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java @@ -0,0 +1,326 @@ +package jp.juggler.subwaytooter; + +import android.graphics.PorterDuff; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; + +import jp.juggler.subwaytooter.api.entity.TootNotification; +import jp.juggler.subwaytooter.api.entity.TootReport; +import jp.juggler.subwaytooter.api.entity.TootStatus; +import jp.juggler.subwaytooter.table.SavedAccount; +import jp.juggler.subwaytooter.util.LogCategory; + +public class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback { + static final LogCategory log = new LogCategory( "ColumnViewHolder" ); + + public final AtomicBoolean is_destroyed = new AtomicBoolean( false ); + public final ActMain activity; + public final Column column; + public final int column_index; + + public ColumnViewHolder( ActMain activity, Column column, int column_index ){ + log.d("ctor"); + this.activity = activity; + this.column = column; + this.column_index = column_index; + } + + public boolean isPageDestroyed(){ + return is_destroyed.get() || activity.isFinishing(); + } + + TextView tvLoading; + ListView listView; + TextView tvColumnName; + StatusListAdapter status_adapter; + + void onPageCreate( View root ){ + log.d("onPageCreate:%s",column.getColumnName() ); + + tvColumnName = (TextView) root.findViewById( R.id.tvColumnName ); + + + root.findViewById( R.id.btnColumnClose ).setOnClickListener( this ); + root.findViewById( R.id.btnColumnReload ).setOnClickListener( this ); + + tvLoading = (TextView) root.findViewById( R.id.tvLoading ); + listView = (ListView) root.findViewById( R.id.listView ); + status_adapter = new StatusListAdapter(); + listView.setAdapter( status_adapter ); + // + + column.addVisualListener( this ); + onVisualColumn(); + } + + void onPageDestroy( View root ){ + log.d("onPageDestroy:%s",column.getColumnName() ); + column.removeVisualListener( this ); + } + + @Override + public void onClick( View v ){ + switch( v.getId() ){ + case R.id.btnColumnClose: + activity.performColumnClose( column ); + break; + case R.id.btnColumnReload: + column.reload(); + break; + } + + } + + @Override + public void onVisualColumn(){ + + tvColumnName.setText(column.getColumnName() ); + + if( column.is_dispose.get() ){ + tvLoading.setVisibility( View.VISIBLE ); + listView.setVisibility( View.GONE ); + tvLoading.setText( "column was disposed." ); + return; + } + + if( column.is_loading ){ + tvLoading.setVisibility( View.VISIBLE ); + listView.setVisibility( View.GONE ); + String progress = column.task_progress; + if( progress == null ) progress = "loading?"; + tvLoading.setText( progress ); + return; + } + tvLoading.setVisibility( View.GONE ); + + if( column.who_account != null ){ + // TODO + }else{ + + } + + switch( column.type ){ + default: + case Column.TYPE_TL_HOME: + case Column.TYPE_TL_LOCAL: + case Column.TYPE_TL_FEDERATE: + case Column.TYPE_TL_FAVOURITES: + case Column.TYPE_TL_STATUSES: + listView.setVisibility( View.VISIBLE ); + status_adapter.set( column.status_list ); + break; + case Column.TYPE_TL_REPORTS: + listView.setVisibility( View.VISIBLE ); + status_adapter.set( column.report_list ); + break; + case Column.TYPE_TL_NOTIFICATIONS: + listView.setVisibility( View.VISIBLE ); + status_adapter.set( column.notification_list ); + break; + } + } + + /////////////////////////////////////////////////////////////////// + + class StatusListAdapter extends BaseAdapter { + final ArrayList< Object > status_list = new ArrayList<>(); + + + public void set( TootStatus.List src ){ + this.status_list.clear(); + this.status_list.addAll( src ); + notifyDataSetChanged(); + } + + public void set( TootReport.List src ){ + this.status_list.clear(); + this.status_list.addAll( src ); + notifyDataSetChanged(); + } + + public void set( TootNotification.List src ){ + this.status_list.clear(); + this.status_list.addAll( src ); + notifyDataSetChanged(); + } + + @Override + public int getCount(){ + return status_list.size(); + } + + @Override + public Object getItem( int position ){ + if( position >= 0 && position < status_list.size() ) return status_list.get( position ); + return null; + } + + @Override + public long getItemId( int position ){ + return 0; + } + + @Override + public View getView( int position, View view, ViewGroup parent ){ + Object o = ( position >= 0 && position < status_list.size() ? status_list.get( position ) : null ); + + StatusViewHolder holder; + if( view == null ){ + view = activity.getLayoutInflater().inflate( R.layout.lv_status, parent, false ); + holder = new StatusViewHolder( view ); + view.setTag( holder ); + }else{ + holder = (StatusViewHolder) view.getTag(); + } + holder.bind( activity, view, o, column.access_info ); + return view; + } + + } + + static class StatusViewHolder { + + final View llBoosted; + final ImageView ivBoosted; + final TextView tvBoosted; + final TextView tvBoostedTime; + + final View llFollow; + final ImageView ivFollow; + final TextView tvFollowerName; + final TextView tvFollowerAcct; + + final View llStatus; + final ImageView ivThumbnail; + final TextView tvName; + final TextView tvTime; + final TextView tvContent; + final ImageView ivMedia; + + final ImageButton btnReply; + final ImageButton btnBoost; + final ImageButton btnFavourite; + final ImageButton btnMore; + + Object item; + SavedAccount account; + + public StatusViewHolder( View view ){ + this.llBoosted = view.findViewById( R.id.llBoosted ); + this.ivBoosted = (ImageView) view.findViewById( R.id.ivBoosted ); + this.tvBoosted = (TextView) view.findViewById( R.id.tvBoosted ); + this.tvBoostedTime = (TextView) view.findViewById( R.id.tvBoostedTime ); + + this.llFollow = view.findViewById( R.id.llFollow ); + this.ivFollow = (ImageView) view.findViewById( R.id.ivFollow ); + this.tvFollowerName = (TextView) view.findViewById( R.id.tvFollowerName ); + this.tvFollowerAcct = (TextView) view.findViewById( R.id.tvFollowerAcct ); + + this.llStatus = view.findViewById( R.id.llStatus ); + + this.ivThumbnail = (ImageView) view.findViewById( R.id.ivThumbnail ); + this.tvName = (TextView) view.findViewById( R.id.tvName ); + this.tvTime = (TextView) view.findViewById( R.id.tvTime ); + this.tvContent = (TextView) view.findViewById( R.id.tvContent ); + this.ivMedia = (ImageView) view.findViewById( R.id.ivMedia ); + this.btnReply = (ImageButton) view.findViewById( R.id.btnReply ); + this.btnBoost = (ImageButton) view.findViewById( R.id.btnBoost ); + this.btnFavourite = (ImageButton) view.findViewById( R.id.btnFavourite ); + this.btnMore = (ImageButton) view.findViewById( R.id.btnMore ); + } + + public void bind( ActMain activity, View view, Object item, SavedAccount account ){ + this.account = account; + this.item = item; + + llBoosted.setVisibility( View.GONE ); + llFollow.setVisibility( View.GONE ); + llStatus.setVisibility( View.GONE ); + + if( item == null ) return; + + if( item instanceof TootNotification ){ + TootNotification n = (TootNotification) item; + if( TootNotification.TYPE_FAVOURITE.equals( n.type ) ){ + llBoosted.setVisibility( View.VISIBLE ); + ivBoosted.setImageResource( R.drawable.btn_favourite ); + tvBoostedTime.setText(TootStatus.formatTime( n.time_created_at ) + +"\n"+ account.getFullAcct( n.account ) + ); + tvBoosted.setText( activity.getString( R.string.favourited_by, n.account.display_name ) ); + + if( n.status != null ) bindSub( activity, view, n.status,account ); + }else if( TootNotification.TYPE_REBLOG.equals( n.type ) ){ + llBoosted.setVisibility( View.VISIBLE ); + ivBoosted.setImageResource( R.drawable.btn_boost ); + tvBoostedTime.setText(TootStatus.formatTime( n.time_created_at ) + +"\n"+ account.getFullAcct( n.account ) + ); + tvBoosted.setText( activity.getString( R.string.boosted_by, n.account.display_name ) ); + if( n.status != null ) bindSub( activity, view, n.status,account ); + }else if( TootNotification.TYPE_FOLLOW.equals( n.type )){ + llBoosted.setVisibility( View.VISIBLE ); + ivBoosted.setImageResource( R.drawable.btn_boost ); + tvBoostedTime.setText(TootStatus.formatTime( n.time_created_at ) + +"\n"+ account.getFullAcct( n.account ) + ); + tvBoosted.setText( activity.getString( R.string.boosted_by, n.account.display_name ) ); + // + llFollow.setVisibility( View.VISIBLE ); + ivFollow.setImageResource( R.drawable.btn_follow ); + tvFollowerName.setText( n.account.display_name ); + tvFollowerAcct.setText( account.getFullAcct( n.account )); + }else if( TootNotification.TYPE_MENTION.equals( n.type ) ){ + if( n.status != null ) bindSub( activity, view, n.status,account ); + } + return; + } + + if( item instanceof TootStatus ){ + TootStatus status = (TootStatus)item; + if( status.reblog != null ){ + llBoosted.setVisibility( View.VISIBLE ); + ivBoosted.setImageResource( R.drawable.btn_boost ); + tvBoostedTime.setText(TootStatus.formatTime( status.time_created_at ) + +"\n"+ account.getFullAcct( status.account ) + ); + tvBoosted.setText( activity.getString( R.string.boosted_by, status.account.display_name ) ); + bindSub( activity, view, status.reblog,account ); + }else{ + bindSub( activity, view, status ,account); + } + } + } + + private void bindSub( ActMain activity, View view, TootStatus status, SavedAccount account ){ + llStatus.setVisibility( View.VISIBLE ); + tvTime.setText( TootStatus.formatTime( status.time_created_at ) + +"\n"+ account.getFullAcct( status.account ) + ); + tvName.setText( status.account.display_name ); + tvContent.setText( status.content ); + + // TODO media + + btnBoost.getDrawable().setColorFilter( + ( status.reblogged ? 0xff0088ff : 0xff000000 ) + , PorterDuff.Mode.SRC_ATOP + ); + + btnFavourite.getDrawable().setColorFilter( + ( status.favourited ? 0xff0088ff : 0xff000000 ) + , PorterDuff.Mode.SRC_ATOP + ); + // todo show count of boost/fav + } + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/PagerAdapterBase.java b/app/src/main/java/jp/juggler/subwaytooter/PagerAdapterBase.java new file mode 100644 index 00000000..8626af24 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/PagerAdapterBase.java @@ -0,0 +1,123 @@ +package jp.juggler.subwaytooter; + + +import android.app.Activity; +import android.support.v4.view.PagerAdapter; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; + +public class PagerAdapterBase extends PagerAdapter{ + + public static abstract class PageViewHolder{ + + public final AtomicBoolean is_destroyed = new AtomicBoolean( false ); + public final Activity activity; + + @SuppressWarnings( "UnusedParameters" ) + public PageViewHolder( Activity activity, View ignored ){ + this.activity = activity; + } + + public boolean isPageDestroyed(){ + return is_destroyed.get() || activity.isFinishing(); + } + + @SuppressWarnings( "RedundantThrows" ) + protected abstract void onPageCreate( @SuppressWarnings( "UnusedParameters" ) int page_idx, View root ) throws Throwable; + + @SuppressWarnings( "RedundantThrows" ) + protected abstract void onPageDestroy( @SuppressWarnings( "UnusedParameters" ) int page_idx, @SuppressWarnings( "UnusedParameters" ) View root ) throws Throwable; + } + + public final Activity activity; + public final LayoutInflater inflater; + + public PagerAdapterBase( Activity activity ){ + this.activity = activity; + this.inflater = activity.getLayoutInflater(); + } + + protected final ArrayList title_list = new ArrayList<>(); + protected final ArrayList layout_id_list = new ArrayList<>(); + protected final ArrayList> holder_class_list = new ArrayList<>(); + protected final SparseArray holder_list = new SparseArray<>(); + + public int addPage( CharSequence title, int layout_id, Class holder_class ){ + int idx = title_list.size(); + title_list.add( title ); + layout_id_list.add( layout_id ); + holder_class_list.add( holder_class ); + // ページのインデックスを返す + return idx; + } + + // ページが存在する場合そのViewHolderを返す + // ページのViewが生成されていない場合はnullを返す + public T getPage( int idx ){ + PageViewHolder vh = holder_list.get( idx ); + if( vh == null ) return null; + return (T) holder_class_list.get( idx ).cast( vh ); + } + + public boolean loop_mode = false; + + public int getCountReal(){ + return title_list.size(); + } + + @Override public int getCount(){ + return loop_mode ? Integer.MAX_VALUE : title_list.size(); + } + + @Override public CharSequence getPageTitle( int page_idx ){ + return title_list.get( page_idx % getCountReal() ); + } + + @Override + public boolean isViewFromObject( View view, Object object ){ + return view == object; + } + + @Override public Object instantiateItem( ViewGroup container, int page_idx ){ + View root = inflater.inflate( layout_id_list.get( page_idx % getCountReal() ), container, false ); + container.addView( root, 0 ); + + try{ + PageViewHolder holder = + holder_class_list.get( page_idx % getCountReal() ) + .getConstructor( Activity.class, View.class ) + .newInstance( activity, root ); + // + holder_list.put( page_idx, holder ); + // + holder.onPageCreate( page_idx % getCountReal(), root ); + // + }catch( Throwable ex ){ + ex.printStackTrace(); + } + return root; + } + + @Override public void destroyItem( ViewGroup container, int page_idx, Object object ){ + View view = (View) object; + // + container.removeView( view ); + // + try{ + PageViewHolder holder = holder_list.get( page_idx ); + holder_list.remove( page_idx ); + if( holder != null ){ + holder.is_destroyed.set( true ); + holder.onPageDestroy( page_idx % getCountReal(), view ); + } + }catch( Throwable ex ){ + ex.printStackTrace(); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.java b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.java new file mode 100644 index 00000000..6104c8ec --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.java @@ -0,0 +1,190 @@ +package jp.juggler.subwaytooter.api; + +import android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.UUID; + +import jp.juggler.subwaytooter.table.AccessToken; +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; + +public class TootApiClient { + private static final LogCategory log = new LogCategory( "TootApiClient" ); + + + public interface Callback { + boolean isCancelled(); + + void publishProgress( String s ); + } + + private final Context context; + public final Callback callback; + + public TootApiClient( @NonNull Context context, @NonNull Callback callback ){ + this.context = context; + this.callback = callback; + } + + private String instance; + private String user_mail; + private String password; + + public void setUserInfo( String instance, String user_mail, String password ){ + this.instance = instance; + this.user_mail = user_mail; + this.password = password; + } + public void setAccessInfo( SavedAccount access_info ){ + this.instance = access_info.host; + this.user_mail = access_info.user_mail; + } + + public TootApiResult get( String path ){ + + final HTTPClient client = new HTTPClient( 60000, 10, "account", new CancelChecker() { + @Override + public boolean isCancelled(){ + return callback.isCancelled(); + } + } ); + + JSONObject client_info = null; + JSONObject token_info = null; + for( ; ; ){ + if( callback.isCancelled() ) return 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 ) ); + + // 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 ) ); + } + try{ + String result = Utils.decodeUTF8( data ); + // {"id":999,"redirect_uri":"urn:ietf:wg:oauth:2.0:oob","client_id":"******","client_secret":"******"} + client_info = new JSONObject( result ); + 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 ); + continue; + }catch( JSONException ex ){ + ex.printStackTrace(); + return new TootApiResult( Utils.formatError( ex, "API data error" ) ); + } + } + if( token_info == null ){ + // DBにあるならそれを使う + token_info = AccessToken.load( instance, user_mail ); + if( token_info != null ) continue; + + if( password == null ){ + // 手動でアクセストークンを再取得しなければいけない + return new TootApiResult( context.getString( R.string.login_required ) ); + } + + callback.publishProgress( 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; + + // TODO: アプリIDが無効な場合はどんなエラーが出る? + + if( data == null ){ + return new TootApiResult( context.getString( R.string.network_error, client.last_error ) ); + } + + try{ + String result = Utils.decodeUTF8( data ); + // {"access_token":"******","token_type":"bearer","scope":"read","created_at":1492334641} + token_info = new JSONObject( result ); + String error = Utils.optStringX( client_info, "error" ); + if( ! TextUtils.isEmpty( error ) ){ + return new TootApiResult( context.getString( R.string.api_error, error ) ); + } + AccessToken.save( instance, user_mail, result ); + continue; + }catch( JSONException 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( result,array ); + }else{ + JSONObject json = new JSONObject( result ); + + String error = Utils.optStringX( client_info, "error" ); + if( ! TextUtils.isEmpty( error ) ){ + return new TootApiResult( context.getString( R.string.api_error, error ) ); + } + return new TootApiResult( result,json ); + } + }catch( JSONException ex ){ + ex.printStackTrace(); + return new TootApiResult( Utils.formatError( ex, "API data error" ) ); + } + } + } + } +} + + diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResult.java b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResult.java new file mode 100644 index 00000000..faf02060 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResult.java @@ -0,0 +1,25 @@ +package jp.juggler.subwaytooter.api; + +import org.json.JSONArray; +import org.json.JSONObject; + +public class TootApiResult { + public String error; + public JSONObject object; + public JSONArray array; + public String json; + public TootApiResult( String error ){ + this.error = error; + } + + public TootApiResult( String json,JSONObject object ){ + this.json = json; + this.object = object; + } + + public TootApiResult( String json,JSONArray array ){ + this.json = json; + this.array = array; + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.java new file mode 100644 index 00000000..5ce5427a --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.java @@ -0,0 +1,109 @@ +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 TootAccount { + + public static class List extends ArrayList< TootAccount > { + + } + + // The ID of the account + public long id; + + // The username of the account + public String username; + + // Equals username for local users, includes @domain for remote ones + public String acct; + + // The account's display name + public String display_name; + + //Boolean for when the account cannot be followed without waiting for approval first + public boolean locked; + + // The time the account was created + // ex: "2017-04-13T11:06:08.289Z" + public String created_at; + + // The number of followers for the account + public long followers_count; + + //The number of accounts the given account is following + public long following_count; + + // The number of statuses the account has made + public long statuses_count; + + // Biography of user + // 説明文。改行は\r\n。リンクなどはHTMLタグで書かれている + public String note; + + //URL of the user's profile page (can be remote) + // https://mastodon.juggler.jp/@tateisu + public String url; + + // URL to the avatar image + public String avatar; + + // URL to the avatar static image (gif) + public String avatar_static; + + //URL to the header image + public String header; + + // URL to the header static image (gif) + public String header_static; + + public static TootAccount parse( LogCategory log, JSONObject src, TootAccount dst ){ + if( src == null ) return null; + try{ + 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" ); + 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.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"} + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e( ex, "TootAccount.parse failed." ); + return null; + } + } + + public static TootAccount parse( LogCategory log, JSONObject src ){ + return parse( log, src, new TootAccount() ); + } + + public static List parseList( LogCategory log, JSONArray array ){ + List result = new List(); + if( array != null ){ + for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + JSONObject src = array.optJSONObject( i ); + if( src == null ) continue; + TootAccount item = parse( log, src ); + if( item != null ) result.add( 0, item ); + } + } + return result; + } + + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootApplication.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootApplication.java new file mode 100644 index 00000000..c9897eb6 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootApplication.java @@ -0,0 +1,24 @@ +package jp.juggler.subwaytooter.api.entity; + +import org.json.JSONObject; + +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class TootApplication { + public String name; + public String website; + + public static TootApplication parse( LogCategory log, JSONObject src ){ + try{ + TootApplication dst = new TootApplication(); + dst.name = Utils.optStringX( src, "name" ); + dst.website = Utils.optStringX( src, "website" ); + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e( ex, "TootApplication.parse failed." ); + return null; + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAttachment.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAttachment.java new file mode 100644 index 00000000..3dc03435 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAttachment.java @@ -0,0 +1,67 @@ +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 TootAttachment { + + public static class List extends ArrayList< TootAttachment > { + + } + + // ID of the attachment + public long id; + + //One of: "image", "video", "gifv". or may null ? may "unknown" ? + public String type; + public static final String TYPE_IMAGE = "image"; + public static final String TYPE_VIDEO = "video"; + public static final String TYPE_GIFV = "gifv"; + + //URL of the locally hosted version of the image + public String url; + + //For remote images, the remote URL of the original image + public String remote_url; + + // URL of the preview image + public String preview_url; + + // Shorter URL for the image, for insertion into text (only present on local images) + public String text_url; + + public static TootAttachment parse( LogCategory log, JSONObject src ){ + if( src == null ) return null; + try{ + TootAttachment dst = new TootAttachment(); + dst.id = src.optLong( "id" ); + dst.type = Utils.optStringX( src, "type" ); + dst.url = Utils.optStringX( src, "url" ); + dst.remote_url = Utils.optStringX( src, "remote_url" ); + dst.preview_url = Utils.optStringX( src, "preview_url" ); + dst.text_url = Utils.optStringX( src, "text_url" ); + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e( ex, "TootAttachment.parse failed." ); + return null; + } + } + + public static List parseList( LogCategory log, JSONArray array ){ + List result = new List(); + for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + JSONObject src = array.optJSONObject( i ); + if( src == null ) continue; + TootAttachment item = parse( log, src ); + if( item != null ) result.add( 0, item ); + } + return result; + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootCard.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootCard.java new file mode 100644 index 00000000..35ddef21 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootCard.java @@ -0,0 +1,39 @@ +package jp.juggler.subwaytooter.api.entity; + +import org.json.JSONObject; + +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class TootCard { + + + // The url associated with the card + public String url; + + // The title of the card + public String title; + + // The card description + public String description; + + // The image associated with the card, if any + public String image; + + public static TootCard parse( LogCategory log, JSONObject src ){ + if( src==null) return null; + try{ + TootCard dst = new TootCard(); + dst.url = Utils.optStringX( src, "url" ); + dst.title = Utils.optStringX( src, "title" ); + dst.description = Utils.optStringX( src, "description" ); + dst.image = Utils.optStringX( src, "image" ); + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e(ex,"TootCard.parse failed."); + return null; + } + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootContext.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootContext.java new file mode 100644 index 00000000..309802dd --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootContext.java @@ -0,0 +1,28 @@ +package jp.juggler.subwaytooter.api.entity; + +import org.json.JSONObject; + +import jp.juggler.subwaytooter.util.LogCategory; + +public class TootContext { + + // The ancestors of the status in the conversation, as a list of Statuses + public TootStatus.List ancestors; + + // descendants The descendants of the status in the conversation, as a list of Statuses + public TootStatus.List descendants; + + public static TootContext parse( LogCategory log, JSONObject src ){ + if( src==null) return null; + try{ + TootContext dst = new TootContext(); + dst.ancestors = TootStatus.parseList( log,src.optJSONArray( "ancestors" ) ); + dst.descendants = TootStatus.parseList(log, src.optJSONArray( "descendants" ) ); + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e(ex,"TootContext.parse failed."); + return null; + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootError.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootError.java new file mode 100644 index 00000000..0f82a952 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootError.java @@ -0,0 +1,26 @@ +package jp.juggler.subwaytooter.api.entity; + +import org.json.JSONObject; + +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class TootError { + + // A textual description of the error + public String error; + + public static TootError parse( LogCategory log, JSONObject src ){ + if( src==null ) return null; + try{ + TootError dst = new TootError(); + dst.error = Utils.optStringX( src, "error" ); + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e(ex,"TootError.parse failed."); + return null; + } + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.java new file mode 100644 index 00000000..21ed3bea --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.java @@ -0,0 +1,37 @@ +package jp.juggler.subwaytooter.api.entity; + +import org.json.JSONObject; + +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class TootInstance { + + // URI of the current instance + public String uri; + + // The instance's title + public String title; + + // A description for the instance + public String description; + + // An email address which can be used to contact the instance administrator + public String email; + + public static TootInstance parse( LogCategory log, JSONObject src ){ + if( src == null ) return null; + try{ + TootInstance dst = new TootInstance(); + dst.uri = Utils.optStringX( src, "uri" ); + dst.title = Utils.optStringX( src, "title" ); + dst.description = Utils.optStringX( src, "description" ); + dst.email = Utils.optStringX( src, "email" ); + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e( ex, "TootInstance.parse failed." ); + return null; + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootMention.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootMention.java new file mode 100644 index 00000000..0f6e1dff --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootMention.java @@ -0,0 +1,57 @@ +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 TootMention { + + public static class List extends ArrayList< TootMention > { + + } + // URL of user's profile (can be remote) + public String url; + + // The username of the account + public String username; + + // Equals username for local users, includes @domain for remote ones + public String acct; + + // Account ID + public long id; + + public static TootMention parse( LogCategory log, JSONObject src ){ + if( src == null ) return null; + try{ + TootMention dst = new TootMention(); + dst.url = Utils.optStringX( src, "url" ); + dst.username = Utils.optStringX( src, "username" ); + dst.acct = Utils.optStringX( src, "acct" ); + dst.id = src.optLong( "id" ); + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e( ex, "TootMention.parse failed." ); + return null; + } + } + + + public static List parseList( LogCategory log, JSONArray array ){ + List result = new List(); + if( array != null ){ + for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + JSONObject src = array.optJSONObject( i ); + if( src == null ) continue; + TootMention item = parse( log, src ); + if( item != null ) result.add( 0, item ); + } + } + return result; + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.java new file mode 100644 index 00000000..5cde5efb --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.java @@ -0,0 +1,74 @@ +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 TootNotification { + + // The notification ID + public long id; + + // One of: "mention", "reblog", "favourite", "follow" + public String type; + + public static final String TYPE_MENTION = "mention"; + public static final String TYPE_REBLOG = "reblog"; + public static final String TYPE_FAVOURITE = "favourite"; + public static final String TYPE_FOLLOW = "follow"; + + // The time the notification was created + public String created_at; + + // The Account sending the notification to the user + public TootAccount account; + + // The Status associated with the notification, if applicable + public TootStatus status; + + public long time_created_at; + + public static TootNotification parse( LogCategory log, JSONObject src ){ + if( src == null ) return null; + try{ + TootNotification dst = new TootNotification(); + dst.id = src.optLong( "id" ); + dst.type = Utils.optStringX( src, "type" ); + dst.created_at = Utils.optStringX( src, "created_at" ); + dst.account = TootAccount.parse( log, src.optJSONObject( "account" ) ); + dst.status = TootStatus.parse( log, src.optJSONObject( "status" ) ); + + + dst.time_created_at = TootStatus.parseTime( log, dst.created_at ); + + + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e( ex, "TootNotification.parse failed." ); + return null; + } + } + + + public static class List extends ArrayList< TootNotification > { + + } + + public static List parseList( LogCategory log, JSONArray array ){ + List result = new List(); + if( array != null ){ + for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + JSONObject src = array.optJSONObject( i ); + if( src == null ) continue; + TootNotification item = parse( log, src ); + if( item != null ) result.add( 0, item ); + } + } + return result; + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootRelationShip.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootRelationShip.java new file mode 100644 index 00000000..a0b94975 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootRelationShip.java @@ -0,0 +1,45 @@ +package jp.juggler.subwaytooter.api.entity; + +import org.json.JSONObject; + +import jp.juggler.subwaytooter.util.LogCategory; + +public class TootRelationShip { + + // Target account id + public long id; + + // Whether the user is currently following the account + public boolean following; + + // Whether the user is currently being followed by the account + public boolean followed_by; + + // Whether the user is currently blocking the account + public boolean blocking; + + // Whether the user is currently muting the account + public boolean muting; + + // Whether the user has requested to follow the account + public boolean requested; + + public static TootRelationShip parse( LogCategory log, JSONObject src ){ + if( src == null ) return null; + try{ + TootRelationShip dst = new TootRelationShip(); + dst.id = src.optLong( "id" ); + dst.following = src.optBoolean( "following" ); + dst.followed_by = src.optBoolean( "followed_by" ); + dst.blocking = src.optBoolean( "blocking" ); + dst.muting = src.optBoolean( "muting" ); + dst.requested = src.optBoolean( "requested" ); + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e(ex,"TootRelationShip.parse failed."); + return null; + } + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootReport.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootReport.java new file mode 100644 index 00000000..063b570a --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootReport.java @@ -0,0 +1,49 @@ +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 TootReport { + + // The ID of the report + public long id; + + // The action taken in response to the report + public String action_taken; + + public static TootReport parse( LogCategory log, JSONObject src ){ + if( src == null ) return null; + try{ + TootReport dst = new TootReport(); + dst.id = src.optLong( "id" ); + dst.action_taken = Utils.optStringX( src, "action_taken" ); + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e( ex, "TootReport.parse failed." ); + return null; + } + } + + public static class List extends ArrayList< TootReport > { + + } + + public static List parseList( LogCategory log, JSONArray array ){ + List result = new List(); + if( array != null ){ + for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + JSONObject src = array.optJSONObject( i ); + if( src == null ) continue; + TootReport item = parse( log, src ); + if( item != null ) result.add( 0, item ); + } + } + return result; + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootResults.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootResults.java new file mode 100644 index 00000000..affc9048 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootResults.java @@ -0,0 +1,34 @@ +package jp.juggler.subwaytooter.api.entity; + +import org.json.JSONObject; + +import java.util.ArrayList; + +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class TootResults { + // An array of matched Accounts + public TootAccount.List accounts; + + // An array of matchhed Statuses + public TootStatus.List statuses; + + // An array of matched hashtags, as strings + public ArrayList< String > hashtags; + + public static TootResults parse( LogCategory log, JSONObject src ){ + if( src == null ) return null; + try{ + TootResults dst = new TootResults(); + dst.accounts = TootAccount.parseList( log, src.optJSONArray( "accounts" ) ); + dst.statuses = TootStatus.parseList( log, src.optJSONArray( "statuses" ) ); + dst.hashtags = Utils.parseStringArray( log, src.optJSONArray( "hashtags" ) ); + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e( ex, "TootResults.parse failed." ); + return null; + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.java new file mode 100644 index 00000000..b6affb08 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.java @@ -0,0 +1,160 @@ +package jp.juggler.subwaytooter.api.entity; + +import android.text.TextUtils; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; + +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class TootStatus { + + public static class List extends ArrayList< TootStatus > { + + } + + // The ID of the status + public long id; + + // A Fediverse-unique resource ID + public String uri; + + //URL to the status page (can be remote) + public String url; + + // The TootAccount which posted the status + public TootAccount account; + + // null or the ID of the status it replies to + public String in_reply_to_id; + + // null or the ID of the account it replies to + public String in_reply_to_account_id; + + // null or the reblogged Status + public TootStatus reblog; + + // Body of the status; this will contain HTML (remote HTML already sanitized) + public String content; + + // The time the status was created + public String created_at; + + //The number of reblogs for the status + public long reblogs_count; + + //The number of favourites for the status + public long favourites_count; + + // Whether the authenticated user has reblogged the status + public boolean reblogged; + + // Whether the authenticated user has favourited the status + public boolean favourited; + + //Whether media attachments should be hidden by default + public boolean sensitive; + + //If not empty, warning text that should be displayed before the actual content + public String spoiler_text; + + //One of: public, unlisted, private, direct + public String visibility; + + // An array of Attachments + public TootAttachment.List media_attachments; + + // An array of Mentions + public TootMention.List mentions; + + //An array of Tags + public ArrayList tags; + + //Application from which the status was posted + public String application; + + public long time_created_at; + + public static TootStatus parse( LogCategory log, JSONObject src ){ + + if( src == null ) return null; + + try{ + TootStatus status = new TootStatus(); + // log.d( "parse: %s", src.toString() ); + status.id = src.optLong( "id" ); + status.uri = Utils.optStringX( src, "uri" ); + status.url = Utils.optStringX( src, "url" ); + status.account = TootAccount.parse( log, src.optJSONObject( "account" ) ); + status.in_reply_to_id = Utils.optStringX( src, "in_reply_to_id" ); // null + status.in_reply_to_account_id = Utils.optStringX( src, "in_reply_to_account_id" ); // null + status.reblog = TootStatus.parse( log, src.optJSONObject( "reblog" )); + status.content = Utils.optStringX( src, "content" ); + status.created_at = Utils.optStringX( src, "created_at" ); // "2017-04-16T09:37:14.000Z" + status.reblogs_count = src.optLong( "reblogs_count" ); + status.favourites_count = src.optLong( "favourites_count" ); + status.reblogged = src.optBoolean( "reblogged" ); + status.favourited = src.optBoolean( "favourited" ); + status.sensitive = src.optBoolean( "sensitive" ); // false + status.spoiler_text = Utils.optStringX( src, "spoiler_text" ); // "",null, or CW text + 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.application = Utils.optStringX( src, "application" ); // null + + status.time_created_at = parseTime( log, status.created_at ); + + return status; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e( ex, "TootStatus.parse failed." ); + return null; + } + } + + public static List parseList( LogCategory log, JSONArray array ){ + List result = new List(); + if( array != null ){ + for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + JSONObject src = array.optJSONObject( i ); + if( src == null ) continue; + TootStatus item = parse( log, src ); + if( item != null ) result.add( 0, item ); + } + } + return result; + } + + private static final SimpleDateFormat date_format_utc = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault() ); + + public static long parseTime( LogCategory log, String strTime ){ + if( ! TextUtils.isEmpty( strTime ) ){ + try{ + date_format_utc.setTimeZone( TimeZone.getTimeZone( "GMT" ) ); + return date_format_utc.parse( strTime ).getTime(); + }catch( ParseException ex ){ + ex.printStackTrace(); + log.e( ex, "TootStatus.parseTime failed." ); + } + } + return 0L; + } + + private static final SimpleDateFormat date_format = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss", Locale.getDefault() ); + + public static String formatTime( long t ){ + date_format.setTimeZone( TimeZone.getDefault() ); + return date_format.format( new Date( t ) ); + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/dialog/AccountPicker.java b/app/src/main/java/jp/juggler/subwaytooter/dialog/AccountPicker.java new file mode 100644 index 00000000..9e93889e --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/dialog/AccountPicker.java @@ -0,0 +1,54 @@ +package jp.juggler.subwaytooter.dialog; + +import android.app.AlertDialog; +import android.content.DialogInterface; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +import jp.juggler.subwaytooter.ActMain; +import jp.juggler.subwaytooter.R; +import jp.juggler.subwaytooter.table.SavedAccount; + +public class AccountPicker { + + public interface AccountPickerCallback{ + void onAccountPicked(SavedAccount ai); + } + + public static void pick( ActMain activity, final AccountPickerCallback callback){ + + final ArrayList account_list = SavedAccount.loadAccountList(ActMain.log); + + Collections.sort( account_list, new Comparator< SavedAccount >() { + @Override + public int compare( SavedAccount o1, SavedAccount o2 ){ + int i = String.CASE_INSENSITIVE_ORDER.compare( o1.acct, o2.acct ); + if( i != 0 ) return i; + return String.CASE_INSENSITIVE_ORDER.compare( o1.host, o2.host ); + } + } ); + + String[] caption_list = new String[ account_list.size() ]; + + for(int i=0,ie=account_list.size();i= 0 && which < account_list.size() ){ + callback.onAccountPicked(account_list.get(which)); + dialog.dismiss(); + } + } + } ) + .setTitle( R.string.account_pick ) + .show(); + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/dialog/LoginForm.java b/app/src/main/java/jp/juggler/subwaytooter/dialog/LoginForm.java new file mode 100644 index 00000000..7861dcdc --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/dialog/LoginForm.java @@ -0,0 +1,61 @@ +package jp.juggler.subwaytooter.dialog; + +import android.app.Dialog; +import android.text.TextUtils; +import android.view.View; +import android.view.WindowManager; +import android.widget.EditText; + +import jp.juggler.subwaytooter.ActMain; +import jp.juggler.subwaytooter.R; +import jp.juggler.subwaytooter.util.Utils; + +/** + * Created by tateisu on 2017/04/16. + */ + +public class LoginForm { + + public interface LoginFormCallback{ + void startLogin(Dialog dialog,String instance,String user_main,String password); + } + + public static void showLoginForm(final ActMain activity,final LoginFormCallback callback){ + final View view = activity.getLayoutInflater().inflate( R.layout.dlg_account_add, null, false ); + final EditText etInstance = (EditText) view.findViewById( R.id.etInstance ); + final EditText etUserMail = (EditText) view.findViewById( R.id.etUserMail ); + final EditText etUserPassword = (EditText) view.findViewById( R.id.etUserPassword ); + final Dialog dialog = new Dialog( activity ); + dialog.setContentView( view ); + view.findViewById( R.id.btnOk ).setOnClickListener( new View.OnClickListener() { + @Override + public void onClick( View v ){ + final String instance = etInstance.getText().toString().trim(); + final String user_mail = etUserMail.getText().toString().trim(); + final String password = etUserPassword.getText().toString().trim(); + if( TextUtils.isEmpty( instance ) ){ + Utils.showToast( activity, true, R.string.instance_not_specified ); + return; + } + if( TextUtils.isEmpty( user_mail ) ){ + Utils.showToast(activity, true, R.string.mail_not_specified ); + return; + } + if( TextUtils.isEmpty( password ) ){ + Utils.showToast( activity, true, R.string.password_not_specified ); + return; + } + callback.startLogin( dialog,instance,user_mail,password ); + } + } ); + view.findViewById( R.id.btnCancel ).setOnClickListener( new View.OnClickListener() { + @Override + public void onClick( View v ){ + dialog.cancel(); + } + } ); + //noinspection ConstantConditions + dialog.getWindow().setLayout( WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT ); + dialog.show(); + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/AccessToken.java b/app/src/main/java/jp/juggler/subwaytooter/table/AccessToken.java new file mode 100644 index 00000000..8edce58a --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/table/AccessToken.java @@ -0,0 +1,78 @@ +package jp.juggler.subwaytooter.table; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +import jp.juggler.subwaytooter.App1; +import jp.juggler.subwaytooter.util.LogCategory; + +public class AccessToken { + + static final LogCategory log = new LogCategory( "AccessToken" ); + + static final String table = "access_token"; + + static final String COL_HOST = "h"; + static final String COL_USER_MAIL = "um"; + static final String COL_TOKEN = "t"; + + public String host; + public String user_mail; + + public static void onDBCreate( SQLiteDatabase db ){ + db.execSQL( + "create table if not exists " + table + + "(_id INTEGER PRIMARY KEY" + + ",h text not null" + + ",um text not null" + + ",t text not null" + + ")" + ); + db.execSQL( + "create unique index if not exists " + table + "_host on " + table + + "(h" + + ",um" + + ")" + ); + } + + public static void onDBUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){ + + } + + public static JSONObject load( String instance, String user_mail ){ + try{ + Cursor cursor = App1.getDB().query( table, null, "h=? and um=?", new String[]{ instance, user_mail }, null, null, null ); + try{ + if( cursor.moveToFirst() ){ + return new JSONObject( cursor.getString( cursor.getColumnIndex( COL_TOKEN ) ) ); + } + }finally{ + cursor.close(); + } + }catch( Throwable ex ){ + log.e( ex, "load failed." ); + } + return null; + } + + public static void save( String host, String user_mail, String json ){ + try{ + ContentValues cv = new ContentValues(); + cv.put( COL_HOST, host ); + cv.put( COL_USER_MAIL, user_mail ); + cv.put( COL_TOKEN, json ); + App1.getDB().replace( table, null, cv ); + }catch( Throwable ex ){ + log.e( ex, "save failed." ); + } + } + + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/ClientInfo.java b/app/src/main/java/jp/juggler/subwaytooter/table/ClientInfo.java new file mode 100644 index 00000000..429ad3ea --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/table/ClientInfo.java @@ -0,0 +1,64 @@ +package jp.juggler.subwaytooter.table; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import org.json.JSONObject; + +import jp.juggler.subwaytooter.App1; +import jp.juggler.subwaytooter.util.LogCategory; + +public class ClientInfo { + static final LogCategory log = new LogCategory( "ClientInfo" ); + + static final String table = "client_info"; + static final String COL_HOST = "h"; + static final String COL_RESULT = "r"; + + public static void onDBCreate( SQLiteDatabase db ){ + db.execSQL( + "create table if not exists " + table + + "(_id INTEGER PRIMARY KEY" + + ",h text not null" + + ",r text not null" + + ")" + ); + db.execSQL( + "create unique index if not exists " + table + "_host on " + table + + "(h" + + ")" + ); + } + + public static void onDBUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){ + + } + + public static JSONObject load( String instance ){ + try{ + Cursor cursor = App1.getDB().query( table, null, "h=?", new String[]{ instance }, null, null, null ); + try{ + if( cursor.moveToFirst() ){ + return new JSONObject( cursor.getString( cursor.getColumnIndex( COL_RESULT ) ) ); + } + }finally{ + cursor.close(); + } + }catch( Throwable ex ){ + log.e( ex, "load failed." ); + } + return null; + } + + public static void save( String host, String json ){ + try{ + ContentValues cv = new ContentValues(); + cv.put( COL_HOST, host ); + cv.put( COL_RESULT, json ); + App1.getDB().replace( table, null, cv ); + }catch( Throwable ex ){ + log.e( ex, "save failed." ); + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/LogData.java b/app/src/main/java/jp/juggler/subwaytooter/table/LogData.java new file mode 100644 index 00000000..f13c1b75 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/table/LogData.java @@ -0,0 +1,84 @@ +package jp.juggler.subwaytooter.table; + +import android.content.ContentValues; +import android.database.sqlite.SQLiteDatabase; +import android.util.Log; + +import jp.juggler.subwaytooter.App1; + +public class LogData { + static final String TAG = "SubwayTooter"; + + static final String table = "log"; + + public static final String COL_TIME = "t"; + public static final String COL_LEVEL = "l"; + public static final String COL_CATEGORY = "c"; + public static final String COL_MESSAGE = "m"; + + public static final int LEVEL_ERROR = 100; + public static final int LEVEL_WARNING = 200; + public static final int LEVEL_INFO = 300; + public static final int LEVEL_VERBOSE = 400; + public static final int LEVEL_DEBUG = 500; + public static final int LEVEL_HEARTBEAT = 600; + public static final int LEVEL_FLOOD = 700; + + + + public static void onDBCreate( SQLiteDatabase db ){ + db.execSQL( + "create table if not exists " + table + + "(_id INTEGER PRIMARY KEY" + + ",t integer not null" + + ",l integer not null" + + ",c text not null" + + ",m text not null" + + ")" + ); + db.execSQL( + "create index if not exists " + table + "_time on " + table + + "(t" + + ",l" + + ")" + ); + } + + public static void onDBUpgrade( SQLiteDatabase db, int v_old, int v_new ){ + + } + + public static long insert( ContentValues cv, long time, int level,String category, String message ){ + Log.d( TAG,category+": "+message); + try{ + cv.clear(); + cv.put( COL_TIME, time ); + cv.put( COL_LEVEL, level ); + cv.put( COL_MESSAGE, message ); + cv.put( COL_CATEGORY, category ); + return App1.getDB().insert( table, null, cv ); + }catch( Throwable ex ){ + ex.printStackTrace(); + return - 1L; + } + } + + public static String getLogLevelString( int level ){ + if( level >= LogData.LEVEL_FLOOD ){ + return "Flood"; + }else if( level >= LogData.LEVEL_HEARTBEAT ){ + return "HeartBeat"; + }else if( level >= LogData.LEVEL_DEBUG ){ + return "Debug"; + }else if( level >= LogData.LEVEL_VERBOSE ){ + return "Verbose"; + }else if( level >= LogData.LEVEL_INFO ){ + return "Info"; + }else if( level >= LogData.LEVEL_WARNING ){ + return "Warning"; + }else{ + return "Error"; + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.java b/app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.java new file mode 100644 index 00000000..1fe2fe90 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.java @@ -0,0 +1,125 @@ +package jp.juggler.subwaytooter.table; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +import jp.juggler.subwaytooter.App1; +import jp.juggler.subwaytooter.api.entity.TootAccount; +import jp.juggler.subwaytooter.util.LogCategory; + +public class SavedAccount extends TootAccount{ + + static final String table = "access_info"; + + static final String COL_HOST = "h"; + static final String COL_USER_MAIL = "um"; + static final String COL_ACCOUNT = "a"; + static final String COL_LOGIN_REQUIRED = "lr"; + + // login information + public String host; + public String user_mail; + public boolean login_required; + + + public static void onDBCreate( SQLiteDatabase db ){ + db.execSQL( + "create table if not exists " + table + + "(_id INTEGER PRIMARY KEY" + + ",h text not null" + + ",um text not null" + + ",a text not null" + + ",lr integer default 0" + + ")" + ); + db.execSQL( + "create unique index if not exists " + table + "_host on " + table + + "(h" + + ",um" + + ")" + ); + } + + public static void onDBUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){ + + } + + private static SavedAccount parse( LogCategory log, 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.host = cursor.getString( cursor.getColumnIndex( COL_HOST ) ); + dst.user_mail = cursor.getString( cursor.getColumnIndex( COL_USER_MAIL ) ); + dst.login_required = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_LOGIN_REQUIRED ) ) ); + } + return dst; + } + + + public static void save( LogCategory log,String instance, String user_mail, JSONObject data ){ + try{ + ContentValues cv = new ContentValues(); + cv.put( COL_HOST, instance ); + cv.put( COL_USER_MAIL, user_mail ); + cv.put( COL_ACCOUNT, data.toString() ); + App1.getDB().replace( table, null, cv ); + }catch( Throwable ex ){ + log.e( ex, "saveAccount failed." ); + } + } + + public static SavedAccount loadAccount( LogCategory log, String instance, String user_mail ){ + try{ + Cursor cursor = App1.getDB().query( table, null, "h=? and um=?", new String[]{ instance, user_mail }, null, null, null ); + try{ + if( cursor.moveToFirst() ){ + return parse( log,cursor ); + } + }finally{ + cursor.close(); + } + }catch( Throwable ex ){ + log.e( ex, "loadToken failed." ); + } + return null; + } + + public static ArrayList< SavedAccount > loadAccountList(LogCategory log){ + ArrayList< SavedAccount > result = new ArrayList<>(); + + try{ + Cursor cursor = App1.getDB().query( table, null, null, null, null, null, null ); + try{ + while( cursor.moveToNext() ){ + result.add( parse( log,cursor ) ); + } + return result; + }finally{ + cursor.close(); + } + }catch( Throwable ex ){ + log.e( ex, "loadAccountList failed." ); + } + return null; + } + + public static boolean hasAccount( LogCategory log,String instance, String user_mail ){ + return null != loadAccount( log,instance,user_mail ); + } + + public String getFullAcct(TootAccount who ){ + if( who== null || who.acct ==null ) return "@?"; + if( -1 != who.acct.indexOf( '@' ) ){ + return "@" + who.acct; + }else{ + return "@"+ who.acct +"@"+ this.host; + } + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/CancelChecker.java b/app/src/main/java/jp/juggler/subwaytooter/util/CancelChecker.java new file mode 100644 index 00000000..f29393ab --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/util/CancelChecker.java @@ -0,0 +1,5 @@ +package jp.juggler.subwaytooter.util; + +public interface CancelChecker { + boolean isCancelled(); +} \ No newline at end of file diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/HTTPClient.java b/app/src/main/java/jp/juggler/subwaytooter/util/HTTPClient.java new file mode 100644 index 00000000..670fa6cc --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/util/HTTPClient.java @@ -0,0 +1,692 @@ +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; + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/HTTPClientReceiver.java b/app/src/main/java/jp/juggler/subwaytooter/util/HTTPClientReceiver.java new file mode 100644 index 00000000..9c9dcb8c --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/util/HTTPClientReceiver.java @@ -0,0 +1,9 @@ +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); +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/LogCategory.java b/app/src/main/java/jp/juggler/subwaytooter/util/LogCategory.java new file mode 100644 index 00000000..96dbf4c5 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/util/LogCategory.java @@ -0,0 +1,152 @@ +package jp.juggler.subwaytooter.util; + +import android.content.ContentValues; +import android.content.res.Resources; + +import jp.juggler.subwaytooter.table.LogData; + +public class LogCategory { + + final ContentValues cv = new ContentValues(); + final String category; + + public LogCategory( String category ){ + this.category = category; + } + + @SuppressWarnings("unused") + public void addLog( int level, String message ){ + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), level, category, message ); + } + } + + @SuppressWarnings("unused") + public void e( String fmt, Object... args ){ + if( args.length > 0 ) fmt = String.format( fmt, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_ERROR, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void w( String fmt, Object... args ){ + if( args.length > 0 ) fmt = String.format( fmt, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_WARNING, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void i( String fmt, Object... args ){ + if( args.length > 0 ) fmt = String.format( fmt, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_INFO, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void v( String fmt, Object... args ){ + if( args.length > 0 ) fmt = String.format( fmt, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_VERBOSE, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void d( String fmt, Object... args ){ + if( args.length > 0 ) fmt = String.format( fmt, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_DEBUG, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void h( String fmt, Object... args ){ + if( args.length > 0 ) fmt = String.format( fmt, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_HEARTBEAT, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void f( String fmt, Object... args ){ + if( args.length > 0 ) fmt = String.format( fmt, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_FLOOD, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void e( Resources res, int string_id, Object... args ){ + String fmt = res.getString( string_id, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_ERROR, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void w( Resources res, int string_id, Object... args ){ + String fmt = res.getString( string_id, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_WARNING, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void i( Resources res, int string_id, Object... args ){ + String fmt = res.getString( string_id, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_INFO, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void v( Resources res, int string_id, Object... args ){ + String fmt = res.getString( string_id, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_VERBOSE, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void d( Resources res, int string_id, Object... args ){ + String fmt = res.getString( string_id, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_DEBUG, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void h( Resources res, int string_id, Object... args ){ + String fmt = res.getString( string_id, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_HEARTBEAT, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void f( Resources res, int string_id, Object... args ){ + String fmt = res.getString( string_id, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_FLOOD, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void e( Throwable ex, String fmt, Object... args ){ + if( args.length > 0 ) fmt = String.format( fmt, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_ERROR, category, fmt + String.format( ":%s %s", ex.getClass().getSimpleName(), ex.getMessage() ) ); + } + } + + @SuppressWarnings("unused") + public void e( Throwable ex, Resources res, int string_id, Object... args ){ + String fmt = res.getString( string_id, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_ERROR, category, fmt + String.format( ":%s %s", ex.getClass().getSimpleName(), ex.getMessage() ) ); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/Utils.java b/app/src/main/java/jp/juggler/subwaytooter/util/Utils.java new file mode 100644 index 00000000..9409b52a --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/util/Utils.java @@ -0,0 +1,787 @@ +package jp.juggler.subwaytooter.util; + +import android.annotation.SuppressLint; +import android.content.Context; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.reflect.Method; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; + +import android.content.res.Resources; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Looper; +import android.os.storage.StorageManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Base64; +import android.util.SparseBooleanArray; + +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.webkit.MimeTypeMap; +import android.widget.Toast; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import java.text.DecimalFormat; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +public class Utils { + + @SuppressLint("DefaultLocale") + public static String formatTimeDuration( long t ){ + StringBuilder sb = new StringBuilder(); + long n; + // day + n = t / 86400000L; + if( n > 0 ){ + sb.append( String.format( Locale.JAPAN, "%dd", n ) ); + t -= n * 86400000L; + } + // h + n = t / 3600000L; + if( n > 0 || sb.length() > 0 ){ + sb.append( String.format( Locale.JAPAN, "%dh", n ) ); + t -= n * 3600000L; + } + // m + n = t / 60000L; + if( n > 0 || sb.length() > 0 ){ + sb.append( String.format( Locale.JAPAN, "%dm", n ) ); + t -= n * 60000L; + } + // s + float f = t / 1000f; + sb.append( String.format( Locale.JAPAN, "%.03fs", f ) ); + + return sb.toString(); + } + + static DecimalFormat bytes_format = new DecimalFormat( "#,###" ); + + public static String formatBytes( long t ){ + return bytes_format.format( t ); + +// StringBuilder sb = new StringBuilder(); +// long n; +// // giga +// n = t / 1000000000L; +// if( n > 0 ){ +// sb.append( String.format( Locale.JAPAN, "%dg", n ) ); +// t -= n * 1000000000L; +// } +// // Mega +// n = t / 1000000L; +// if( sb.length() > 0 ){ +// sb.append( String.format( Locale.JAPAN, "%03dm", n ) ); +// t -= n * 1000000L; +// }else if( n > 0 ){ +// sb.append( String.format( Locale.JAPAN, "%dm", n ) ); +// t -= n * 1000000L; +// } +// // kilo +// n = t / 1000L; +// if( sb.length() > 0 ){ +// sb.append( String.format( Locale.JAPAN, "%03dk", n ) ); +// t -= n * 1000L; +// }else if( n > 0 ){ +// sb.append( String.format( Locale.JAPAN, "%dk", n ) ); +// t -= n * 1000L; +// } +// // remain +// if( sb.length() > 0 ){ +// sb.append( String.format( Locale.JAPAN, "%03d", t ) ); +// }else if( n > 0 ){ +// sb.append( String.format( Locale.JAPAN, "%d", t ) ); +// } +// +// return sb.toString(); + } + + // public static PendingIntent createAlarmPendingIntent( Context context ){ +// Intent i = new Intent( context.getApplicationContext(), Receiver1.class ); +// i.setAction( Receiver1.ACTION_ALARM ); +// return PendingIntent.getBroadcast( context.getApplicationContext(), 0, i, 0 ); +// } +// + // 文字列とバイト列の変換 + public static byte[] encodeUTF8( String str ){ + try{ + return str.getBytes( "UTF-8" ); + }catch( Throwable ex ){ + return null; // 入力がnullの場合のみ発生 + } + } + + // 文字列とバイト列の変換 + public static String decodeUTF8( byte[] data ){ + try{ + return new String( data, "UTF-8" ); + }catch( Throwable ex ){ + return null; // 入力がnullの場合のみ発生 + } + } + + // 文字列と整数の変換 + public static int parse_int( String v, int defval ){ + try{ + return Integer.parseInt( v, 10 ); + }catch( Throwable ex ){ + return defval; + } + } + + public static String optStringX( JSONObject src, String key){ + return src.isNull( key ) ? null : src.optString( key ); + } + + public static String optStringX( JSONArray src, int key){ + return src.isNull( key ) ? null : src.optString( key ); + } + + public static ArrayList< String > parseStringArray( LogCategory log, JSONArray array ){ + ArrayList< String > dst_list = new ArrayList<>( ); + if( array != null ){ + for(int i=0,ie=array.length();i> 4 ) & 15 ] ); + sb.append( hex[ ( b ) & 15 ] ); + } + + public static int hex2int( int c ){ + switch( c ){ + default: + return 0; + case '0': + return 0; + case '1': + return 1; + case '2': + return 2; + case '3': + return 3; + case '4': + return 4; + case '5': + return 5; + case '6': + return 6; + case '7': + return 7; + case '8': + return 8; + case '9': + return 9; + case 'a': + return 0xa; + case 'b': + return 0xb; + case 'c': + return 0xc; + case 'd': + return 0xd; + case 'e': + return 0xe; + case 'f': + return 0xf; + case 'A': + return 0xa; + case 'B': + return 0xb; + case 'C': + return 0xc; + case 'D': + return 0xd; + case 'E': + return 0xe; + case 'F': + return 0xf; + } + } + + // 16進ダンプ + public static String encodeHex( byte[] data ){ + if( data == null ) return null; + StringBuilder sb = new StringBuilder(); + for( byte b : data ){ + addHex( sb, b ); + } + return sb.toString(); + } + + public static byte[] encodeSHA256( byte[] src ){ + try{ + MessageDigest digest = MessageDigest.getInstance( "SHA-256" ); + digest.reset(); + return digest.digest( src ); + }catch( NoSuchAlgorithmException e1 ){ + return null; + } + } + + public static String encodeBase64Safe( byte[] src ){ + try{ + return Base64.encodeToString( src, Base64.URL_SAFE ); + }catch( Throwable ex ){ + return null; + } + } + + public static String url2name( String url ){ + if( url == null ) return null; + return encodeBase64Safe( encodeSHA256( encodeUTF8( url ) ) ); + } + +// public static String name2url(String entry) { +// if(entry==null) return null; +// byte[] b = new byte[entry.length()/2]; +// for(int i=0,ie=b.length;i taisaku_map = new HashMap<>(); + static SparseBooleanArray taisaku_map2 = new SparseBooleanArray(); + + static void _taisaku_add_string( String z, String h ){ + for( int i = 0, e = z.length() ; i < e ; ++ i ){ + char zc = z.charAt( i ); + taisaku_map.put( zc, "" + Character.toString( h.charAt( i ) ) ); + taisaku_map2.put( (int) zc, true ); + } + } + + static{ + taisaku_map = new HashMap<>(); + taisaku_map2 = new SparseBooleanArray(); + + // tilde,wave dash,horizontal ellipsis,minus sign + _taisaku_add_string( + "\u2073\u301C\u22EF\uFF0D" + , "\u007e\uFF5E\u2026\u2212" + ); + // zenkaku to hankaku + _taisaku_add_string( + " !”#$%&’()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}" + , " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}" + ); + + } + + static boolean isBadChar2( char c ){ + return c == 0xa || taisaku_map2.get( (int) c ); + } + + //! フォントによって全角文字が化けるので、その対策 + public static String font_taisaku( String text, boolean lf2br ){ + if( text == null ) return null; + int l = text.length(); + StringBuilder sb = new StringBuilder( l ); + if( ! lf2br ){ + for( int i = 0 ; i < l ; ++ i ){ + int start = i; + while( i < l && ! taisaku_map2.get( (int) text.charAt( i ) ) ) ++ i; + if( i > start ){ + sb.append( text.substring( start, i ) ); + if( i >= l ) break; + } + sb.append( taisaku_map.get( text.charAt( i ) ) ); + } + }else{ + for( int i = 0 ; i < l ; ++ i ){ + int start = i; + while( i < l && ! isBadChar2( text.charAt( i ) ) ) ++ i; + if( i > start ){ + sb.append( text.substring( start, i ) ); + if( i >= l ) break; + } + char c = text.charAt( i ); + if( c == 0xa ){ + sb.append( "
" ); + }else{ + sb.append( taisaku_map.get( c ) ); + } + } + } + return sb.toString(); + } + + //////////////////////////// + + public static String toLower( String from ){ + if( from == null ) return null; + return from.toLowerCase( Locale.US ); + } + + public static String toUpper( String from ){ + if( from == null ) return null; + return from.toUpperCase( Locale.US ); + } + + public static String getString( Bundle b, String key, String defval ){ + try{ + String v = b.getString( key ); + if( v != null ) return v; + }catch( Throwable ignored ){ + } + return defval; + } + + public static byte[] loadFile( File file ) throws IOException{ + int size = (int) file.length(); + byte[] data = new byte[ size ]; + FileInputStream in = new FileInputStream( file ); + try{ + int nRead = 0; + while( nRead < size ){ + int delta = in.read( data, nRead, size - nRead ); + if( delta <= 0 ) break; + } + return data; + }finally{ + try{ + in.close(); + }catch( Throwable ignored ){ + } + } + } + + public static String ellipsize( String t, int max ){ + return ( t.length() > max ? t.substring( 0, max - 1 ) + "…" : t ); + } + +// public static int getEnumStringId( String residPrefix, String name,Context context ) { +// name = residPrefix + name; +// try{ +// int iv = context.getResources().getIdentifier(name,"string",context.getPackageName() ); +// if( iv != 0 ) return iv; +// }catch(Throwable ex){ +// } +// log.e("missing resid for %s",name); +// return R.string.Dialog_Cancel; +// } + +// public static String getConnectionResultErrorMessage( ConnectionResult connectionResult ){ +// int code = connectionResult.getErrorCode(); +// String msg = connectionResult.getErrorMessage(); +// if( TextUtils.isEmpty( msg ) ){ +// switch( code ){ +// case ConnectionResult.SUCCESS: +// msg = "SUCCESS"; +// break; +// case ConnectionResult.SERVICE_MISSING: +// msg = "SERVICE_MISSING"; +// break; +// case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED: +// msg = "SERVICE_VERSION_UPDATE_REQUIRED"; +// break; +// case ConnectionResult.SERVICE_DISABLED: +// msg = "SERVICE_DISABLED"; +// break; +// case ConnectionResult.SIGN_IN_REQUIRED: +// msg = "SIGN_IN_REQUIRED"; +// break; +// case ConnectionResult.INVALID_ACCOUNT: +// msg = "INVALID_ACCOUNT"; +// break; +// case ConnectionResult.RESOLUTION_REQUIRED: +// msg = "RESOLUTION_REQUIRED"; +// break; +// case ConnectionResult.NETWORK_ERROR: +// msg = "NETWORK_ERROR"; +// break; +// case ConnectionResult.INTERNAL_ERROR: +// msg = "INTERNAL_ERROR"; +// break; +// case ConnectionResult.SERVICE_INVALID: +// msg = "SERVICE_INVALID"; +// break; +// case ConnectionResult.DEVELOPER_ERROR: +// msg = "DEVELOPER_ERROR"; +// break; +// case ConnectionResult.LICENSE_CHECK_FAILED: +// msg = "LICENSE_CHECK_FAILED"; +// break; +// case ConnectionResult.CANCELED: +// msg = "CANCELED"; +// break; +// case ConnectionResult.TIMEOUT: +// msg = "TIMEOUT"; +// break; +// case ConnectionResult.INTERRUPTED: +// msg = "INTERRUPTED"; +// break; +// case ConnectionResult.API_UNAVAILABLE: +// msg = "API_UNAVAILABLE"; +// break; +// case ConnectionResult.SIGN_IN_FAILED: +// msg = "SIGN_IN_FAILED"; +// break; +// case ConnectionResult.SERVICE_UPDATING: +// msg = "SERVICE_UPDATING"; +// break; +// case ConnectionResult.SERVICE_MISSING_PERMISSION: +// msg = "SERVICE_MISSING_PERMISSION"; +// break; +// case ConnectionResult.RESTRICTED_PROFILE: +// msg = "RESTRICTED_PROFILE"; +// break; +// +// } +// } +// return msg; +// } + +// public static String getConnectionSuspendedMessage( int i ){ +// switch( i ){ +// default: +// return "?"; +// case GoogleApiClient.ConnectionCallbacks.CAUSE_NETWORK_LOST: +// return "NETWORK_LOST"; +// case GoogleApiClient.ConnectionCallbacks.CAUSE_SERVICE_DISCONNECTED: +// return "SERVICE_DISCONNECTED"; +// } +// } + + static HashMap< String, String > mime_type_ex = null; + static final Object mime_type_ex_lock = new Object(); + + static String findMimeTypeEx( String ext ){ + synchronized( mime_type_ex_lock ){ + if( mime_type_ex == null ){ + HashMap< String, String > tmp = new HashMap<>(); + tmp.put( "BDM", "application/vnd.syncml.dm+wbxml" ); + tmp.put( "DAT", "" ); + tmp.put( "TID", "" ); + tmp.put( "js", "text/javascript" ); + tmp.put( "sh", "application/x-sh" ); + tmp.put( "lua", "text/x-lua" ); + mime_type_ex = tmp; + } + return mime_type_ex.get( ext ); + } + } + + public static final String MIME_TYPE_APPLICATION_OCTET_STREAM = "application/octet-stream"; + + public static String getMimeType( LogCategory log, String src ){ + String ext = MimeTypeMap.getFileExtensionFromUrl( src ); + if( ! TextUtils.isEmpty( ext ) ){ + ext = ext.toLowerCase( Locale.US ); + + // + String mime_type = MimeTypeMap.getSingleton().getMimeTypeFromExtension( ext ); + if( ! TextUtils.isEmpty( mime_type ) ) return mime_type; + + // + mime_type = findMimeTypeEx( ext ); + if( ! TextUtils.isEmpty( mime_type ) ) return mime_type; + + // 戻り値が空文字列の場合とnullの場合があり、空文字列の場合は既知でありログ出力しない + + if( mime_type == null && log != null ) + log.w( "getMimeType(): unknown file extension '%s'", ext ); + } + return MIME_TYPE_APPLICATION_OCTET_STREAM; + } + + + + static class FileInfo { + + Uri uri; + String mime_type; + + FileInfo( String any_uri ){ + if( any_uri == null ) return; + + if( any_uri.startsWith( "/" ) ){ + uri = Uri.fromFile( new File( any_uri ) ); + }else{ + uri = Uri.parse( any_uri ); + } + + String ext = MimeTypeMap.getFileExtensionFromUrl( any_uri ); + if( ext != null ){ + mime_type = MimeTypeMap.getSingleton().getMimeTypeFromExtension( ext.toLowerCase() ); + } + } + } + + static + @NonNull + Map< String, String > getSecondaryStorageVolumesMap( Context context ){ + Map< String, String > result = new HashMap<>(); + try{ + + StorageManager sm = (StorageManager) context.getApplicationContext().getSystemService( Context.STORAGE_SERVICE ); + + // SDカードスロットのある7.0端末が手元にないから検証できない +// if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ){ +// for(StorageVolume volume : sm.getStorageVolumes() ){ +// // String path = volume.getPath(); +// String state = volume.getState(); +// +// } +// } + + Method getVolumeList = sm.getClass().getMethod( "getVolumeList" ); + Object[] volumes = (Object[]) getVolumeList.invoke( sm ); + // + for( Object volume : volumes ){ + Class< ? > volume_clazz = volume.getClass(); + + String path = (String) volume_clazz.getMethod( "getPath" ).invoke( volume ); + String state = (String) volume_clazz.getMethod( "getState" ).invoke( volume ); + if( ! TextUtils.isEmpty( path ) && "mounted".equals( state ) ){ + // + boolean isPrimary = (Boolean) volume_clazz.getMethod( "isPrimary" ).invoke( volume ); + if( isPrimary ) result.put( "primary", path ); + // + String uuid = (String) volume_clazz.getMethod( "getUuid" ).invoke( volume ); + if( ! TextUtils.isEmpty( uuid ) ) result.put( uuid, path ); + } + } + }catch( Throwable ex ){ + ex.printStackTrace(); + } + return result; + } + + public static String toCamelCase( String src ){ + StringBuilder sb = new StringBuilder(); + for( String s : src.split( "_" ) ){ + if( TextUtils.isEmpty( s ) ) continue; + sb.append( Character.toUpperCase( s.charAt( 0 ) ) ); + sb.append( s.substring( 1, s.length() ).toLowerCase() ); + } + return sb.toString(); + } + + private static DocumentBuilder xml_builder; + + public static Element parseXml( byte[] src ){ + if( xml_builder == null ){ + try{ + xml_builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + }catch( Throwable ex ){ + ex.printStackTrace(); + return null; + } + } + try{ + return xml_builder.parse( new ByteArrayInputStream( src ) ).getDocumentElement(); + }catch( Throwable ex ){ + ex.printStackTrace(); + return null; + } + } + + public static String getAttribute( NamedNodeMap attr_map, String name, String defval ){ + Node node = attr_map.getNamedItem( name ); + if( node != null ) return node.getNodeValue(); + return defval; + } + + @SuppressWarnings("unused") + public static String formatError( Throwable ex, String fmt, Object... args ){ + if( args.length > 0 ) fmt = String.format( fmt, args ); + return fmt + String.format( " :%s %s", ex.getClass().getSimpleName(), ex.getMessage() ); + } + + @SuppressWarnings("unused") + public static String formatError( Throwable ex, Resources resources, int string_id, Object... args ){ + return resources.getString( string_id, args ) + String.format( " :%s %s", ex.getClass().getSimpleName(), ex.getMessage() ); + } + + public static void runOnMainThread( @NonNull Runnable proc ){ + if( Looper.getMainLooper().getThread() == Thread.currentThread() ){ + proc.run(); + }else{ + new Handler( Looper.getMainLooper() ).post( proc ); + } + } + + public static void showToast( final Context context, final boolean bLong, final String fmt, final Object... args ){ + runOnMainThread( new Runnable() { + @Override + public void run(){ + Toast.makeText( + context + , ( args.length == 0 ? fmt : String.format( fmt, args ) ) + , bLong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT + ).show(); + } + } ); + } + + public static void showToast( final Context context, final Throwable ex, final String fmt, final Object... args ){ + runOnMainThread( new Runnable() { + @Override + public void run(){ + Toast.makeText( + context + , formatError( ex, fmt, args ) + , Toast.LENGTH_LONG + ).show(); + } + } ); + } + + public static void showToast( final Context context, final boolean bLong, final int string_id, final Object... args ){ + runOnMainThread( new Runnable() { + @Override + public void run(){ + + Toast.makeText( + context + , context.getString( string_id, args ) + , bLong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT + ).show(); + } + } ); + } + + public static void showToast( final Context context, final Throwable ex, final int string_id, final Object... args ){ + runOnMainThread( new Runnable() { + @Override + public void run(){ + Toast.makeText( + context + , formatError( ex, context.getResources(), string_id, args ) + , Toast.LENGTH_LONG + ).show(); + } + } ); + } + + public static boolean isExternalStorageDocument( Uri uri ){ + return "com.android.externalstorage.documents".equals( uri.getAuthority() ); + } + + private static final String PATH_TREE = "tree"; + private static final String PATH_DOCUMENT = "document"; + + public static String getDocumentId( Uri documentUri ){ + final List< String > paths = documentUri.getPathSegments(); + if( paths.size() >= 2 && PATH_DOCUMENT.equals( paths.get( 0 ) ) ){ + // document + return paths.get( 1 ); + } + if( paths.size() >= 4 && PATH_TREE.equals( paths.get( 0 ) ) + && PATH_DOCUMENT.equals( paths.get( 2 ) ) ){ + // document in tree + return paths.get( 3 ); + } + if( paths.size() >= 2 && PATH_TREE.equals( paths.get( 0 ) ) ){ + // tree + return paths.get( 1 ); + } + throw new IllegalArgumentException( "Invalid URI: " + documentUri ); + } + + public static + @Nullable + File getFile( Context context, @NonNull String path ){ + try{ + if( path.startsWith( "/" ) ) return new File( path ); + Uri uri = Uri.parse( path ); + if( "file".equals( uri.getScheme() ) ) return new File( uri.getPath() ); + + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT ){ + if( isExternalStorageDocument( uri ) ){ + try{ + final String docId = getDocumentId( uri ); + final String[] split = docId.split( ":" ); + if( split.length >= 2 ){ + final String uuid = split[ 0 ]; + if( "primary".equalsIgnoreCase( uuid ) ){ + return new File( Environment.getExternalStorageDirectory() + "/" + split[ 1 ] ); + }else{ + Map< String, String > volume_map = Utils.getSecondaryStorageVolumesMap( context ); + String volume_path = volume_map.get( uuid ); + if( volume_path != null ){ + return new File( volume_path + "/" + split[ 1 ] ); + } + } + } + }catch( Throwable ex2 ){ + ex2.printStackTrace(); + } + } + } + // MediaStore Uri + Cursor cursor = context.getContentResolver().query( uri, null, null, null, null ); + if( cursor != null ){ + try{ + if( cursor.moveToFirst() ){ + int col_count = cursor.getColumnCount(); + for( int i = 0 ; i < col_count ; ++ i ){ + int type = cursor.getType( i ); + if( type != Cursor.FIELD_TYPE_STRING ) continue; + String name = cursor.getColumnName( i ); + String value = cursor.isNull( i ) ? null : cursor.getString( i ); + if( ! TextUtils.isEmpty( value ) ){ + if( "filePath".equals( name ) ){ + return new File( value ); + } + } + } + } + }finally{ + cursor.close(); + } + } + }catch( Throwable ex ){ + ex.printStackTrace(); + } + return null; + } + +} diff --git a/app/src/main/res/drawable-hdpi/black_close.png b/app/src/main/res/drawable-hdpi/black_close.png new file mode 100644 index 00000000..0cd254b3 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/black_close.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_boost.png b/app/src/main/res/drawable-hdpi/btn_boost.png new file mode 100644 index 00000000..61a43f36 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_boost.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_favourite.png b/app/src/main/res/drawable-hdpi/btn_favourite.png new file mode 100644 index 00000000..315a0b06 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_favourite.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_federate_tl.png b/app/src/main/res/drawable-hdpi/btn_federate_tl.png new file mode 100644 index 00000000..1fcfc4a2 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_federate_tl.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_follow.png b/app/src/main/res/drawable-hdpi/btn_follow.png new file mode 100644 index 00000000..a50def11 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_follow.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_home.png b/app/src/main/res/drawable-hdpi/btn_home.png new file mode 100644 index 00000000..855ff15d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_home.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_local_tl.png b/app/src/main/res/drawable-hdpi/btn_local_tl.png new file mode 100644 index 00000000..281a60a1 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_local_tl.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_more.png b/app/src/main/res/drawable-hdpi/btn_more.png new file mode 100644 index 00000000..44ab0560 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_more.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_notification.png b/app/src/main/res/drawable-hdpi/btn_notification.png new file mode 100644 index 00000000..021bcf45 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_notification.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_refresh.png b/app/src/main/res/drawable-hdpi/btn_refresh.png new file mode 100644 index 00000000..0afc96f5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_refresh.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_reload.png b/app/src/main/res/drawable-hdpi/btn_reload.png new file mode 100644 index 00000000..04b58201 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_reload.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_reply.png b/app/src/main/res/drawable-hdpi/btn_reply.png new file mode 100644 index 00000000..647286e5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_reply.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_report.png b/app/src/main/res/drawable-hdpi/btn_report.png new file mode 100644 index 00000000..3b40c0e6 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_report.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_statuses.png b/app/src/main/res/drawable-hdpi/btn_statuses.png new file mode 100644 index 00000000..c62cd0c9 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_statuses.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_account_add.png b/app/src/main/res/drawable-hdpi/ic_account_add.png new file mode 100644 index 00000000..a50def11 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_account_add.png differ diff --git a/app/src/main/res/drawable-mdpi/black_close.png b/app/src/main/res/drawable-mdpi/black_close.png new file mode 100644 index 00000000..320daff7 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/black_close.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_boost.png b/app/src/main/res/drawable-mdpi/btn_boost.png new file mode 100644 index 00000000..f9a1df18 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_boost.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_favourite.png b/app/src/main/res/drawable-mdpi/btn_favourite.png new file mode 100644 index 00000000..f690ae44 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_favourite.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_federate_tl.png b/app/src/main/res/drawable-mdpi/btn_federate_tl.png new file mode 100644 index 00000000..dd487f62 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_federate_tl.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_follow.png b/app/src/main/res/drawable-mdpi/btn_follow.png new file mode 100644 index 00000000..569de966 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_follow.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_home.png b/app/src/main/res/drawable-mdpi/btn_home.png new file mode 100644 index 00000000..852b5f7b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_home.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_local_tl.png b/app/src/main/res/drawable-mdpi/btn_local_tl.png new file mode 100644 index 00000000..de625fda Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_local_tl.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_more.png b/app/src/main/res/drawable-mdpi/btn_more.png new file mode 100644 index 00000000..cef6b9b4 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_more.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_notification.png b/app/src/main/res/drawable-mdpi/btn_notification.png new file mode 100644 index 00000000..20a2088e Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_notification.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_refresh.png b/app/src/main/res/drawable-mdpi/btn_refresh.png new file mode 100644 index 00000000..4cda6177 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_refresh.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_reload.png b/app/src/main/res/drawable-mdpi/btn_reload.png new file mode 100644 index 00000000..ab658ad2 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_reload.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_reply.png b/app/src/main/res/drawable-mdpi/btn_reply.png new file mode 100644 index 00000000..b6f1e62c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_reply.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_report.png b/app/src/main/res/drawable-mdpi/btn_report.png new file mode 100644 index 00000000..e7b68df8 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_report.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_statuses.png b/app/src/main/res/drawable-mdpi/btn_statuses.png new file mode 100644 index 00000000..bf952aaa Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_statuses.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_account_add.png b/app/src/main/res/drawable-mdpi/ic_account_add.png new file mode 100644 index 00000000..569de966 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_account_add.png differ diff --git a/app/src/main/res/drawable-xhdpi/black_close.png b/app/src/main/res/drawable-xhdpi/black_close.png new file mode 100644 index 00000000..658a422f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/black_close.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_boost.png b/app/src/main/res/drawable-xhdpi/btn_boost.png new file mode 100644 index 00000000..a6da9683 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_boost.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_favourite.png b/app/src/main/res/drawable-xhdpi/btn_favourite.png new file mode 100644 index 00000000..0a32d047 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_favourite.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_federate_tl.png b/app/src/main/res/drawable-xhdpi/btn_federate_tl.png new file mode 100644 index 00000000..0e0682fd Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_federate_tl.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_follow.png b/app/src/main/res/drawable-xhdpi/btn_follow.png new file mode 100644 index 00000000..6bca480e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_follow.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_home.png b/app/src/main/res/drawable-xhdpi/btn_home.png new file mode 100644 index 00000000..82d1b916 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_home.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_local_tl.png b/app/src/main/res/drawable-xhdpi/btn_local_tl.png new file mode 100644 index 00000000..f17fb702 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_local_tl.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_more.png b/app/src/main/res/drawable-xhdpi/btn_more.png new file mode 100644 index 00000000..44b7c614 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_more.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_notification.png b/app/src/main/res/drawable-xhdpi/btn_notification.png new file mode 100644 index 00000000..1a1b3a3e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_notification.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_refresh.png b/app/src/main/res/drawable-xhdpi/btn_refresh.png new file mode 100644 index 00000000..749a9eb4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_refresh.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_reload.png b/app/src/main/res/drawable-xhdpi/btn_reload.png new file mode 100644 index 00000000..b75e28c7 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_reload.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_reply.png b/app/src/main/res/drawable-xhdpi/btn_reply.png new file mode 100644 index 00000000..00a62542 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_reply.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_report.png b/app/src/main/res/drawable-xhdpi/btn_report.png new file mode 100644 index 00000000..4f0df604 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_report.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_statuses.png b/app/src/main/res/drawable-xhdpi/btn_statuses.png new file mode 100644 index 00000000..4011f4e5 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_statuses.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_account_add.png b/app/src/main/res/drawable-xhdpi/ic_account_add.png new file mode 100644 index 00000000..6bca480e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_account_add.png differ diff --git a/app/src/main/res/drawable-xxhdpi/black_close.png b/app/src/main/res/drawable-xxhdpi/black_close.png new file mode 100644 index 00000000..9932e90d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/black_close.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_boost.png b/app/src/main/res/drawable-xxhdpi/btn_boost.png new file mode 100644 index 00000000..5a2a0c1a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_boost.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_favourite.png b/app/src/main/res/drawable-xxhdpi/btn_favourite.png new file mode 100644 index 00000000..bfd9ad2f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_favourite.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_federate_tl.png b/app/src/main/res/drawable-xxhdpi/btn_federate_tl.png new file mode 100644 index 00000000..fd4554eb Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_federate_tl.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_follow.png b/app/src/main/res/drawable-xxhdpi/btn_follow.png new file mode 100644 index 00000000..ad140127 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_follow.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_home.png b/app/src/main/res/drawable-xxhdpi/btn_home.png new file mode 100644 index 00000000..1b32b28e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_home.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_local_tl.png b/app/src/main/res/drawable-xxhdpi/btn_local_tl.png new file mode 100644 index 00000000..3ade7c47 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_local_tl.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_more.png b/app/src/main/res/drawable-xxhdpi/btn_more.png new file mode 100644 index 00000000..fce82fd3 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_more.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_notification.png b/app/src/main/res/drawable-xxhdpi/btn_notification.png new file mode 100644 index 00000000..67f5ff83 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_notification.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_refresh.png b/app/src/main/res/drawable-xxhdpi/btn_refresh.png new file mode 100644 index 00000000..2b3f8534 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_refresh.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_reload.png b/app/src/main/res/drawable-xxhdpi/btn_reload.png new file mode 100644 index 00000000..315a2533 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_reload.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_reply.png b/app/src/main/res/drawable-xxhdpi/btn_reply.png new file mode 100644 index 00000000..31bcefbc Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_reply.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_report.png b/app/src/main/res/drawable-xxhdpi/btn_report.png new file mode 100644 index 00000000..992a50fb Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_report.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_statuses.png b/app/src/main/res/drawable-xxhdpi/btn_statuses.png new file mode 100644 index 00000000..a30bacd9 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_statuses.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_account_add.png b/app/src/main/res/drawable-xxhdpi/ic_account_add.png new file mode 100644 index 00000000..ad140127 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_account_add.png differ diff --git a/app/src/main/res/drawable/ic_menu_camera.xml b/app/src/main/res/drawable/ic_menu_camera.xml new file mode 100644 index 00000000..7d1c5833 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_camera.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_menu_gallery.xml b/app/src/main/res/drawable/ic_menu_gallery.xml new file mode 100644 index 00000000..2f2ca2aa --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_gallery.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_manage.xml b/app/src/main/res/drawable/ic_menu_manage.xml new file mode 100644 index 00000000..065d9fa8 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_manage.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_menu_send.xml b/app/src/main/res/drawable/ic_menu_send.xml new file mode 100644 index 00000000..a5546577 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_send.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_share.xml b/app/src/main/res/drawable/ic_menu_share.xml new file mode 100644 index 00000000..8151b38e --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_share.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_slideshow.xml b/app/src/main/res/drawable/ic_menu_slideshow.xml new file mode 100644 index 00000000..e7509918 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_slideshow.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/side_nav_bar.xml b/app/src/main/res/drawable/side_nav_bar.xml new file mode 100644 index 00000000..c1dff26b --- /dev/null +++ b/app/src/main/res/drawable/side_nav_bar.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/act_main.xml b/app/src/main/res/layout/act_main.xml new file mode 100644 index 00000000..7266a595 --- /dev/null +++ b/app/src/main/res/layout/act_main.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dlg_account_add.xml b/app/src/main/res/layout/dlg_account_add.xml new file mode 100644 index 00000000..cf53804b --- /dev/null +++ b/app/src/main/res/layout/dlg_account_add.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + +