initial import
|
@ -0,0 +1,9 @@
|
|||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<resourceExtensions />
|
||||
<wildcardResourcePatterns>
|
||||
<entry name="!?*.java" />
|
||||
<entry name="!?*.form" />
|
||||
<entry name="!?*.class" />
|
||||
<entry name="!?*.groovy" />
|
||||
<entry name="!?*.scala" />
|
||||
<entry name="!?*.flex" />
|
||||
<entry name="!?*.kt" />
|
||||
<entry name="!?*.clj" />
|
||||
<entry name="!?*.aj" />
|
||||
</wildcardResourcePatterns>
|
||||
<annotationProcessing>
|
||||
<profile default="true" name="Default" enabled="false">
|
||||
<processorPath useClasspath="true" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,3 @@
|
|||
<component name="CopyrightManager">
|
||||
<settings default="" />
|
||||
</component>
|
|
@ -0,0 +1,12 @@
|
|||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="tateisu">
|
||||
<words>
|
||||
<w>favourited</w>
|
||||
<w>reblog</w>
|
||||
<w>reblogged</w>
|
||||
<w>reblogs</w>
|
||||
<w>subwaytooter</w>
|
||||
<w>timelines</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="PROJECT" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,10 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="LoggerInitializedWithForeignClass" enabled="false" level="WARNING" enabled_by_default="false">
|
||||
<option name="loggerClassName" value="org.apache.log4j.Logger,org.slf4j.LoggerFactory,org.apache.commons.logging.LogFactory,java.util.logging.Logger" />
|
||||
<option name="loggerFactoryMethodName" value="getLogger,getLogger,getLog,getLogger" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="TryFinallyCanBeTryWithResources" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
</profile>
|
||||
</component>
|
|
@ -0,0 +1,7 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="PROJECT_PROFILE" value="Project Default" />
|
||||
<option name="USE_PROJECT_PROFILE" value="true" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="EntryPointsManager">
|
||||
<entry_points version="2.0" />
|
||||
</component>
|
||||
<component name="NullableNotNullManager">
|
||||
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
|
||||
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
|
||||
<option name="myNullables">
|
||||
<value>
|
||||
<list size="4">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
|
||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
|
||||
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
|
||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
<option name="myNotNulls">
|
||||
<value>
|
||||
<list size="4">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
|
||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
|
||||
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
|
||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
|
||||
<OptionsSetting value="true" id="Add" />
|
||||
<OptionsSetting value="true" id="Remove" />
|
||||
<OptionsSetting value="true" id="Checkout" />
|
||||
<OptionsSetting value="true" id="Update" />
|
||||
<OptionsSetting value="true" id="Status" />
|
||||
<OptionsSetting value="true" id="Edit" />
|
||||
<ConfirmationsSetting value="0" id="Add" />
|
||||
<ConfirmationsSetting value="0" id="Remove" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="true" assert-keyword="true" jdk-15="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/SubwayTooter.iml" filepath="$PROJECT_DIR$/SubwayTooter.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -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'
|
||||
}
|
|
@ -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
|
|
@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@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() );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="jp.juggler.subwaytooter">
|
||||
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:name=".App1"
|
||||
>
|
||||
|
||||
<activity
|
||||
android:name=".ActMain"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -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 );
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 );
|
||||
}
|
||||
}
|
|
@ -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> column_list = new ArrayList<>();
|
||||
final SparseArray<ColumnViewHolder> 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 );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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<CharSequence> title_list = new ArrayList<>();
|
||||
protected final ArrayList<Integer> layout_id_list = new ArrayList<>();
|
||||
protected final ArrayList<Class<? extends PageViewHolder>> holder_class_list = new ArrayList<>();
|
||||
protected final SparseArray<PageViewHolder> holder_list = new SparseArray<>();
|
||||
|
||||
public int addPage( CharSequence title, int layout_id, Class<? extends PageViewHolder> 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> 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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" ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String> 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 ) );
|
||||
}
|
||||
|
||||
}
|
|
@ -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<SavedAccount > 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<ie;++i){
|
||||
SavedAccount ai = account_list.get(i);
|
||||
caption_list[i] = ai.acct + ( ai.login_required ? " "+ activity.getString( R.string.login_required) : "");
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(activity)
|
||||
.setNegativeButton( R.string.cancel,null )
|
||||
.setItems( caption_list, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick( DialogInterface dialog, int which ){
|
||||
if( which >= 0 && which < account_list.size() ){
|
||||
callback.onAccountPicked(account_list.get(which));
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
} )
|
||||
.setTitle( R.string.account_pick )
|
||||
.show();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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." );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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." );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package jp.juggler.subwaytooter.util;
|
||||
|
||||
public interface CancelChecker {
|
||||
boolean isCancelled();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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() ) );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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<ie;++i){
|
||||
String sv = Utils.optStringX(array,i);
|
||||
dst_list.add( sv );
|
||||
}
|
||||
}
|
||||
return dst_list;
|
||||
}
|
||||
|
||||
static final char[] hex = new char[]{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
|
||||
|
||||
public static void addHex( StringBuilder sb, byte b ){
|
||||
sb.append( hex[ ( b >> 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<ie;++i){
|
||||
// b[i]= (byte)((hex2int(entry.charAt(i*2))<<4)| hex2int(entry.charAt(i*2+1)));
|
||||
// }
|
||||
// return decodeUTF8(b);
|
||||
// }
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
|
||||
// MD5ハッシュの作成
|
||||
public static String digestMD5( String s ){
|
||||
if( s == null ) return null;
|
||||
try{
|
||||
MessageDigest md = MessageDigest.getInstance( "MD5" );
|
||||
md.reset();
|
||||
return encodeHex( md.digest( s.getBytes( "UTF-8" ) ) );
|
||||
}catch( Throwable ex ){
|
||||
ex.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////
|
||||
|
||||
static HashMap< Character, String > 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( "<br/>" );
|
||||
}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;
|
||||
}
|
||||
|
||||
}
|
After Width: | Height: | Size: 372 B |
After Width: | Height: | Size: 420 B |
After Width: | Height: | Size: 719 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 583 B |
After Width: | Height: | Size: 448 B |
After Width: | Height: | Size: 763 B |
After Width: | Height: | Size: 241 B |
After Width: | Height: | Size: 357 B |
After Width: | Height: | Size: 779 B |
After Width: | Height: | Size: 776 B |
After Width: | Height: | Size: 506 B |
After Width: | Height: | Size: 364 B |
After Width: | Height: | Size: 500 B |
After Width: | Height: | Size: 583 B |
After Width: | Height: | Size: 257 B |
After Width: | Height: | Size: 278 B |
After Width: | Height: | Size: 478 B |
After Width: | Height: | Size: 798 B |
After Width: | Height: | Size: 404 B |
After Width: | Height: | Size: 315 B |
After Width: | Height: | Size: 508 B |
After Width: | Height: | Size: 175 B |
After Width: | Height: | Size: 264 B |
After Width: | Height: | Size: 507 B |
After Width: | Height: | Size: 485 B |
After Width: | Height: | Size: 336 B |
After Width: | Height: | Size: 258 B |
After Width: | Height: | Size: 362 B |
After Width: | Height: | Size: 404 B |
After Width: | Height: | Size: 406 B |
After Width: | Height: | Size: 396 B |
After Width: | Height: | Size: 918 B |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 733 B |
After Width: | Height: | Size: 449 B |
After Width: | Height: | Size: 910 B |
After Width: | Height: | Size: 307 B |
After Width: | Height: | Size: 394 B |
After Width: | Height: | Size: 941 B |
After Width: | Height: | Size: 911 B |
After Width: | Height: | Size: 587 B |
After Width: | Height: | Size: 415 B |
After Width: | Height: | Size: 655 B |
After Width: | Height: | Size: 733 B |
After Width: | Height: | Size: 663 B |
After Width: | Height: | Size: 675 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 826 B |
After Width: | Height: | Size: 1.5 KiB |