From d0e8e55e695cd49bbe7bf410386a03a38972828d Mon Sep 17 00:00:00 2001 From: tateisu Date: Fri, 21 Apr 2017 01:23:59 +0900 Subject: [PATCH] initial import --- .gitignore | 9 + .idea/compiler.xml | 22 + .idea/copyright/profiles_settings.xml | 3 + .idea/dictionaries/tateisu.xml | 12 + .idea/encodings.xml | 6 + .idea/gradle.xml | 18 + .idea/inspectionProfiles/Project_Default.xml | 10 + .../inspectionProfiles/profiles_settings.xml | 7 + .idea/misc.xml | 46 + .idea/modules.xml | 9 + .idea/runConfigurations.xml | 12 + app/.gitignore | 1 + app/build.gradle | 32 + app/proguard-rules.pro | 25 + .../subwaytooter/ExampleInstrumentedTest.java | 26 + app/src/main/AndroidManifest.xml | 31 + .../java/jp/juggler/subwaytooter/ActMain.java | 259 ++++++ .../java/jp/juggler/subwaytooter/App1.java | 61 ++ .../java/jp/juggler/subwaytooter/Column.java | 282 +++++++ .../subwaytooter/ColumnPagerAdapter.java | 104 +++ .../subwaytooter/ColumnViewHolder.java | 326 ++++++++ .../subwaytooter/PagerAdapterBase.java | 123 +++ .../subwaytooter/api/TootApiClient.java | 190 +++++ .../subwaytooter/api/TootApiResult.java | 25 + .../subwaytooter/api/entity/TootAccount.java | 109 +++ .../api/entity/TootApplication.java | 24 + .../api/entity/TootAttachment.java | 67 ++ .../subwaytooter/api/entity/TootCard.java | 39 + .../subwaytooter/api/entity/TootContext.java | 28 + .../subwaytooter/api/entity/TootError.java | 26 + .../subwaytooter/api/entity/TootInstance.java | 37 + .../subwaytooter/api/entity/TootMention.java | 57 ++ .../api/entity/TootNotification.java | 74 ++ .../api/entity/TootRelationShip.java | 45 + .../subwaytooter/api/entity/TootReport.java | 49 ++ .../subwaytooter/api/entity/TootResults.java | 34 + .../subwaytooter/api/entity/TootStatus.java | 160 ++++ .../subwaytooter/dialog/AccountPicker.java | 54 ++ .../subwaytooter/dialog/LoginForm.java | 61 ++ .../subwaytooter/table/AccessToken.java | 78 ++ .../subwaytooter/table/ClientInfo.java | 64 ++ .../juggler/subwaytooter/table/LogData.java | 84 ++ .../subwaytooter/table/SavedAccount.java | 125 +++ .../subwaytooter/util/CancelChecker.java | 5 + .../juggler/subwaytooter/util/HTTPClient.java | 692 +++++++++++++++ .../subwaytooter/util/HTTPClientReceiver.java | 9 + .../subwaytooter/util/LogCategory.java | 152 ++++ .../jp/juggler/subwaytooter/util/Utils.java | 787 ++++++++++++++++++ .../main/res/drawable-hdpi/black_close.png | Bin 0 -> 372 bytes app/src/main/res/drawable-hdpi/btn_boost.png | Bin 0 -> 420 bytes .../main/res/drawable-hdpi/btn_favourite.png | Bin 0 -> 719 bytes .../res/drawable-hdpi/btn_federate_tl.png | Bin 0 -> 1038 bytes app/src/main/res/drawable-hdpi/btn_follow.png | Bin 0 -> 583 bytes app/src/main/res/drawable-hdpi/btn_home.png | Bin 0 -> 448 bytes .../main/res/drawable-hdpi/btn_local_tl.png | Bin 0 -> 763 bytes app/src/main/res/drawable-hdpi/btn_more.png | Bin 0 -> 241 bytes .../res/drawable-hdpi/btn_notification.png | Bin 0 -> 357 bytes .../main/res/drawable-hdpi/btn_refresh.png | Bin 0 -> 779 bytes app/src/main/res/drawable-hdpi/btn_reload.png | Bin 0 -> 776 bytes app/src/main/res/drawable-hdpi/btn_reply.png | Bin 0 -> 506 bytes app/src/main/res/drawable-hdpi/btn_report.png | Bin 0 -> 364 bytes .../main/res/drawable-hdpi/btn_statuses.png | Bin 0 -> 500 bytes .../main/res/drawable-hdpi/ic_account_add.png | Bin 0 -> 583 bytes .../main/res/drawable-mdpi/black_close.png | Bin 0 -> 257 bytes app/src/main/res/drawable-mdpi/btn_boost.png | Bin 0 -> 278 bytes .../main/res/drawable-mdpi/btn_favourite.png | Bin 0 -> 478 bytes .../res/drawable-mdpi/btn_federate_tl.png | Bin 0 -> 798 bytes app/src/main/res/drawable-mdpi/btn_follow.png | Bin 0 -> 404 bytes app/src/main/res/drawable-mdpi/btn_home.png | Bin 0 -> 315 bytes .../main/res/drawable-mdpi/btn_local_tl.png | Bin 0 -> 508 bytes app/src/main/res/drawable-mdpi/btn_more.png | Bin 0 -> 175 bytes .../res/drawable-mdpi/btn_notification.png | Bin 0 -> 264 bytes .../main/res/drawable-mdpi/btn_refresh.png | Bin 0 -> 507 bytes app/src/main/res/drawable-mdpi/btn_reload.png | Bin 0 -> 485 bytes app/src/main/res/drawable-mdpi/btn_reply.png | Bin 0 -> 336 bytes app/src/main/res/drawable-mdpi/btn_report.png | Bin 0 -> 258 bytes .../main/res/drawable-mdpi/btn_statuses.png | Bin 0 -> 362 bytes .../main/res/drawable-mdpi/ic_account_add.png | Bin 0 -> 404 bytes .../main/res/drawable-xhdpi/black_close.png | Bin 0 -> 406 bytes app/src/main/res/drawable-xhdpi/btn_boost.png | Bin 0 -> 396 bytes .../main/res/drawable-xhdpi/btn_favourite.png | Bin 0 -> 918 bytes .../res/drawable-xhdpi/btn_federate_tl.png | Bin 0 -> 1518 bytes .../main/res/drawable-xhdpi/btn_follow.png | Bin 0 -> 733 bytes app/src/main/res/drawable-xhdpi/btn_home.png | Bin 0 -> 449 bytes .../main/res/drawable-xhdpi/btn_local_tl.png | Bin 0 -> 910 bytes app/src/main/res/drawable-xhdpi/btn_more.png | Bin 0 -> 307 bytes .../res/drawable-xhdpi/btn_notification.png | Bin 0 -> 394 bytes .../main/res/drawable-xhdpi/btn_refresh.png | Bin 0 -> 941 bytes .../main/res/drawable-xhdpi/btn_reload.png | Bin 0 -> 911 bytes app/src/main/res/drawable-xhdpi/btn_reply.png | Bin 0 -> 587 bytes .../main/res/drawable-xhdpi/btn_report.png | Bin 0 -> 415 bytes .../main/res/drawable-xhdpi/btn_statuses.png | Bin 0 -> 655 bytes .../res/drawable-xhdpi/ic_account_add.png | Bin 0 -> 733 bytes .../main/res/drawable-xxhdpi/black_close.png | Bin 0 -> 663 bytes .../main/res/drawable-xxhdpi/btn_boost.png | Bin 0 -> 675 bytes .../res/drawable-xxhdpi/btn_favourite.png | Bin 0 -> 1460 bytes .../res/drawable-xxhdpi/btn_federate_tl.png | Bin 0 -> 2409 bytes .../main/res/drawable-xxhdpi/btn_follow.png | Bin 0 -> 1125 bytes app/src/main/res/drawable-xxhdpi/btn_home.png | Bin 0 -> 826 bytes .../main/res/drawable-xxhdpi/btn_local_tl.png | Bin 0 -> 1543 bytes app/src/main/res/drawable-xxhdpi/btn_more.png | Bin 0 -> 542 bytes .../res/drawable-xxhdpi/btn_notification.png | Bin 0 -> 647 bytes .../main/res/drawable-xxhdpi/btn_refresh.png | Bin 0 -> 1573 bytes .../main/res/drawable-xxhdpi/btn_reload.png | Bin 0 -> 1521 bytes .../main/res/drawable-xxhdpi/btn_reply.png | Bin 0 -> 966 bytes .../main/res/drawable-xxhdpi/btn_report.png | Bin 0 -> 725 bytes .../main/res/drawable-xxhdpi/btn_statuses.png | Bin 0 -> 1059 bytes .../res/drawable-xxhdpi/ic_account_add.png | Bin 0 -> 1125 bytes app/src/main/res/drawable/ic_menu_camera.xml | 12 + app/src/main/res/drawable/ic_menu_gallery.xml | 9 + app/src/main/res/drawable/ic_menu_manage.xml | 9 + app/src/main/res/drawable/ic_menu_send.xml | 9 + app/src/main/res/drawable/ic_menu_share.xml | 9 + .../main/res/drawable/ic_menu_slideshow.xml | 9 + app/src/main/res/drawable/side_nav_bar.xml | 9 + app/src/main/res/layout/act_main.xml | 92 ++ app/src/main/res/layout/dlg_account_add.xml | 103 +++ app/src/main/res/layout/lv_status.xml | 216 +++++ .../main/res/layout/nav_header_act_main.xml | 36 + app/src/main/res/layout/page_column.xml | 68 ++ app/src/main/res/layout/page_guide.xml | 12 + app/src/main/res/menu/act_main.xml | 9 + app/src/main/res/menu/men_navi_drawer.xml | 74 ++ app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10486 bytes app/src/main/res/values/colors.xml | 6 + app/src/main/res/values/dimens.xml | 8 + app/src/main/res/values/strings.xml | 58 ++ app/src/main/res/values/styles.xml | 20 + .../juggler/subwaytooter/ExampleUnitTest.java | 17 + build.gradle | 23 + gradle.properties | 17 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 160 ++++ gradlew.bat | 90 ++ settings.gradle | 1 + 140 files changed, 5581 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/compiler.xml create mode 100644 .idea/copyright/profiles_settings.xml create mode 100644 .idea/dictionaries/tateisu.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/jp/juggler/subwaytooter/ExampleInstrumentedTest.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/jp/juggler/subwaytooter/ActMain.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/App1.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/Column.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/ColumnPagerAdapter.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/PagerAdapterBase.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/api/TootApiResult.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/api/entity/TootApplication.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAttachment.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/api/entity/TootCard.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/api/entity/TootContext.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/api/entity/TootError.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/api/entity/TootMention.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/api/entity/TootRelationShip.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/api/entity/TootReport.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/api/entity/TootResults.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/dialog/AccountPicker.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/dialog/LoginForm.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/table/AccessToken.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/table/ClientInfo.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/table/LogData.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/util/CancelChecker.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/util/HTTPClient.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/util/HTTPClientReceiver.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/util/LogCategory.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/util/Utils.java create mode 100644 app/src/main/res/drawable-hdpi/black_close.png create mode 100644 app/src/main/res/drawable-hdpi/btn_boost.png create mode 100644 app/src/main/res/drawable-hdpi/btn_favourite.png create mode 100644 app/src/main/res/drawable-hdpi/btn_federate_tl.png create mode 100644 app/src/main/res/drawable-hdpi/btn_follow.png create mode 100644 app/src/main/res/drawable-hdpi/btn_home.png create mode 100644 app/src/main/res/drawable-hdpi/btn_local_tl.png create mode 100644 app/src/main/res/drawable-hdpi/btn_more.png create mode 100644 app/src/main/res/drawable-hdpi/btn_notification.png create mode 100644 app/src/main/res/drawable-hdpi/btn_refresh.png create mode 100644 app/src/main/res/drawable-hdpi/btn_reload.png create mode 100644 app/src/main/res/drawable-hdpi/btn_reply.png create mode 100644 app/src/main/res/drawable-hdpi/btn_report.png create mode 100644 app/src/main/res/drawable-hdpi/btn_statuses.png create mode 100644 app/src/main/res/drawable-hdpi/ic_account_add.png create mode 100644 app/src/main/res/drawable-mdpi/black_close.png create mode 100644 app/src/main/res/drawable-mdpi/btn_boost.png create mode 100644 app/src/main/res/drawable-mdpi/btn_favourite.png create mode 100644 app/src/main/res/drawable-mdpi/btn_federate_tl.png create mode 100644 app/src/main/res/drawable-mdpi/btn_follow.png create mode 100644 app/src/main/res/drawable-mdpi/btn_home.png create mode 100644 app/src/main/res/drawable-mdpi/btn_local_tl.png create mode 100644 app/src/main/res/drawable-mdpi/btn_more.png create mode 100644 app/src/main/res/drawable-mdpi/btn_notification.png create mode 100644 app/src/main/res/drawable-mdpi/btn_refresh.png create mode 100644 app/src/main/res/drawable-mdpi/btn_reload.png create mode 100644 app/src/main/res/drawable-mdpi/btn_reply.png create mode 100644 app/src/main/res/drawable-mdpi/btn_report.png create mode 100644 app/src/main/res/drawable-mdpi/btn_statuses.png create mode 100644 app/src/main/res/drawable-mdpi/ic_account_add.png create mode 100644 app/src/main/res/drawable-xhdpi/black_close.png create mode 100644 app/src/main/res/drawable-xhdpi/btn_boost.png create mode 100644 app/src/main/res/drawable-xhdpi/btn_favourite.png create mode 100644 app/src/main/res/drawable-xhdpi/btn_federate_tl.png create mode 100644 app/src/main/res/drawable-xhdpi/btn_follow.png create mode 100644 app/src/main/res/drawable-xhdpi/btn_home.png create mode 100644 app/src/main/res/drawable-xhdpi/btn_local_tl.png create mode 100644 app/src/main/res/drawable-xhdpi/btn_more.png create mode 100644 app/src/main/res/drawable-xhdpi/btn_notification.png create mode 100644 app/src/main/res/drawable-xhdpi/btn_refresh.png create mode 100644 app/src/main/res/drawable-xhdpi/btn_reload.png create mode 100644 app/src/main/res/drawable-xhdpi/btn_reply.png create mode 100644 app/src/main/res/drawable-xhdpi/btn_report.png create mode 100644 app/src/main/res/drawable-xhdpi/btn_statuses.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_account_add.png create mode 100644 app/src/main/res/drawable-xxhdpi/black_close.png create mode 100644 app/src/main/res/drawable-xxhdpi/btn_boost.png create mode 100644 app/src/main/res/drawable-xxhdpi/btn_favourite.png create mode 100644 app/src/main/res/drawable-xxhdpi/btn_federate_tl.png create mode 100644 app/src/main/res/drawable-xxhdpi/btn_follow.png create mode 100644 app/src/main/res/drawable-xxhdpi/btn_home.png create mode 100644 app/src/main/res/drawable-xxhdpi/btn_local_tl.png create mode 100644 app/src/main/res/drawable-xxhdpi/btn_more.png create mode 100644 app/src/main/res/drawable-xxhdpi/btn_notification.png create mode 100644 app/src/main/res/drawable-xxhdpi/btn_refresh.png create mode 100644 app/src/main/res/drawable-xxhdpi/btn_reload.png create mode 100644 app/src/main/res/drawable-xxhdpi/btn_reply.png create mode 100644 app/src/main/res/drawable-xxhdpi/btn_report.png create mode 100644 app/src/main/res/drawable-xxhdpi/btn_statuses.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_account_add.png create mode 100644 app/src/main/res/drawable/ic_menu_camera.xml create mode 100644 app/src/main/res/drawable/ic_menu_gallery.xml create mode 100644 app/src/main/res/drawable/ic_menu_manage.xml create mode 100644 app/src/main/res/drawable/ic_menu_send.xml create mode 100644 app/src/main/res/drawable/ic_menu_share.xml create mode 100644 app/src/main/res/drawable/ic_menu_slideshow.xml create mode 100644 app/src/main/res/drawable/side_nav_bar.xml create mode 100644 app/src/main/res/layout/act_main.xml create mode 100644 app/src/main/res/layout/dlg_account_add.xml create mode 100644 app/src/main/res/layout/lv_status.xml create mode 100644 app/src/main/res/layout/nav_header_act_main.xml create mode 100644 app/src/main/res/layout/page_column.xml create mode 100644 app/src/main/res/layout/page_guide.xml create mode 100644 app/src/main/res/menu/act_main.xml create mode 100644 app/src/main/res/menu/men_navi_drawer.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/test/java/jp/juggler/subwaytooter/ExampleUnitTest.java create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a4c78382 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 00000000..96cc43ef --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 00000000..c7d1c5a8 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/dictionaries/tateisu.xml b/.idea/dictionaries/tateisu.xml new file mode 100644 index 00000000..b4a54887 --- /dev/null +++ b/.idea/dictionaries/tateisu.xml @@ -0,0 +1,12 @@ + + + + favourited + reblog + reblogged + reblogs + subwaytooter + timelines + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 00000000..97626ba4 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 00000000..7ac24c77 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..b70ab773 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..6933c1ea --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..5d199810 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..452a776c --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 00000000..7f68460d --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..3543521e --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..8fd669f9 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,32 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 24 + buildToolsVersion '25.0.0' + defaultConfig { + applicationId "jp.juggler.subwaytooter" + minSdkVersion 21 + targetSdkVersion 24 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + compile 'com.android.support:appcompat-v7:24.2.0' + compile 'com.android.support:support-v4:24.2.0' + compile 'com.android.support:design:24.2.0' + compile 'com.android.support.constraint:constraint-layout:1.0.2' + testCompile 'junit:junit:4.12' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 00000000..acfee8c6 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in C:\android\sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/ExampleInstrumentedTest.java b/app/src/androidTest/java/jp/juggler/subwaytooter/ExampleInstrumentedTest.java new file mode 100644 index 00000000..89f4c663 --- /dev/null +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package jp.juggler.subwaytooter; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumentation test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception{ + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals( "jp.juggler.subwaytooter", appContext.getPackageName() ); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..543a4ead --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMain.java b/app/src/main/java/jp/juggler/subwaytooter/ActMain.java new file mode 100644 index 00000000..ff0a0c92 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMain.java @@ -0,0 +1,259 @@ +package jp.juggler.subwaytooter; + +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.design.widget.FloatingActionButton; +import android.support.design.widget.Snackbar; +import android.support.v4.os.AsyncTaskCompat; +import android.support.v4.view.ViewPager; +import android.view.View; +import android.support.design.widget.NavigationView; +import android.support.v4.view.GravityCompat; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.Menu; +import android.view.MenuItem; + +import jp.juggler.subwaytooter.api.TootApiClient; +import jp.juggler.subwaytooter.api.TootApiResult; +import jp.juggler.subwaytooter.dialog.AccountPicker; +import jp.juggler.subwaytooter.dialog.LoginForm; +import jp.juggler.subwaytooter.table.SavedAccount; +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class ActMain extends AppCompatActivity + implements NavigationView.OnNavigationItemSelectedListener { + public static final LogCategory log = new LogCategory( "ActMain" ); + + @Override + protected void onCreate( Bundle savedInstanceState ){ + super.onCreate( savedInstanceState ); + initUI(); + + } + + @Override + public void onBackPressed(){ + DrawerLayout drawer = (DrawerLayout) findViewById( R.id.drawer_layout ); + if( drawer.isDrawerOpen( GravityCompat.START ) ){ + drawer.closeDrawer( GravityCompat.START ); + }else{ + super.onBackPressed(); + } + } + + @Override + public boolean onCreateOptionsMenu( Menu menu ){ + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate( R.menu.act_main, menu ); + return true; + } + + @Override + public boolean onOptionsItemSelected( MenuItem item ){ + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + + //noinspection SimplifiableIfStatement + if( id == R.id.action_settings ){ + return true; + } + + return super.onOptionsItemSelected( item ); + } + + @SuppressWarnings("StatementWithEmptyBody") + @Override + public boolean onNavigationItemSelected( MenuItem item ){ + // Handle navigation view item clicks here. + int id = item.getItemId(); + + if( id == R.id.nav_account_add ){ + performAccountAdd(); + }else if( id == R.id.nav_add_tl_home ){ + performAddTimeline(Column.TYPE_TL_HOME ); + }else if( id == R.id.nav_add_tl_local ){ + performAddTimeline(Column.TYPE_TL_LOCAL ); + }else if( id == R.id.nav_add_tl_federate ){ + performAddTimeline(Column.TYPE_TL_FEDERATE ); + + }else if( id == R.id.nav_add_favourites ){ + performAddTimeline(Column.TYPE_TL_FAVOURITES ); +// }else if( id == R.id.nav_add_reports ){ +// performAddTimeline(Column.TYPE_TL_REPORTS ); + }else if( id == R.id.nav_add_statuses ){ + performAddTimeline(Column.TYPE_TL_STATUSES ); + }else if( id == R.id.nav_add_notifications ){ + performAddTimeline(Column.TYPE_TL_NOTIFICATIONS ); + + // Handle the camera action +// }else if( id == R.id.nav_gallery ){ +// +// }else if( id == R.id.nav_slideshow ){ +// +// }else if( id == R.id.nav_manage ){ +// +// }else if( id == R.id.nav_share ){ +// +// }else if( id == R.id.nav_send ){ + + } + + DrawerLayout drawer = (DrawerLayout) findViewById( R.id.drawer_layout ); + drawer.closeDrawer( GravityCompat.START ); + return true; + } + + ViewPager pager; + ColumnPagerAdapter pager_adapter; + View llEmpty; + + void initUI(){ + setContentView( R.layout.act_main ); + llEmpty = findViewById( R.id.llEmpty ); + + // toolbar + Toolbar toolbar = (Toolbar) findViewById( R.id.toolbar ); + setSupportActionBar( toolbar ); + + // navigation drawer + DrawerLayout drawer = (DrawerLayout) findViewById( R.id.drawer_layout ); + ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( + this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close ); + drawer.addDrawerListener( toggle ); + toggle.syncState(); + + NavigationView navigationView = (NavigationView) findViewById( R.id.nav_view ); + navigationView.setNavigationItemSelectedListener( this ); + + // floating action button + FloatingActionButton fab = (FloatingActionButton) findViewById( R.id.fab ); + fab.setOnClickListener( new View.OnClickListener() { + @Override + public void onClick( View view ){ + Snackbar.make( view, "Replace with your own action", Snackbar.LENGTH_LONG ) + .setAction( "Action", null ).show(); + } + } ); + + // ViewPager + pager = (ViewPager) findViewById( R.id.viewPager ); + pager_adapter = new ColumnPagerAdapter( this ); + pager.setAdapter( pager_adapter ); + } + + public void performAccountAdd(){ + LoginForm.showLoginForm( this, new LoginForm.LoginFormCallback() { + + @Override + public void startLogin( final Dialog dialog,final String instance, final String user_mail, final String password ){ + + final ProgressDialog progress = new ProgressDialog( ActMain.this ); + + final AsyncTask< Void, String, TootApiResult > task = new AsyncTask< Void, String, TootApiResult >() { + + boolean __isCancelled(){ + return isCancelled(); + } + + boolean is_added = false; + + @Override + protected TootApiResult doInBackground( Void... params ){ + TootApiClient api_client = new TootApiClient( ActMain.this, new TootApiClient.Callback() { + @Override + public boolean isCancelled(){ + return __isCancelled(); + } + + @Override + public void publishProgress( final String s ){ + Utils.runOnMainThread( new Runnable() { + @Override + public void run(){ + progress.setMessage( s ); + } + } ); + } + } ); + + api_client.setUserInfo( instance, user_mail, password ); + + TootApiResult result = api_client.get( "/api/v1/accounts/verify_credentials" ); + if( result != null && result.object != null ){ + is_added = ! SavedAccount.hasAccount(log,instance, user_mail); + SavedAccount.save( log,instance, user_mail, result.object ); + } + return result; + } + + @Override + protected void onPostExecute( TootApiResult result ){ + progress.dismiss(); + + if( result == null ){ + // cancelled. + }else if( result.object == null ){ + Utils.showToast( ActMain.this, true, result.error ); + log.e( result.error ); + }else{ + SavedAccount account = SavedAccount.loadAccount(log,instance,user_mail); + if( account != null ){ + ActMain.this.onAccountUpdated(account,is_added); + dialog.dismiss(); + } + } + } + }; + progress.setIndeterminate( true ); + progress.setCancelable( true ); + progress.setOnCancelListener( new DialogInterface.OnCancelListener() { + @Override + public void onCancel( DialogInterface dialog ){ + task.cancel( true ); + } + } ); + progress.show(); + AsyncTaskCompat.executeParallel( task ); + } + } ); + + } + + public void performColumnClose( Column column ){ + pager_adapter.removeColumn( pager,column ); + if( pager_adapter.getCount() == 0 ){ + llEmpty.setVisibility( View.VISIBLE ); + } + } + + private void onAccountUpdated( SavedAccount data, boolean is_added){ + Utils.showToast(this,false,R.string.accout_confirmed); + if( is_added ){ + Column col = new Column( this, data, Column.TYPE_TL_HOME ); + pager_adapter.addColumn( pager, col ); + llEmpty.setVisibility( View.GONE ); + } + } + + + private void performAddTimeline( final int type,final Object... params){ + AccountPicker.pick( this, new AccountPicker.AccountPickerCallback() { + @Override + public void onAccountPicked( SavedAccount ai ){ + Column col = new Column( ActMain.this, ai, type ,ai.id,params); + pager_adapter.addColumn( pager, col ); + pager.setCurrentItem( pager_adapter.getCount() -1 ); + llEmpty.setVisibility( View.GONE ); + } + } ); + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/App1.java b/app/src/main/java/jp/juggler/subwaytooter/App1.java new file mode 100644 index 00000000..9cac398a --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/App1.java @@ -0,0 +1,61 @@ +package jp.juggler.subwaytooter; + +import android.app.Application; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import jp.juggler.subwaytooter.table.AccessToken; +import jp.juggler.subwaytooter.table.ClientInfo; +import jp.juggler.subwaytooter.table.LogData; +import jp.juggler.subwaytooter.table.SavedAccount; + +public class App1 extends Application{ + + @Override + public void onCreate(){ + super.onCreate(); + if( db_open_helper == null ){ + db_open_helper = new DBOpenHelper( getApplicationContext() ); + } + } + + @Override + public void onTerminate(){ + super.onTerminate(); + } + + + static final String DB_NAME = "app_db"; + static final int DB_VERSION = 1; + + static DBOpenHelper db_open_helper; + + public static SQLiteDatabase getDB(){ + return db_open_helper.getWritableDatabase(); + } + + static class DBOpenHelper extends SQLiteOpenHelper { + + public DBOpenHelper( Context context ){ + super( context, DB_NAME, null , DB_VERSION ); + } + + @Override + public void onCreate( SQLiteDatabase db ){ + LogData.onDBCreate( db); + // + AccessToken.onDBCreate(db); + SavedAccount.onDBCreate(db); + ClientInfo.onDBCreate( db); + } + + @Override + public void onUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){ + LogData.onDBUpgrade( db,oldVersion,newVersion ); + AccessToken.onDBUpgrade( db,oldVersion,newVersion ); + SavedAccount.onDBUpgrade( db,oldVersion,newVersion ); + ClientInfo.onDBUpgrade( db,oldVersion,newVersion ); + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/Column.java b/app/src/main/java/jp/juggler/subwaytooter/Column.java new file mode 100644 index 00000000..a6cb231a --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/Column.java @@ -0,0 +1,282 @@ +package jp.juggler.subwaytooter; + +import android.os.AsyncTask; +import android.support.v4.os.AsyncTaskCompat; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.concurrent.atomic.AtomicBoolean; + +import jp.juggler.subwaytooter.api.TootApiClient; +import jp.juggler.subwaytooter.api.TootApiResult; +import jp.juggler.subwaytooter.api.entity.TootAccount; +import jp.juggler.subwaytooter.api.entity.TootNotification; +import jp.juggler.subwaytooter.api.entity.TootReport; +import jp.juggler.subwaytooter.api.entity.TootStatus; +import jp.juggler.subwaytooter.table.SavedAccount; +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class Column { + static final LogCategory log = new LogCategory( "Column" ); + + final ActMain activity; + final SavedAccount access_info; + final int type; + final long who_id; + + static final int TYPE_TL_HOME = 1; + static final int TYPE_TL_LOCAL = 2; + static final int TYPE_TL_FEDERATE = 3; + static final int TYPE_TL_STATUSES = 4; + static final int TYPE_TL_FAVOURITES = 5; + static final int TYPE_TL_REPORTS = 6; + static final int TYPE_TL_NOTIFICATIONS = 7; + + public Column( ActMain activity, SavedAccount access_info, int type ){ + this( activity,access_info,type,access_info.id); + } + + public Column( ActMain activity, SavedAccount access_info, int type ,long who_id,Object... params){ + this.activity = activity; + this.access_info = access_info; + this.type = type; + this.who_id = who_id; + startLoading(); + } + + final AtomicBoolean is_dispose = new AtomicBoolean(); + + void dispose(){ + is_dispose.set( true ); + } + + public String getColumnName(){ + switch( type ){ + default: + return access_info.getFullAcct( access_info ) + "\n" + "?"; + case TYPE_TL_HOME: + return access_info.getFullAcct( access_info ) + "\n" + activity.getString( R.string.home ); + case TYPE_TL_LOCAL: + return access_info.getFullAcct( access_info ) + "\n" + activity.getString( R.string.local_timeline ); + case TYPE_TL_FEDERATE: + return access_info.getFullAcct( access_info ) + "\n" + activity.getString( R.string.federate_timeline ); + + case TYPE_TL_STATUSES: + return access_info.getFullAcct( access_info ) + "\n" + activity.getString( R.string.statuses_of + , who_account != null ? access_info.getFullAcct( who_account ) : Long.toString( who_id ) + ); + + case TYPE_TL_FAVOURITES: + return access_info.getFullAcct( access_info ) + "\n" + activity.getString( R.string.favourites ); + + case TYPE_TL_REPORTS: + return access_info.getFullAcct( access_info ) + "\n" + activity.getString( R.string.reports ); + + case TYPE_TL_NOTIFICATIONS: + return access_info.getFullAcct( access_info ) + "\n" + activity.getString( R.string.notifications ); + + } + } + + public interface VisualCallback { + void onVisualColumn(); + } + + final LinkedList< VisualCallback > visual_callback = new LinkedList<>(); + + void addVisualListener( VisualCallback listener ){ + if( listener == null ) return; + Iterator< VisualCallback > it = visual_callback.iterator(); + while( it.hasNext() ){ + VisualCallback vc = it.next(); + if( vc == listener ) return; + } + visual_callback.add( listener ); + } + + void removeVisualListener( VisualCallback listener ){ + if( listener == null ) return; + Iterator< VisualCallback > it = visual_callback.iterator(); + while( it.hasNext() ){ + VisualCallback vc = it.next(); + if( vc == listener ) it.remove(); + } + } + + private void fireVisualCallback(){ + Iterator< VisualCallback > it = visual_callback.iterator(); + while( it.hasNext() ){ + it.next().onVisualColumn(); + } + } + + AsyncTask< Void, Void, TootApiResult > last_task; + + void cancelLastTask(){ + if( last_task != null ) last_task.cancel( true ); + } + + boolean is_loading = false; + String task_progress; + String error = null; + + final TootStatus.List status_list = new TootStatus.List(); + final TootReport.List report_list = new TootReport.List(); + final TootNotification.List notification_list = new TootNotification.List(); + volatile TootAccount who_account; + + public void reload(){ + status_list.clear(); + startLoading(); + } + + void startLoading(){ + error = null; + is_loading = true; + fireVisualCallback(); + cancelLastTask(); + + AsyncTask< Void, Void, TootApiResult > task = this.last_task = new AsyncTask< Void, Void, TootApiResult >() { + boolean __isCancelled(){ + return isCancelled(); + } + + TootStatus.List tmp_list_status; + TootReport.List tmp_list_report; + TootNotification.List tmp_list_notification; + + TootApiResult parseStatuses( TootApiResult result ){ + if( result != null ){ + tmp_list_status = TootStatus.parseList( log, result.array ); + } + return result; + } + + TootApiResult parseAccount( TootApiResult result ){ + if( result != null ){ + who_account = TootAccount.parse( log, result.object ); + } + return result; + } + + TootApiResult parseReports( TootApiResult result ){ + if( result != null ){ + tmp_list_report = TootReport.parseList( log, result.array ); + } + return result; + } + + TootApiResult parseNotifications( TootApiResult result ){ + if( result != null ){ + tmp_list_notification = TootNotification.parseList( log, result.array ); + } + return result; + } + + @Override + protected TootApiResult doInBackground( Void... params ){ + TootApiClient client = new TootApiClient( activity, new TootApiClient.Callback() { + @Override + public boolean isCancelled(){ + return __isCancelled() || is_dispose.get(); + } + + @Override + public void publishProgress( final String s ){ + Utils.runOnMainThread( new Runnable() { + @Override + public void run(){ + if( isCancelled() ) return; + task_progress = s; + fireVisualCallback(); + } + } ); + } + } ); + + client.setAccessInfo( access_info ); + + switch( type ){ + default: + case TYPE_TL_HOME: + return parseStatuses( client.get( "/api/v1/timelines/home" ) ); + + case TYPE_TL_LOCAL: + return parseStatuses( client.get( "/api/v1/timelines/public?local=1" ) ); + + case TYPE_TL_FEDERATE: + return parseStatuses( client.get( "/api/v1/timelines/public" ) ); + + case TYPE_TL_STATUSES: + if( who_account == null ){ + parseAccount( client.get( "/api/v1/accounts/" + who_id ) ); + client.callback.publishProgress( "" ); + } + + return parseStatuses( client.get( "/api/v1/accounts/"+who_id+"/statuses" ) ); + + case TYPE_TL_FAVOURITES: + return parseStatuses( client.get( "/api/v1/favourites" ) ); + + case TYPE_TL_REPORTS: + return parseReports( client.get( "/api/v1/reports" ) ); + + case TYPE_TL_NOTIFICATIONS: + return parseNotifications( client.get( "/api/v1/notifications" ) ); + } + } + + @Override + protected void onCancelled( TootApiResult result ){ + onPostExecute( null ); + } + + @Override + protected void onPostExecute( TootApiResult result ){ + is_loading = false; + if( result == null ){ + Column.this.error = activity.getString( R.string.cancelled ); + }else if( result.error != null ){ + Column.this.error = result.error; + }else{ + switch( type ){ + default: + case TYPE_TL_HOME: + case TYPE_TL_LOCAL: + case TYPE_TL_FEDERATE: + case TYPE_TL_STATUSES: + case TYPE_TL_FAVOURITES: + if( tmp_list_status != null ){ + for( int i = tmp_list_status.size() - 1 ; i >= 0 ; -- i ){ + status_list.add( 0, tmp_list_status.get( i ) ); + } + } + break; + + case TYPE_TL_REPORTS: + if( tmp_list_report != null ){ + for( int i = tmp_list_report.size() - 1 ; i >= 0 ; -- i ){ + report_list.add( 0, tmp_list_report.get( i ) ); + } + } + break; + + case TYPE_TL_NOTIFICATIONS: + if( tmp_list_notification != null ){ + for( int i = tmp_list_notification.size() - 1 ; i >= 0 ; -- i ){ + notification_list.add( 0, tmp_list_notification.get( i ) ); + } + } + break; + } + + } + fireVisualCallback(); + } + }; + + AsyncTaskCompat.executeParallel( task ); + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnPagerAdapter.java b/app/src/main/java/jp/juggler/subwaytooter/ColumnPagerAdapter.java new file mode 100644 index 00000000..a6230ca6 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnPagerAdapter.java @@ -0,0 +1,104 @@ +package jp.juggler.subwaytooter; + + +import android.app.Activity; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; + +public class ColumnPagerAdapter extends PagerAdapter{ + + final ActMain activity; + final LayoutInflater inflater; + + boolean loop_mode = false; + + ColumnPagerAdapter( ActMain activity ){ + this.activity = activity; + this.inflater = activity.getLayoutInflater(); + } + + final ArrayList column_list = new ArrayList<>(); + final SparseArray holder_list = new SparseArray<>(); + + int addColumn( ViewPager pager, Column column ){ + int size = column_list.size(); + if( size == 0 ){ + column_list.add( column ); + notifyDataSetChanged(); + return 0; + }else{ + int idx = 1+pager.getCurrentItem(); + column_list.add( idx, column ); + notifyDataSetChanged(); + pager.setCurrentItem( idx ); + return idx; + } + } + public void removeColumn( ViewPager pager,Column column ){ + int idx_column = column_list.indexOf( column ); + if( idx_column == - 1 ) return; + int idx_showing = pager.getCurrentItem(); + pager.setAdapter( null ); + column_list.remove( idx_column ); + pager.setAdapter( this ); + pager.setCurrentItem( idx_showing >= column_list.size() ? idx_showing -1 : idx_showing ); + + } + + + public Column getColumn( int idx ){ + return column_list.get( idx ); + } + + public ColumnViewHolder getColumnViewHolder( int idx ){ + return holder_list.get( idx ); + } + + + @Override public int getCount(){ + return column_list.size(); + } + + @Override public CharSequence getPageTitle( int page_idx ){ + return "page"+ page_idx; + } + + @Override + public boolean isViewFromObject( View view, Object object ){ + return view == object; + } + + @Override public Object instantiateItem( ViewGroup container, int page_idx ){ + View root = inflater.inflate( R.layout.page_column, container, false ); + container.addView( root, 0 ); + + Column column = column_list.get( page_idx ); + ColumnViewHolder holder = new ColumnViewHolder( activity,column, page_idx ); + // + holder_list.put( page_idx, holder ); + // + holder.onPageCreate( root ); + + return root; + } + + @Override public void destroyItem( ViewGroup container, int page_idx, Object object ){ + View view = (View) object; + // + container.removeView( view ); + // + ColumnViewHolder holder = holder_list.get( page_idx ); + holder_list.remove( page_idx ); + if( holder != null ){ + holder.is_destroyed.set( true ); + holder.onPageDestroy( view ); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java new file mode 100644 index 00000000..92742e67 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java @@ -0,0 +1,326 @@ +package jp.juggler.subwaytooter; + +import android.graphics.PorterDuff; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; + +import jp.juggler.subwaytooter.api.entity.TootNotification; +import jp.juggler.subwaytooter.api.entity.TootReport; +import jp.juggler.subwaytooter.api.entity.TootStatus; +import jp.juggler.subwaytooter.table.SavedAccount; +import jp.juggler.subwaytooter.util.LogCategory; + +public class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback { + static final LogCategory log = new LogCategory( "ColumnViewHolder" ); + + public final AtomicBoolean is_destroyed = new AtomicBoolean( false ); + public final ActMain activity; + public final Column column; + public final int column_index; + + public ColumnViewHolder( ActMain activity, Column column, int column_index ){ + log.d("ctor"); + this.activity = activity; + this.column = column; + this.column_index = column_index; + } + + public boolean isPageDestroyed(){ + return is_destroyed.get() || activity.isFinishing(); + } + + TextView tvLoading; + ListView listView; + TextView tvColumnName; + StatusListAdapter status_adapter; + + void onPageCreate( View root ){ + log.d("onPageCreate:%s",column.getColumnName() ); + + tvColumnName = (TextView) root.findViewById( R.id.tvColumnName ); + + + root.findViewById( R.id.btnColumnClose ).setOnClickListener( this ); + root.findViewById( R.id.btnColumnReload ).setOnClickListener( this ); + + tvLoading = (TextView) root.findViewById( R.id.tvLoading ); + listView = (ListView) root.findViewById( R.id.listView ); + status_adapter = new StatusListAdapter(); + listView.setAdapter( status_adapter ); + // + + column.addVisualListener( this ); + onVisualColumn(); + } + + void onPageDestroy( View root ){ + log.d("onPageDestroy:%s",column.getColumnName() ); + column.removeVisualListener( this ); + } + + @Override + public void onClick( View v ){ + switch( v.getId() ){ + case R.id.btnColumnClose: + activity.performColumnClose( column ); + break; + case R.id.btnColumnReload: + column.reload(); + break; + } + + } + + @Override + public void onVisualColumn(){ + + tvColumnName.setText(column.getColumnName() ); + + if( column.is_dispose.get() ){ + tvLoading.setVisibility( View.VISIBLE ); + listView.setVisibility( View.GONE ); + tvLoading.setText( "column was disposed." ); + return; + } + + if( column.is_loading ){ + tvLoading.setVisibility( View.VISIBLE ); + listView.setVisibility( View.GONE ); + String progress = column.task_progress; + if( progress == null ) progress = "loading?"; + tvLoading.setText( progress ); + return; + } + tvLoading.setVisibility( View.GONE ); + + if( column.who_account != null ){ + // TODO + }else{ + + } + + switch( column.type ){ + default: + case Column.TYPE_TL_HOME: + case Column.TYPE_TL_LOCAL: + case Column.TYPE_TL_FEDERATE: + case Column.TYPE_TL_FAVOURITES: + case Column.TYPE_TL_STATUSES: + listView.setVisibility( View.VISIBLE ); + status_adapter.set( column.status_list ); + break; + case Column.TYPE_TL_REPORTS: + listView.setVisibility( View.VISIBLE ); + status_adapter.set( column.report_list ); + break; + case Column.TYPE_TL_NOTIFICATIONS: + listView.setVisibility( View.VISIBLE ); + status_adapter.set( column.notification_list ); + break; + } + } + + /////////////////////////////////////////////////////////////////// + + class StatusListAdapter extends BaseAdapter { + final ArrayList< Object > status_list = new ArrayList<>(); + + + public void set( TootStatus.List src ){ + this.status_list.clear(); + this.status_list.addAll( src ); + notifyDataSetChanged(); + } + + public void set( TootReport.List src ){ + this.status_list.clear(); + this.status_list.addAll( src ); + notifyDataSetChanged(); + } + + public void set( TootNotification.List src ){ + this.status_list.clear(); + this.status_list.addAll( src ); + notifyDataSetChanged(); + } + + @Override + public int getCount(){ + return status_list.size(); + } + + @Override + public Object getItem( int position ){ + if( position >= 0 && position < status_list.size() ) return status_list.get( position ); + return null; + } + + @Override + public long getItemId( int position ){ + return 0; + } + + @Override + public View getView( int position, View view, ViewGroup parent ){ + Object o = ( position >= 0 && position < status_list.size() ? status_list.get( position ) : null ); + + StatusViewHolder holder; + if( view == null ){ + view = activity.getLayoutInflater().inflate( R.layout.lv_status, parent, false ); + holder = new StatusViewHolder( view ); + view.setTag( holder ); + }else{ + holder = (StatusViewHolder) view.getTag(); + } + holder.bind( activity, view, o, column.access_info ); + return view; + } + + } + + static class StatusViewHolder { + + final View llBoosted; + final ImageView ivBoosted; + final TextView tvBoosted; + final TextView tvBoostedTime; + + final View llFollow; + final ImageView ivFollow; + final TextView tvFollowerName; + final TextView tvFollowerAcct; + + final View llStatus; + final ImageView ivThumbnail; + final TextView tvName; + final TextView tvTime; + final TextView tvContent; + final ImageView ivMedia; + + final ImageButton btnReply; + final ImageButton btnBoost; + final ImageButton btnFavourite; + final ImageButton btnMore; + + Object item; + SavedAccount account; + + public StatusViewHolder( View view ){ + this.llBoosted = view.findViewById( R.id.llBoosted ); + this.ivBoosted = (ImageView) view.findViewById( R.id.ivBoosted ); + this.tvBoosted = (TextView) view.findViewById( R.id.tvBoosted ); + this.tvBoostedTime = (TextView) view.findViewById( R.id.tvBoostedTime ); + + this.llFollow = view.findViewById( R.id.llFollow ); + this.ivFollow = (ImageView) view.findViewById( R.id.ivFollow ); + this.tvFollowerName = (TextView) view.findViewById( R.id.tvFollowerName ); + this.tvFollowerAcct = (TextView) view.findViewById( R.id.tvFollowerAcct ); + + this.llStatus = view.findViewById( R.id.llStatus ); + + this.ivThumbnail = (ImageView) view.findViewById( R.id.ivThumbnail ); + this.tvName = (TextView) view.findViewById( R.id.tvName ); + this.tvTime = (TextView) view.findViewById( R.id.tvTime ); + this.tvContent = (TextView) view.findViewById( R.id.tvContent ); + this.ivMedia = (ImageView) view.findViewById( R.id.ivMedia ); + this.btnReply = (ImageButton) view.findViewById( R.id.btnReply ); + this.btnBoost = (ImageButton) view.findViewById( R.id.btnBoost ); + this.btnFavourite = (ImageButton) view.findViewById( R.id.btnFavourite ); + this.btnMore = (ImageButton) view.findViewById( R.id.btnMore ); + } + + public void bind( ActMain activity, View view, Object item, SavedAccount account ){ + this.account = account; + this.item = item; + + llBoosted.setVisibility( View.GONE ); + llFollow.setVisibility( View.GONE ); + llStatus.setVisibility( View.GONE ); + + if( item == null ) return; + + if( item instanceof TootNotification ){ + TootNotification n = (TootNotification) item; + if( TootNotification.TYPE_FAVOURITE.equals( n.type ) ){ + llBoosted.setVisibility( View.VISIBLE ); + ivBoosted.setImageResource( R.drawable.btn_favourite ); + tvBoostedTime.setText(TootStatus.formatTime( n.time_created_at ) + +"\n"+ account.getFullAcct( n.account ) + ); + tvBoosted.setText( activity.getString( R.string.favourited_by, n.account.display_name ) ); + + if( n.status != null ) bindSub( activity, view, n.status,account ); + }else if( TootNotification.TYPE_REBLOG.equals( n.type ) ){ + llBoosted.setVisibility( View.VISIBLE ); + ivBoosted.setImageResource( R.drawable.btn_boost ); + tvBoostedTime.setText(TootStatus.formatTime( n.time_created_at ) + +"\n"+ account.getFullAcct( n.account ) + ); + tvBoosted.setText( activity.getString( R.string.boosted_by, n.account.display_name ) ); + if( n.status != null ) bindSub( activity, view, n.status,account ); + }else if( TootNotification.TYPE_FOLLOW.equals( n.type )){ + llBoosted.setVisibility( View.VISIBLE ); + ivBoosted.setImageResource( R.drawable.btn_boost ); + tvBoostedTime.setText(TootStatus.formatTime( n.time_created_at ) + +"\n"+ account.getFullAcct( n.account ) + ); + tvBoosted.setText( activity.getString( R.string.boosted_by, n.account.display_name ) ); + // + llFollow.setVisibility( View.VISIBLE ); + ivFollow.setImageResource( R.drawable.btn_follow ); + tvFollowerName.setText( n.account.display_name ); + tvFollowerAcct.setText( account.getFullAcct( n.account )); + }else if( TootNotification.TYPE_MENTION.equals( n.type ) ){ + if( n.status != null ) bindSub( activity, view, n.status,account ); + } + return; + } + + if( item instanceof TootStatus ){ + TootStatus status = (TootStatus)item; + if( status.reblog != null ){ + llBoosted.setVisibility( View.VISIBLE ); + ivBoosted.setImageResource( R.drawable.btn_boost ); + tvBoostedTime.setText(TootStatus.formatTime( status.time_created_at ) + +"\n"+ account.getFullAcct( status.account ) + ); + tvBoosted.setText( activity.getString( R.string.boosted_by, status.account.display_name ) ); + bindSub( activity, view, status.reblog,account ); + }else{ + bindSub( activity, view, status ,account); + } + } + } + + private void bindSub( ActMain activity, View view, TootStatus status, SavedAccount account ){ + llStatus.setVisibility( View.VISIBLE ); + tvTime.setText( TootStatus.formatTime( status.time_created_at ) + +"\n"+ account.getFullAcct( status.account ) + ); + tvName.setText( status.account.display_name ); + tvContent.setText( status.content ); + + // TODO media + + btnBoost.getDrawable().setColorFilter( + ( status.reblogged ? 0xff0088ff : 0xff000000 ) + , PorterDuff.Mode.SRC_ATOP + ); + + btnFavourite.getDrawable().setColorFilter( + ( status.favourited ? 0xff0088ff : 0xff000000 ) + , PorterDuff.Mode.SRC_ATOP + ); + // todo show count of boost/fav + } + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/PagerAdapterBase.java b/app/src/main/java/jp/juggler/subwaytooter/PagerAdapterBase.java new file mode 100644 index 00000000..8626af24 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/PagerAdapterBase.java @@ -0,0 +1,123 @@ +package jp.juggler.subwaytooter; + + +import android.app.Activity; +import android.support.v4.view.PagerAdapter; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; + +public class PagerAdapterBase extends PagerAdapter{ + + public static abstract class PageViewHolder{ + + public final AtomicBoolean is_destroyed = new AtomicBoolean( false ); + public final Activity activity; + + @SuppressWarnings( "UnusedParameters" ) + public PageViewHolder( Activity activity, View ignored ){ + this.activity = activity; + } + + public boolean isPageDestroyed(){ + return is_destroyed.get() || activity.isFinishing(); + } + + @SuppressWarnings( "RedundantThrows" ) + protected abstract void onPageCreate( @SuppressWarnings( "UnusedParameters" ) int page_idx, View root ) throws Throwable; + + @SuppressWarnings( "RedundantThrows" ) + protected abstract void onPageDestroy( @SuppressWarnings( "UnusedParameters" ) int page_idx, @SuppressWarnings( "UnusedParameters" ) View root ) throws Throwable; + } + + public final Activity activity; + public final LayoutInflater inflater; + + public PagerAdapterBase( Activity activity ){ + this.activity = activity; + this.inflater = activity.getLayoutInflater(); + } + + protected final ArrayList title_list = new ArrayList<>(); + protected final ArrayList layout_id_list = new ArrayList<>(); + protected final ArrayList> holder_class_list = new ArrayList<>(); + protected final SparseArray holder_list = new SparseArray<>(); + + public int addPage( CharSequence title, int layout_id, Class holder_class ){ + int idx = title_list.size(); + title_list.add( title ); + layout_id_list.add( layout_id ); + holder_class_list.add( holder_class ); + // ページのインデックスを返す + return idx; + } + + // ページが存在する場合そのViewHolderを返す + // ページのViewが生成されていない場合はnullを返す + public T getPage( int idx ){ + PageViewHolder vh = holder_list.get( idx ); + if( vh == null ) return null; + return (T) holder_class_list.get( idx ).cast( vh ); + } + + public boolean loop_mode = false; + + public int getCountReal(){ + return title_list.size(); + } + + @Override public int getCount(){ + return loop_mode ? Integer.MAX_VALUE : title_list.size(); + } + + @Override public CharSequence getPageTitle( int page_idx ){ + return title_list.get( page_idx % getCountReal() ); + } + + @Override + public boolean isViewFromObject( View view, Object object ){ + return view == object; + } + + @Override public Object instantiateItem( ViewGroup container, int page_idx ){ + View root = inflater.inflate( layout_id_list.get( page_idx % getCountReal() ), container, false ); + container.addView( root, 0 ); + + try{ + PageViewHolder holder = + holder_class_list.get( page_idx % getCountReal() ) + .getConstructor( Activity.class, View.class ) + .newInstance( activity, root ); + // + holder_list.put( page_idx, holder ); + // + holder.onPageCreate( page_idx % getCountReal(), root ); + // + }catch( Throwable ex ){ + ex.printStackTrace(); + } + return root; + } + + @Override public void destroyItem( ViewGroup container, int page_idx, Object object ){ + View view = (View) object; + // + container.removeView( view ); + // + try{ + PageViewHolder holder = holder_list.get( page_idx ); + holder_list.remove( page_idx ); + if( holder != null ){ + holder.is_destroyed.set( true ); + holder.onPageDestroy( page_idx % getCountReal(), view ); + } + }catch( Throwable ex ){ + ex.printStackTrace(); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.java b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.java new file mode 100644 index 00000000..6104c8ec --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.java @@ -0,0 +1,190 @@ +package jp.juggler.subwaytooter.api; + +import android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.UUID; + +import jp.juggler.subwaytooter.table.AccessToken; +import jp.juggler.subwaytooter.table.SavedAccount; +import jp.juggler.subwaytooter.util.CancelChecker; +import jp.juggler.subwaytooter.util.HTTPClient; +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.R; +import jp.juggler.subwaytooter.util.Utils; +import jp.juggler.subwaytooter.table.ClientInfo; + +public class TootApiClient { + private static final LogCategory log = new LogCategory( "TootApiClient" ); + + + public interface Callback { + boolean isCancelled(); + + void publishProgress( String s ); + } + + private final Context context; + public final Callback callback; + + public TootApiClient( @NonNull Context context, @NonNull Callback callback ){ + this.context = context; + this.callback = callback; + } + + private String instance; + private String user_mail; + private String password; + + public void setUserInfo( String instance, String user_mail, String password ){ + this.instance = instance; + this.user_mail = user_mail; + this.password = password; + } + public void setAccessInfo( SavedAccount access_info ){ + this.instance = access_info.host; + this.user_mail = access_info.user_mail; + } + + public TootApiResult get( String path ){ + + final HTTPClient client = new HTTPClient( 60000, 10, "account", new CancelChecker() { + @Override + public boolean isCancelled(){ + return callback.isCancelled(); + } + } ); + + JSONObject client_info = null; + JSONObject token_info = null; + for( ; ; ){ + if( callback.isCancelled() ) return null; + if( client_info == null ){ + // DBにあるならそれを使う + client_info = ClientInfo.load( instance ); + if( client_info != null ) continue; + + callback.publishProgress( context.getString( R.string.register_app_to_server, instance ) ); + + // OAuth2 クライアント登録 + String client_name = "jp.juggler.subwaytooter." + UUID.randomUUID().toString(); + client.post_content = Utils.encodeUTF8( + "client_name=" + Uri.encode( client_name ) + + "&redirect_uris=urn:ietf:wg:oauth:2.0:oob" + + "&scopes=read write follow" + ); + byte[] data = client.getHTTP( log, "https://" + instance + "/api/v1/apps" ); + if( callback.isCancelled() ) return null; + + if( data == null ){ + return new TootApiResult( context.getString( R.string.network_error, client.last_error ) ); + } + try{ + String result = Utils.decodeUTF8( data ); + // {"id":999,"redirect_uri":"urn:ietf:wg:oauth:2.0:oob","client_id":"******","client_secret":"******"} + client_info = new JSONObject( result ); + String error = Utils.optStringX( client_info, "error" ); + if( ! TextUtils.isEmpty( error ) ){ + return new TootApiResult( context.getString( R.string.api_error, error ) ); + } + ClientInfo.save( instance, result ); + continue; + }catch( JSONException ex ){ + ex.printStackTrace(); + return new TootApiResult( Utils.formatError( ex, "API data error" ) ); + } + } + if( token_info == null ){ + // DBにあるならそれを使う + token_info = AccessToken.load( instance, user_mail ); + if( token_info != null ) continue; + + if( password == null ){ + // 手動でアクセストークンを再取得しなければいけない + return new TootApiResult( context.getString( R.string.login_required ) ); + } + + callback.publishProgress( context.getString( R.string.request_access_token ) ); + + // アクセストークンの取得 +// + client.post_content = Utils.encodeUTF8( + "client_id=" + Uri.encode( Utils.optStringX( client_info , "client_id" ) ) + + "&client_secret=" + Uri.encode( Utils.optStringX( client_info, "client_secret" ) ) + + "&grant_type=password" + + "&username=" + Uri.encode( user_mail ) + + "&password=" + Uri.encode( password ) + ); + byte[] data = client.getHTTP( log, "https://" + instance + "/oauth/token" ); + if( callback.isCancelled() ) return null; + + // TODO: アプリIDが無効な場合はどんなエラーが出る? + + if( data == null ){ + return new TootApiResult( context.getString( R.string.network_error, client.last_error ) ); + } + + try{ + String result = Utils.decodeUTF8( data ); + // {"access_token":"******","token_type":"bearer","scope":"read","created_at":1492334641} + token_info = new JSONObject( result ); + String error = Utils.optStringX( client_info, "error" ); + if( ! TextUtils.isEmpty( error ) ){ + return new TootApiResult( context.getString( R.string.api_error, error ) ); + } + AccessToken.save( instance, user_mail, result ); + continue; + }catch( JSONException ex ){ + ex.printStackTrace(); + return new TootApiResult( Utils.formatError( ex, "API data error" ) ); + } + } + + // アクセストークンを使ってAPIを呼び出す + { + callback.publishProgress( context.getString( R.string.request_api, path ) ); + + client.post_content = null; + client.extra_header = new String[]{ + "Authorization", "Bearer "+ Utils.optStringX( token_info,"access_token") + }; + byte[] data = client.getHTTP( log, "https://" + instance + path ); + if( callback.isCancelled() ) return null; + + // TODO: アクセストークンが無効な場合はどうなる? + // TODO: アプリIDが無効な場合はどうなる? + + if( data == null ){ + return new TootApiResult( context.getString( R.string.network_error, client.last_error ) ); + } + + try{ + String result = Utils.decodeUTF8( data ); + if( result.startsWith( "[" ) ){ + JSONArray array = new JSONArray( result ); + return new TootApiResult( result,array ); + }else{ + JSONObject json = new JSONObject( result ); + + String error = Utils.optStringX( client_info, "error" ); + if( ! TextUtils.isEmpty( error ) ){ + return new TootApiResult( context.getString( R.string.api_error, error ) ); + } + return new TootApiResult( result,json ); + } + }catch( JSONException ex ){ + ex.printStackTrace(); + return new TootApiResult( Utils.formatError( ex, "API data error" ) ); + } + } + } + } +} + + diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResult.java b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResult.java new file mode 100644 index 00000000..faf02060 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResult.java @@ -0,0 +1,25 @@ +package jp.juggler.subwaytooter.api; + +import org.json.JSONArray; +import org.json.JSONObject; + +public class TootApiResult { + public String error; + public JSONObject object; + public JSONArray array; + public String json; + public TootApiResult( String error ){ + this.error = error; + } + + public TootApiResult( String json,JSONObject object ){ + this.json = json; + this.object = object; + } + + public TootApiResult( String json,JSONArray array ){ + this.json = json; + this.array = array; + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.java new file mode 100644 index 00000000..5ce5427a --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.java @@ -0,0 +1,109 @@ +package jp.juggler.subwaytooter.api.entity; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; + +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class TootAccount { + + public static class List extends ArrayList< TootAccount > { + + } + + // The ID of the account + public long id; + + // The username of the account + public String username; + + // Equals username for local users, includes @domain for remote ones + public String acct; + + // The account's display name + public String display_name; + + //Boolean for when the account cannot be followed without waiting for approval first + public boolean locked; + + // The time the account was created + // ex: "2017-04-13T11:06:08.289Z" + public String created_at; + + // The number of followers for the account + public long followers_count; + + //The number of accounts the given account is following + public long following_count; + + // The number of statuses the account has made + public long statuses_count; + + // Biography of user + // 説明文。改行は\r\n。リンクなどはHTMLタグで書かれている + public String note; + + //URL of the user's profile page (can be remote) + // https://mastodon.juggler.jp/@tateisu + public String url; + + // URL to the avatar image + public String avatar; + + // URL to the avatar static image (gif) + public String avatar_static; + + //URL to the header image + public String header; + + // URL to the header static image (gif) + public String header_static; + + public static TootAccount parse( LogCategory log, JSONObject src, TootAccount dst ){ + if( src == null ) return null; + try{ + dst.id = src.optLong( "id" ); + dst.username = Utils.optStringX( src, "username" ); + dst.acct = Utils.optStringX( src, "acct" ); + dst.display_name = Utils.optStringX( src, "display_name" ); + dst.locked = src.optBoolean( "locked" ); + dst.created_at = Utils.optStringX( src, "created_at" ); + dst.followers_count = src.optLong( "followers_count" ); + dst.following_count = src.optLong( "following_count" ); + dst.statuses_count = src.optLong( "statuses_count" ); + dst.note = Utils.optStringX( src, "note" ); + dst.url = Utils.optStringX( src, "url" ); + dst.avatar = Utils.optStringX( src, "avatar" ); // "https:\/\/mastodon.juggler.jp\/system\/accounts\/avatars\/000\/000\/148\/original\/0a468974fac5a448.PNG?1492081886", + dst.avatar_static = Utils.optStringX( src, "avatar_static" ); // "https:\/\/mastodon.juggler.jp\/system\/accounts\/avatars\/000\/000\/148\/original\/0a468974fac5a448.PNG?1492081886", + dst.header = Utils.optStringX( src, "header" ); // "https:\/\/mastodon.juggler.jp\/headers\/original\/missing.png" + dst.header_static = Utils.optStringX( src, "header_static" ); // "https:\/\/mastodon.juggler.jp\/headers\/original\/missing.png"} + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e( ex, "TootAccount.parse failed." ); + return null; + } + } + + public static TootAccount parse( LogCategory log, JSONObject src ){ + return parse( log, src, new TootAccount() ); + } + + public static List parseList( LogCategory log, JSONArray array ){ + List result = new List(); + if( array != null ){ + for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + JSONObject src = array.optJSONObject( i ); + if( src == null ) continue; + TootAccount item = parse( log, src ); + if( item != null ) result.add( 0, item ); + } + } + return result; + } + + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootApplication.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootApplication.java new file mode 100644 index 00000000..c9897eb6 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootApplication.java @@ -0,0 +1,24 @@ +package jp.juggler.subwaytooter.api.entity; + +import org.json.JSONObject; + +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class TootApplication { + public String name; + public String website; + + public static TootApplication parse( LogCategory log, JSONObject src ){ + try{ + TootApplication dst = new TootApplication(); + dst.name = Utils.optStringX( src, "name" ); + dst.website = Utils.optStringX( src, "website" ); + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e( ex, "TootApplication.parse failed." ); + return null; + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAttachment.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAttachment.java new file mode 100644 index 00000000..3dc03435 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAttachment.java @@ -0,0 +1,67 @@ +package jp.juggler.subwaytooter.api.entity; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; + +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class TootAttachment { + + public static class List extends ArrayList< TootAttachment > { + + } + + // ID of the attachment + public long id; + + //One of: "image", "video", "gifv". or may null ? may "unknown" ? + public String type; + public static final String TYPE_IMAGE = "image"; + public static final String TYPE_VIDEO = "video"; + public static final String TYPE_GIFV = "gifv"; + + //URL of the locally hosted version of the image + public String url; + + //For remote images, the remote URL of the original image + public String remote_url; + + // URL of the preview image + public String preview_url; + + // Shorter URL for the image, for insertion into text (only present on local images) + public String text_url; + + public static TootAttachment parse( LogCategory log, JSONObject src ){ + if( src == null ) return null; + try{ + TootAttachment dst = new TootAttachment(); + dst.id = src.optLong( "id" ); + dst.type = Utils.optStringX( src, "type" ); + dst.url = Utils.optStringX( src, "url" ); + dst.remote_url = Utils.optStringX( src, "remote_url" ); + dst.preview_url = Utils.optStringX( src, "preview_url" ); + dst.text_url = Utils.optStringX( src, "text_url" ); + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e( ex, "TootAttachment.parse failed." ); + return null; + } + } + + public static List parseList( LogCategory log, JSONArray array ){ + List result = new List(); + for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + JSONObject src = array.optJSONObject( i ); + if( src == null ) continue; + TootAttachment item = parse( log, src ); + if( item != null ) result.add( 0, item ); + } + return result; + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootCard.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootCard.java new file mode 100644 index 00000000..35ddef21 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootCard.java @@ -0,0 +1,39 @@ +package jp.juggler.subwaytooter.api.entity; + +import org.json.JSONObject; + +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class TootCard { + + + // The url associated with the card + public String url; + + // The title of the card + public String title; + + // The card description + public String description; + + // The image associated with the card, if any + public String image; + + public static TootCard parse( LogCategory log, JSONObject src ){ + if( src==null) return null; + try{ + TootCard dst = new TootCard(); + dst.url = Utils.optStringX( src, "url" ); + dst.title = Utils.optStringX( src, "title" ); + dst.description = Utils.optStringX( src, "description" ); + dst.image = Utils.optStringX( src, "image" ); + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e(ex,"TootCard.parse failed."); + return null; + } + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootContext.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootContext.java new file mode 100644 index 00000000..309802dd --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootContext.java @@ -0,0 +1,28 @@ +package jp.juggler.subwaytooter.api.entity; + +import org.json.JSONObject; + +import jp.juggler.subwaytooter.util.LogCategory; + +public class TootContext { + + // The ancestors of the status in the conversation, as a list of Statuses + public TootStatus.List ancestors; + + // descendants The descendants of the status in the conversation, as a list of Statuses + public TootStatus.List descendants; + + public static TootContext parse( LogCategory log, JSONObject src ){ + if( src==null) return null; + try{ + TootContext dst = new TootContext(); + dst.ancestors = TootStatus.parseList( log,src.optJSONArray( "ancestors" ) ); + dst.descendants = TootStatus.parseList(log, src.optJSONArray( "descendants" ) ); + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e(ex,"TootContext.parse failed."); + return null; + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootError.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootError.java new file mode 100644 index 00000000..0f82a952 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootError.java @@ -0,0 +1,26 @@ +package jp.juggler.subwaytooter.api.entity; + +import org.json.JSONObject; + +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class TootError { + + // A textual description of the error + public String error; + + public static TootError parse( LogCategory log, JSONObject src ){ + if( src==null ) return null; + try{ + TootError dst = new TootError(); + dst.error = Utils.optStringX( src, "error" ); + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e(ex,"TootError.parse failed."); + return null; + } + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.java new file mode 100644 index 00000000..21ed3bea --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.java @@ -0,0 +1,37 @@ +package jp.juggler.subwaytooter.api.entity; + +import org.json.JSONObject; + +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class TootInstance { + + // URI of the current instance + public String uri; + + // The instance's title + public String title; + + // A description for the instance + public String description; + + // An email address which can be used to contact the instance administrator + public String email; + + public static TootInstance parse( LogCategory log, JSONObject src ){ + if( src == null ) return null; + try{ + TootInstance dst = new TootInstance(); + dst.uri = Utils.optStringX( src, "uri" ); + dst.title = Utils.optStringX( src, "title" ); + dst.description = Utils.optStringX( src, "description" ); + dst.email = Utils.optStringX( src, "email" ); + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e( ex, "TootInstance.parse failed." ); + return null; + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootMention.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootMention.java new file mode 100644 index 00000000..0f6e1dff --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootMention.java @@ -0,0 +1,57 @@ +package jp.juggler.subwaytooter.api.entity; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; + +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class TootMention { + + public static class List extends ArrayList< TootMention > { + + } + // URL of user's profile (can be remote) + public String url; + + // The username of the account + public String username; + + // Equals username for local users, includes @domain for remote ones + public String acct; + + // Account ID + public long id; + + public static TootMention parse( LogCategory log, JSONObject src ){ + if( src == null ) return null; + try{ + TootMention dst = new TootMention(); + dst.url = Utils.optStringX( src, "url" ); + dst.username = Utils.optStringX( src, "username" ); + dst.acct = Utils.optStringX( src, "acct" ); + dst.id = src.optLong( "id" ); + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e( ex, "TootMention.parse failed." ); + return null; + } + } + + + public static List parseList( LogCategory log, JSONArray array ){ + List result = new List(); + if( array != null ){ + for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + JSONObject src = array.optJSONObject( i ); + if( src == null ) continue; + TootMention item = parse( log, src ); + if( item != null ) result.add( 0, item ); + } + } + return result; + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.java new file mode 100644 index 00000000..5cde5efb --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.java @@ -0,0 +1,74 @@ +package jp.juggler.subwaytooter.api.entity; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; + +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class TootNotification { + + // The notification ID + public long id; + + // One of: "mention", "reblog", "favourite", "follow" + public String type; + + public static final String TYPE_MENTION = "mention"; + public static final String TYPE_REBLOG = "reblog"; + public static final String TYPE_FAVOURITE = "favourite"; + public static final String TYPE_FOLLOW = "follow"; + + // The time the notification was created + public String created_at; + + // The Account sending the notification to the user + public TootAccount account; + + // The Status associated with the notification, if applicable + public TootStatus status; + + public long time_created_at; + + public static TootNotification parse( LogCategory log, JSONObject src ){ + if( src == null ) return null; + try{ + TootNotification dst = new TootNotification(); + dst.id = src.optLong( "id" ); + dst.type = Utils.optStringX( src, "type" ); + dst.created_at = Utils.optStringX( src, "created_at" ); + dst.account = TootAccount.parse( log, src.optJSONObject( "account" ) ); + dst.status = TootStatus.parse( log, src.optJSONObject( "status" ) ); + + + dst.time_created_at = TootStatus.parseTime( log, dst.created_at ); + + + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e( ex, "TootNotification.parse failed." ); + return null; + } + } + + + public static class List extends ArrayList< TootNotification > { + + } + + public static List parseList( LogCategory log, JSONArray array ){ + List result = new List(); + if( array != null ){ + for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + JSONObject src = array.optJSONObject( i ); + if( src == null ) continue; + TootNotification item = parse( log, src ); + if( item != null ) result.add( 0, item ); + } + } + return result; + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootRelationShip.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootRelationShip.java new file mode 100644 index 00000000..a0b94975 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootRelationShip.java @@ -0,0 +1,45 @@ +package jp.juggler.subwaytooter.api.entity; + +import org.json.JSONObject; + +import jp.juggler.subwaytooter.util.LogCategory; + +public class TootRelationShip { + + // Target account id + public long id; + + // Whether the user is currently following the account + public boolean following; + + // Whether the user is currently being followed by the account + public boolean followed_by; + + // Whether the user is currently blocking the account + public boolean blocking; + + // Whether the user is currently muting the account + public boolean muting; + + // Whether the user has requested to follow the account + public boolean requested; + + public static TootRelationShip parse( LogCategory log, JSONObject src ){ + if( src == null ) return null; + try{ + TootRelationShip dst = new TootRelationShip(); + dst.id = src.optLong( "id" ); + dst.following = src.optBoolean( "following" ); + dst.followed_by = src.optBoolean( "followed_by" ); + dst.blocking = src.optBoolean( "blocking" ); + dst.muting = src.optBoolean( "muting" ); + dst.requested = src.optBoolean( "requested" ); + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e(ex,"TootRelationShip.parse failed."); + return null; + } + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootReport.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootReport.java new file mode 100644 index 00000000..063b570a --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootReport.java @@ -0,0 +1,49 @@ +package jp.juggler.subwaytooter.api.entity; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; + +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class TootReport { + + // The ID of the report + public long id; + + // The action taken in response to the report + public String action_taken; + + public static TootReport parse( LogCategory log, JSONObject src ){ + if( src == null ) return null; + try{ + TootReport dst = new TootReport(); + dst.id = src.optLong( "id" ); + dst.action_taken = Utils.optStringX( src, "action_taken" ); + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e( ex, "TootReport.parse failed." ); + return null; + } + } + + public static class List extends ArrayList< TootReport > { + + } + + public static List parseList( LogCategory log, JSONArray array ){ + List result = new List(); + if( array != null ){ + for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + JSONObject src = array.optJSONObject( i ); + if( src == null ) continue; + TootReport item = parse( log, src ); + if( item != null ) result.add( 0, item ); + } + } + return result; + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootResults.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootResults.java new file mode 100644 index 00000000..affc9048 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootResults.java @@ -0,0 +1,34 @@ +package jp.juggler.subwaytooter.api.entity; + +import org.json.JSONObject; + +import java.util.ArrayList; + +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class TootResults { + // An array of matched Accounts + public TootAccount.List accounts; + + // An array of matchhed Statuses + public TootStatus.List statuses; + + // An array of matched hashtags, as strings + public ArrayList< String > hashtags; + + public static TootResults parse( LogCategory log, JSONObject src ){ + if( src == null ) return null; + try{ + TootResults dst = new TootResults(); + dst.accounts = TootAccount.parseList( log, src.optJSONArray( "accounts" ) ); + dst.statuses = TootStatus.parseList( log, src.optJSONArray( "statuses" ) ); + dst.hashtags = Utils.parseStringArray( log, src.optJSONArray( "hashtags" ) ); + return dst; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e( ex, "TootResults.parse failed." ); + return null; + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.java new file mode 100644 index 00000000..b6affb08 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.java @@ -0,0 +1,160 @@ +package jp.juggler.subwaytooter.api.entity; + +import android.text.TextUtils; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; + +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class TootStatus { + + public static class List extends ArrayList< TootStatus > { + + } + + // The ID of the status + public long id; + + // A Fediverse-unique resource ID + public String uri; + + //URL to the status page (can be remote) + public String url; + + // The TootAccount which posted the status + public TootAccount account; + + // null or the ID of the status it replies to + public String in_reply_to_id; + + // null or the ID of the account it replies to + public String in_reply_to_account_id; + + // null or the reblogged Status + public TootStatus reblog; + + // Body of the status; this will contain HTML (remote HTML already sanitized) + public String content; + + // The time the status was created + public String created_at; + + //The number of reblogs for the status + public long reblogs_count; + + //The number of favourites for the status + public long favourites_count; + + // Whether the authenticated user has reblogged the status + public boolean reblogged; + + // Whether the authenticated user has favourited the status + public boolean favourited; + + //Whether media attachments should be hidden by default + public boolean sensitive; + + //If not empty, warning text that should be displayed before the actual content + public String spoiler_text; + + //One of: public, unlisted, private, direct + public String visibility; + + // An array of Attachments + public TootAttachment.List media_attachments; + + // An array of Mentions + public TootMention.List mentions; + + //An array of Tags + public ArrayList tags; + + //Application from which the status was posted + public String application; + + public long time_created_at; + + public static TootStatus parse( LogCategory log, JSONObject src ){ + + if( src == null ) return null; + + try{ + TootStatus status = new TootStatus(); + // log.d( "parse: %s", src.toString() ); + status.id = src.optLong( "id" ); + status.uri = Utils.optStringX( src, "uri" ); + status.url = Utils.optStringX( src, "url" ); + status.account = TootAccount.parse( log, src.optJSONObject( "account" ) ); + status.in_reply_to_id = Utils.optStringX( src, "in_reply_to_id" ); // null + status.in_reply_to_account_id = Utils.optStringX( src, "in_reply_to_account_id" ); // null + status.reblog = TootStatus.parse( log, src.optJSONObject( "reblog" )); + status.content = Utils.optStringX( src, "content" ); + status.created_at = Utils.optStringX( src, "created_at" ); // "2017-04-16T09:37:14.000Z" + status.reblogs_count = src.optLong( "reblogs_count" ); + status.favourites_count = src.optLong( "favourites_count" ); + status.reblogged = src.optBoolean( "reblogged" ); + status.favourited = src.optBoolean( "favourited" ); + status.sensitive = src.optBoolean( "sensitive" ); // false + status.spoiler_text = Utils.optStringX( src, "spoiler_text" ); // "",null, or CW text + status.visibility = Utils.optStringX( src, "visibility" ); + status.media_attachments = TootAttachment.parseList( log, src.optJSONArray( "media_attachments" ) ); + status.mentions = TootMention.parseList( log, src.optJSONArray( "mentions" )); + status.tags = Utils.parseStringArray( log, src.optJSONArray( "tags" )); + status.application = Utils.optStringX( src, "application" ); // null + + status.time_created_at = parseTime( log, status.created_at ); + + return status; + }catch( Throwable ex ){ + ex.printStackTrace(); + log.e( ex, "TootStatus.parse failed." ); + return null; + } + } + + public static List parseList( LogCategory log, JSONArray array ){ + List result = new List(); + if( array != null ){ + for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + JSONObject src = array.optJSONObject( i ); + if( src == null ) continue; + TootStatus item = parse( log, src ); + if( item != null ) result.add( 0, item ); + } + } + return result; + } + + private static final SimpleDateFormat date_format_utc = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault() ); + + public static long parseTime( LogCategory log, String strTime ){ + if( ! TextUtils.isEmpty( strTime ) ){ + try{ + date_format_utc.setTimeZone( TimeZone.getTimeZone( "GMT" ) ); + return date_format_utc.parse( strTime ).getTime(); + }catch( ParseException ex ){ + ex.printStackTrace(); + log.e( ex, "TootStatus.parseTime failed." ); + } + } + return 0L; + } + + private static final SimpleDateFormat date_format = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss", Locale.getDefault() ); + + public static String formatTime( long t ){ + date_format.setTimeZone( TimeZone.getDefault() ); + return date_format.format( new Date( t ) ); + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/dialog/AccountPicker.java b/app/src/main/java/jp/juggler/subwaytooter/dialog/AccountPicker.java new file mode 100644 index 00000000..9e93889e --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/dialog/AccountPicker.java @@ -0,0 +1,54 @@ +package jp.juggler.subwaytooter.dialog; + +import android.app.AlertDialog; +import android.content.DialogInterface; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +import jp.juggler.subwaytooter.ActMain; +import jp.juggler.subwaytooter.R; +import jp.juggler.subwaytooter.table.SavedAccount; + +public class AccountPicker { + + public interface AccountPickerCallback{ + void onAccountPicked(SavedAccount ai); + } + + public static void pick( ActMain activity, final AccountPickerCallback callback){ + + final ArrayList account_list = SavedAccount.loadAccountList(ActMain.log); + + Collections.sort( account_list, new Comparator< SavedAccount >() { + @Override + public int compare( SavedAccount o1, SavedAccount o2 ){ + int i = String.CASE_INSENSITIVE_ORDER.compare( o1.acct, o2.acct ); + if( i != 0 ) return i; + return String.CASE_INSENSITIVE_ORDER.compare( o1.host, o2.host ); + } + } ); + + String[] caption_list = new String[ account_list.size() ]; + + for(int i=0,ie=account_list.size();i= 0 && which < account_list.size() ){ + callback.onAccountPicked(account_list.get(which)); + dialog.dismiss(); + } + } + } ) + .setTitle( R.string.account_pick ) + .show(); + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/dialog/LoginForm.java b/app/src/main/java/jp/juggler/subwaytooter/dialog/LoginForm.java new file mode 100644 index 00000000..7861dcdc --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/dialog/LoginForm.java @@ -0,0 +1,61 @@ +package jp.juggler.subwaytooter.dialog; + +import android.app.Dialog; +import android.text.TextUtils; +import android.view.View; +import android.view.WindowManager; +import android.widget.EditText; + +import jp.juggler.subwaytooter.ActMain; +import jp.juggler.subwaytooter.R; +import jp.juggler.subwaytooter.util.Utils; + +/** + * Created by tateisu on 2017/04/16. + */ + +public class LoginForm { + + public interface LoginFormCallback{ + void startLogin(Dialog dialog,String instance,String user_main,String password); + } + + public static void showLoginForm(final ActMain activity,final LoginFormCallback callback){ + final View view = activity.getLayoutInflater().inflate( R.layout.dlg_account_add, null, false ); + final EditText etInstance = (EditText) view.findViewById( R.id.etInstance ); + final EditText etUserMail = (EditText) view.findViewById( R.id.etUserMail ); + final EditText etUserPassword = (EditText) view.findViewById( R.id.etUserPassword ); + final Dialog dialog = new Dialog( activity ); + dialog.setContentView( view ); + view.findViewById( R.id.btnOk ).setOnClickListener( new View.OnClickListener() { + @Override + public void onClick( View v ){ + final String instance = etInstance.getText().toString().trim(); + final String user_mail = etUserMail.getText().toString().trim(); + final String password = etUserPassword.getText().toString().trim(); + if( TextUtils.isEmpty( instance ) ){ + Utils.showToast( activity, true, R.string.instance_not_specified ); + return; + } + if( TextUtils.isEmpty( user_mail ) ){ + Utils.showToast(activity, true, R.string.mail_not_specified ); + return; + } + if( TextUtils.isEmpty( password ) ){ + Utils.showToast( activity, true, R.string.password_not_specified ); + return; + } + callback.startLogin( dialog,instance,user_mail,password ); + } + } ); + view.findViewById( R.id.btnCancel ).setOnClickListener( new View.OnClickListener() { + @Override + public void onClick( View v ){ + dialog.cancel(); + } + } ); + //noinspection ConstantConditions + dialog.getWindow().setLayout( WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT ); + dialog.show(); + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/AccessToken.java b/app/src/main/java/jp/juggler/subwaytooter/table/AccessToken.java new file mode 100644 index 00000000..8edce58a --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/table/AccessToken.java @@ -0,0 +1,78 @@ +package jp.juggler.subwaytooter.table; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +import jp.juggler.subwaytooter.App1; +import jp.juggler.subwaytooter.util.LogCategory; + +public class AccessToken { + + static final LogCategory log = new LogCategory( "AccessToken" ); + + static final String table = "access_token"; + + static final String COL_HOST = "h"; + static final String COL_USER_MAIL = "um"; + static final String COL_TOKEN = "t"; + + public String host; + public String user_mail; + + public static void onDBCreate( SQLiteDatabase db ){ + db.execSQL( + "create table if not exists " + table + + "(_id INTEGER PRIMARY KEY" + + ",h text not null" + + ",um text not null" + + ",t text not null" + + ")" + ); + db.execSQL( + "create unique index if not exists " + table + "_host on " + table + + "(h" + + ",um" + + ")" + ); + } + + public static void onDBUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){ + + } + + public static JSONObject load( String instance, String user_mail ){ + try{ + Cursor cursor = App1.getDB().query( table, null, "h=? and um=?", new String[]{ instance, user_mail }, null, null, null ); + try{ + if( cursor.moveToFirst() ){ + return new JSONObject( cursor.getString( cursor.getColumnIndex( COL_TOKEN ) ) ); + } + }finally{ + cursor.close(); + } + }catch( Throwable ex ){ + log.e( ex, "load failed." ); + } + return null; + } + + public static void save( String host, String user_mail, String json ){ + try{ + ContentValues cv = new ContentValues(); + cv.put( COL_HOST, host ); + cv.put( COL_USER_MAIL, user_mail ); + cv.put( COL_TOKEN, json ); + App1.getDB().replace( table, null, cv ); + }catch( Throwable ex ){ + log.e( ex, "save failed." ); + } + } + + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/ClientInfo.java b/app/src/main/java/jp/juggler/subwaytooter/table/ClientInfo.java new file mode 100644 index 00000000..429ad3ea --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/table/ClientInfo.java @@ -0,0 +1,64 @@ +package jp.juggler.subwaytooter.table; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import org.json.JSONObject; + +import jp.juggler.subwaytooter.App1; +import jp.juggler.subwaytooter.util.LogCategory; + +public class ClientInfo { + static final LogCategory log = new LogCategory( "ClientInfo" ); + + static final String table = "client_info"; + static final String COL_HOST = "h"; + static final String COL_RESULT = "r"; + + public static void onDBCreate( SQLiteDatabase db ){ + db.execSQL( + "create table if not exists " + table + + "(_id INTEGER PRIMARY KEY" + + ",h text not null" + + ",r text not null" + + ")" + ); + db.execSQL( + "create unique index if not exists " + table + "_host on " + table + + "(h" + + ")" + ); + } + + public static void onDBUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){ + + } + + public static JSONObject load( String instance ){ + try{ + Cursor cursor = App1.getDB().query( table, null, "h=?", new String[]{ instance }, null, null, null ); + try{ + if( cursor.moveToFirst() ){ + return new JSONObject( cursor.getString( cursor.getColumnIndex( COL_RESULT ) ) ); + } + }finally{ + cursor.close(); + } + }catch( Throwable ex ){ + log.e( ex, "load failed." ); + } + return null; + } + + public static void save( String host, String json ){ + try{ + ContentValues cv = new ContentValues(); + cv.put( COL_HOST, host ); + cv.put( COL_RESULT, json ); + App1.getDB().replace( table, null, cv ); + }catch( Throwable ex ){ + log.e( ex, "save failed." ); + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/LogData.java b/app/src/main/java/jp/juggler/subwaytooter/table/LogData.java new file mode 100644 index 00000000..f13c1b75 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/table/LogData.java @@ -0,0 +1,84 @@ +package jp.juggler.subwaytooter.table; + +import android.content.ContentValues; +import android.database.sqlite.SQLiteDatabase; +import android.util.Log; + +import jp.juggler.subwaytooter.App1; + +public class LogData { + static final String TAG = "SubwayTooter"; + + static final String table = "log"; + + public static final String COL_TIME = "t"; + public static final String COL_LEVEL = "l"; + public static final String COL_CATEGORY = "c"; + public static final String COL_MESSAGE = "m"; + + public static final int LEVEL_ERROR = 100; + public static final int LEVEL_WARNING = 200; + public static final int LEVEL_INFO = 300; + public static final int LEVEL_VERBOSE = 400; + public static final int LEVEL_DEBUG = 500; + public static final int LEVEL_HEARTBEAT = 600; + public static final int LEVEL_FLOOD = 700; + + + + public static void onDBCreate( SQLiteDatabase db ){ + db.execSQL( + "create table if not exists " + table + + "(_id INTEGER PRIMARY KEY" + + ",t integer not null" + + ",l integer not null" + + ",c text not null" + + ",m text not null" + + ")" + ); + db.execSQL( + "create index if not exists " + table + "_time on " + table + + "(t" + + ",l" + + ")" + ); + } + + public static void onDBUpgrade( SQLiteDatabase db, int v_old, int v_new ){ + + } + + public static long insert( ContentValues cv, long time, int level,String category, String message ){ + Log.d( TAG,category+": "+message); + try{ + cv.clear(); + cv.put( COL_TIME, time ); + cv.put( COL_LEVEL, level ); + cv.put( COL_MESSAGE, message ); + cv.put( COL_CATEGORY, category ); + return App1.getDB().insert( table, null, cv ); + }catch( Throwable ex ){ + ex.printStackTrace(); + return - 1L; + } + } + + public static String getLogLevelString( int level ){ + if( level >= LogData.LEVEL_FLOOD ){ + return "Flood"; + }else if( level >= LogData.LEVEL_HEARTBEAT ){ + return "HeartBeat"; + }else if( level >= LogData.LEVEL_DEBUG ){ + return "Debug"; + }else if( level >= LogData.LEVEL_VERBOSE ){ + return "Verbose"; + }else if( level >= LogData.LEVEL_INFO ){ + return "Info"; + }else if( level >= LogData.LEVEL_WARNING ){ + return "Warning"; + }else{ + return "Error"; + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.java b/app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.java new file mode 100644 index 00000000..1fe2fe90 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.java @@ -0,0 +1,125 @@ +package jp.juggler.subwaytooter.table; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +import jp.juggler.subwaytooter.App1; +import jp.juggler.subwaytooter.api.entity.TootAccount; +import jp.juggler.subwaytooter.util.LogCategory; + +public class SavedAccount extends TootAccount{ + + static final String table = "access_info"; + + static final String COL_HOST = "h"; + static final String COL_USER_MAIL = "um"; + static final String COL_ACCOUNT = "a"; + static final String COL_LOGIN_REQUIRED = "lr"; + + // login information + public String host; + public String user_mail; + public boolean login_required; + + + public static void onDBCreate( SQLiteDatabase db ){ + db.execSQL( + "create table if not exists " + table + + "(_id INTEGER PRIMARY KEY" + + ",h text not null" + + ",um text not null" + + ",a text not null" + + ",lr integer default 0" + + ")" + ); + db.execSQL( + "create unique index if not exists " + table + "_host on " + table + + "(h" + + ",um" + + ")" + ); + } + + public static void onDBUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){ + + } + + private static SavedAccount parse( LogCategory log, Cursor cursor ) throws JSONException{ + JSONObject src = new JSONObject( cursor.getString( cursor.getColumnIndex( COL_ACCOUNT ) ) ); + SavedAccount dst = (SavedAccount)parse(log,src,new SavedAccount()); + if( dst != null){ + dst.host = cursor.getString( cursor.getColumnIndex( COL_HOST ) ); + dst.user_mail = cursor.getString( cursor.getColumnIndex( COL_USER_MAIL ) ); + dst.login_required = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_LOGIN_REQUIRED ) ) ); + } + return dst; + } + + + public static void save( LogCategory log,String instance, String user_mail, JSONObject data ){ + try{ + ContentValues cv = new ContentValues(); + cv.put( COL_HOST, instance ); + cv.put( COL_USER_MAIL, user_mail ); + cv.put( COL_ACCOUNT, data.toString() ); + App1.getDB().replace( table, null, cv ); + }catch( Throwable ex ){ + log.e( ex, "saveAccount failed." ); + } + } + + public static SavedAccount loadAccount( LogCategory log, String instance, String user_mail ){ + try{ + Cursor cursor = App1.getDB().query( table, null, "h=? and um=?", new String[]{ instance, user_mail }, null, null, null ); + try{ + if( cursor.moveToFirst() ){ + return parse( log,cursor ); + } + }finally{ + cursor.close(); + } + }catch( Throwable ex ){ + log.e( ex, "loadToken failed." ); + } + return null; + } + + public static ArrayList< SavedAccount > loadAccountList(LogCategory log){ + ArrayList< SavedAccount > result = new ArrayList<>(); + + try{ + Cursor cursor = App1.getDB().query( table, null, null, null, null, null, null ); + try{ + while( cursor.moveToNext() ){ + result.add( parse( log,cursor ) ); + } + return result; + }finally{ + cursor.close(); + } + }catch( Throwable ex ){ + log.e( ex, "loadAccountList failed." ); + } + return null; + } + + public static boolean hasAccount( LogCategory log,String instance, String user_mail ){ + return null != loadAccount( log,instance,user_mail ); + } + + public String getFullAcct(TootAccount who ){ + if( who== null || who.acct ==null ) return "@?"; + if( -1 != who.acct.indexOf( '@' ) ){ + return "@" + who.acct; + }else{ + return "@"+ who.acct +"@"+ this.host; + } + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/CancelChecker.java b/app/src/main/java/jp/juggler/subwaytooter/util/CancelChecker.java new file mode 100644 index 00000000..f29393ab --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/util/CancelChecker.java @@ -0,0 +1,5 @@ +package jp.juggler.subwaytooter.util; + +public interface CancelChecker { + boolean isCancelled(); +} \ No newline at end of file diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/HTTPClient.java b/app/src/main/java/jp/juggler/subwaytooter/util/HTTPClient.java new file mode 100644 index 00000000..670fa6cc --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/util/HTTPClient.java @@ -0,0 +1,692 @@ +package jp.juggler.subwaytooter.util; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ConnectException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.net.ssl.SSLHandshakeException; + +import android.os.SystemClock; + +//! リトライつきHTTPクライアント +public class HTTPClient { + + static final boolean debug_http = false; + + public String[] extra_header; + public int rcode; + public boolean allow_error = false; + public Map< String, List< String > > response_header; + public HashMap< String, String > cookie_pot; + public int max_try; + @SuppressWarnings("unused") + public int timeout_dns = 1000 * 3; + public int timeout_connect; + public int timeout_read; + public String caption; + public boolean silent_error = false; + public long time_expect_connect = 3000; + public boolean bDisableKeepAlive = false; + + @SuppressWarnings("unused") + public HTTPClient( int timeout, int max_try, String caption, CancelChecker cancel_checker ){ + this.cancel_checker = cancel_checker; + this.timeout_connect = this.timeout_read = timeout; + this.max_try = max_try; + this.caption = caption; + } + + @SuppressWarnings("unused") + public HTTPClient( int timeout, int max_try, String caption, final AtomicBoolean _cancel_checker ){ + this.cancel_checker = new CancelChecker() { + @Override + public boolean isCancelled(){ + return _cancel_checker.get(); + } + }; + this.timeout_connect = this.timeout_read = timeout; + this.max_try = max_try; + this.caption = caption; + } + + @SuppressWarnings("unused") + public void setCookiePot( boolean enabled ){ + if( enabled == ( cookie_pot != null ) ) return; + cookie_pot = ( enabled ? new HashMap< String, String >() : null ); + } + + /////////////////////////////// + // デフォルトの入力ストリームハンドラ + + HTTPClientReceiver default_receiver = new HTTPClientReceiver() { + byte[] buf = new byte[ 2048 ]; + ByteArrayOutputStream bao = new ByteArrayOutputStream( 0 ); + + public byte[] onHTTPClientStream( LogCategory log, CancelChecker cancel_checker, InputStream in, int content_length ){ + try{ + bao.reset(); + for( ; ; ){ + if( cancel_checker.isCancelled() ){ + if( debug_http ) log.w( + "[%s,read]cancelled!" + , caption + ); + return null; + } + int delta = in.read( buf ); + if( delta <= 0 ) break; + bao.write( buf, 0, delta ); + } + return bao.toByteArray(); + }catch( Throwable ex ){ + log.e( + "[%s,read] %s:%s" + , caption + , ex.getClass().getSimpleName() + , ex.getMessage() + ); + } + return null; + } + }; + + /////////////////////////////// + // 別スレッドからのキャンセル処理 + + public CancelChecker cancel_checker; + volatile Thread io_thread; + + @SuppressWarnings("unused") + public boolean isCancelled(){ + return cancel_checker.isCancelled(); + } + + @SuppressWarnings("unused") + public synchronized void cancel( LogCategory log ){ + Thread t = io_thread; + if( t == null ) return; + log.i( + "[%s,cancel] %s" + , caption + , t + ); + try{ + t.interrupt(); + }catch( Throwable ex ){ + ex.printStackTrace(); + } + } + + public byte[] post_content = null; + public String post_content_type = null; + public boolean quit_network_error = false; + + public String last_error = null; + public long mtime; + + public static String user_agent = null; + + /////////////////////////////// + // HTTPリクエスト処理 + + @SuppressWarnings("unused") + public byte[] getHTTP( LogCategory log, String url ){ + return getHTTP( log, url, default_receiver ); + } + + @SuppressWarnings("ConstantConditions") + public byte[] getHTTP( LogCategory log, String url, HTTPClientReceiver receiver ){ + +// // http://android-developers.blogspot.jp/2011/09/androids-http-clients.html +// // HTTP connection reuse which was buggy pre-froyo +// if( Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO ){ +// System.setProperty( "http.keepAlive", "false" ); +// } + + try{ + synchronized( this ){ + this.io_thread = Thread.currentThread(); + } + URL urlObject; + try{ + urlObject = new URL( url ); + }catch( MalformedURLException ex ){ + log.d( "[%s,init] bad url %s %s", caption, url, ex.getMessage() ); + return null; + } +/* + // desire だと、どうもリソースリークしているようなので行わないことにした。 + // DNSを引けるか確認する + if(debug_http) Log.d(logcat,"check hostname "+url); + if( !checkDNSResolver(urlObject) ){ + Log.w(logcat,"broken name resolver"); + return null; + } +*/ + long timeStart = SystemClock.elapsedRealtime(); + for( int nTry = 0 ; nTry < max_try ; ++ nTry ){ + long t1, t2, lap; + try{ + this.rcode = 0; + // キャンセルされたか確認 + if( cancel_checker.isCancelled() ) return null; + + // http connection + HttpURLConnection conn = (HttpURLConnection) urlObject.openConnection(); + + if( user_agent != null ) conn.setRequestProperty( "User-Agent", user_agent ); + + // 追加ヘッダがあれば記録する + if( extra_header != null ){ + for( int i = 0 ; i < extra_header.length ; i += 2 ){ + conn.addRequestProperty( extra_header[ i ], extra_header[ i + 1 ] ); + if( debug_http ) + log.d( "%s: %s", extra_header[ i ], extra_header[ i + 1 ] ); + } + } + if( bDisableKeepAlive ){ + conn.setRequestProperty( "Connection", "close" ); + } + // クッキーがあれば指定する + if( cookie_pot != null ){ + StringBuilder sb = new StringBuilder(); + for( Map.Entry< String, String > pair : cookie_pot.entrySet() ){ + if( sb.length() > 0 ) sb.append( "; " ); + sb.append( pair.getKey() ); + sb.append( '=' ); + sb.append( pair.getValue() ); + } + conn.addRequestProperty( "Cookie", sb.toString() ); + } + + // リクエストを送ってレスポンスの頭を読む + try{ + t1 = SystemClock.elapsedRealtime(); + if( debug_http ) + log.d( "[%s,connect] start %s", caption, toHostName( url ) ); + conn.setDoInput( true ); + conn.setConnectTimeout( this.timeout_connect ); + conn.setReadTimeout( this.timeout_read ); + if( post_content == null ){ + conn.setDoOutput( false ); + conn.connect(); + }else{ + conn.setDoOutput( true ); +// if( Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB ){ +// conn.setRequestProperty( "Content-Length", Integer.toString( post_content.length ) ); +// } + if( post_content_type != null ){ + conn.setRequestProperty( "Content-Type", post_content_type ); + } + OutputStream out = conn.getOutputStream(); + out.write( post_content ); + out.flush(); + out.close(); + } + // http://stackoverflow.com/questions/12931791/java-io-ioexception-received-authentication-challenge-is-null-in-ics-4-0-3 + int rcode; + try{ + // Will throw IOException if server responds with 401. + rcode = this.rcode = conn.getResponseCode(); + }catch( IOException ex ){ + String sv = ex.getMessage(); + if( sv != null && sv.contains( "authentication challenge" ) ){ + log.d( "retry getResponseCode!" ); + // Will return 401, because now connection has the correct internal state. + rcode = this.rcode = conn.getResponseCode(); + }else{ + throw ex; + } + } + mtime = conn.getLastModified(); + t2 = SystemClock.elapsedRealtime(); + lap = t2 - t1; + if( lap > time_expect_connect ) + log.d( "[%s,connect] time=%sms %s", caption, lap, toHostName( url ) ); + + // ヘッダを覚えておく + response_header = conn.getHeaderFields(); + + // クッキーが来ていたら覚える + if( cookie_pot != null ){ + String v = conn.getHeaderField( "set-cookie" ); + if( v != null ){ + int pos = v.indexOf( '=' ); + cookie_pot.put( v.substring( 0, pos ), v.substring( pos + 1 ) ); + } + } + + if( rcode >= 500 ){ + if( ! silent_error ) + log.e( "[%s,connect] temporary error %d", caption, rcode ); + last_error = String.format( "(HTTP error %d)", rcode ); + continue; + }else if( ! allow_error && rcode >= 300 ){ + if( ! silent_error ) + log.e( "[%s,connect] permanent error %d", caption, rcode ); + last_error = String.format( "(HTTP error %d)", rcode ); + return null; + } + + }catch( UnknownHostException ex ){ + rcode = 0; + last_error = ex.getClass().getSimpleName(); + // このエラーはリトライしてもムリ + conn.disconnect(); + return null; + }catch( SSLHandshakeException ex ){ + last_error = String.format( "SSL handshake error. Please check device's date and time. (%s %s)", ex.getClass().getSimpleName(), ex.getMessage() ); + + if( ! silent_error ){ + log.e( "[%s,connect] %s" + , caption + , last_error + ); + if( ex.getMessage() == null ){ + ex.printStackTrace(); + } + } + this.rcode = - 1; + return null; + }catch( Throwable ex ){ + last_error = String.format( "%s %s", ex.getClass().getSimpleName(), ex.getMessage() ); + + if( ! silent_error ){ + log.e( "[%s,connect] %s" + , caption + , last_error + ); + if( ex.getMessage() == null ){ + ex.printStackTrace(); + } + } + + // 時計が合ってない場合は Received authentication challenge is null なエラーが出るらしい + // getting a 401 Unauthorized error, due to a malformed Authorization header. + if( ex instanceof IOException + && ex.getMessage() != null + && ex.getMessage().contains( "authentication challenge" ) + ){ + ex.printStackTrace(); + log.d( "Please check device's date and time." ); + this.rcode = 401; + return null; + }else if( ex instanceof ConnectException + && ex.getMessage() != null + && ex.getMessage().contains( "ENETUNREACH" ) + ){ + // このアプリの場合は network unreachable はリトライしない + return null; + } + if( quit_network_error ) return null; + + // 他のエラーはリトライしてみよう。キャンセルされたなら次のループの頭で抜けるはず + conn.disconnect(); + continue; + } + InputStream in = null; + try{ + if( debug_http ) if( rcode != 200 ) + log.d( "[%s,read] start status=%d", caption, this.rcode ); + try{ + in = conn.getInputStream(); + }catch( FileNotFoundException ex ){ + in = conn.getErrorStream(); + } + if( in == null ){ + log.d( "[%s,read] missing input stream. rcode=%d", caption, rcode ); + return null; + } + int content_length = conn.getContentLength(); + byte[] data = receiver.onHTTPClientStream( log, cancel_checker, in, content_length ); + if( data == null ) continue; + if( data.length > 0 ){ + if( nTry > 0 ) log.w( "[%s] OK. retry=%d,time=%dms" + , caption + , nTry + , SystemClock.elapsedRealtime() - timeStart + ); + return data; + } + if( ! cancel_checker.isCancelled() + && ! silent_error + ){ + log.w( + "[%s,read] empty data." + , caption + ); + } + }finally{ + try{ + if( in != null ) in.close(); + }catch( Throwable ignored ){ + } + conn.disconnect(); + } + }catch( Throwable ex ){ + last_error = String.format( "%s %s", ex.getClass().getSimpleName(), ex.getMessage() ); + ex.printStackTrace(); + } + } + if( ! silent_error ) log.e( "[%s] fail. try=%d. rcode=%d", caption, max_try, rcode ); + }catch( Throwable ex ){ + ex.printStackTrace(); + last_error = String.format( "%s %s", ex.getClass().getSimpleName(), ex.getMessage() ); + }finally{ + synchronized( this ){ + io_thread = null; + } + } + return null; + } + + //! HTTPレスポンスのヘッダを読む + @SuppressWarnings("unused") + public void dump_res_header( LogCategory log ){ + log.d( "HTTP code %d", rcode ); + if( response_header != null ){ + for( Map.Entry< String, List< String > > entry : response_header.entrySet() ){ + String k = entry.getKey(); + for( String v : entry.getValue() ){ + log.d( "%s: %s", k, v ); + } + } + } + } + + @SuppressWarnings({ "unused", "ConstantConditions" }) + public String get_cache( LogCategory log, File file, String url ){ + String last_error = null; + for( int nTry = 0 ; nTry < 10 ; ++ nTry ){ + if( cancel_checker.isCancelled() ) return "cancelled"; + + long now = System.currentTimeMillis(); + try{ + HttpURLConnection conn = (HttpURLConnection) new URL( url ).openConnection(); + try{ + conn.setConnectTimeout( 1000 * 10 ); + conn.setReadTimeout( 1000 * 10 ); + if( file.exists() ) conn.setIfModifiedSince( file.lastModified() ); + conn.connect(); + this.rcode = conn.getResponseCode(); + if( rcode == 304 ){ + if( file.exists() ){ + //noinspection ResultOfMethodCallIgnored + file.setLastModified( now ); + } + return null; + } + if( rcode == 200 ){ + InputStream in = conn.getInputStream(); + try{ + ByteArrayOutputStream bao = new ByteArrayOutputStream(); + try{ + byte[] tmp = new byte[ 4096 ]; + for( ; ; ){ + if( cancel_checker.isCancelled() ) return "cancelled"; + int delta = in.read( tmp, 0, tmp.length ); + if( delta <= 0 ) break; + bao.write( tmp, 0, delta ); + } + byte[] data = bao.toByteArray(); + if( data != null ){ + FileOutputStream out = new FileOutputStream( file ); + try{ + out.write( data ); + return null; + }finally{ + try{ + out.close(); + }catch( Throwable ignored ){ + } + } + } + }finally{ + try{ + bao.close(); + }catch( Throwable ignored ){ + } + } + }catch( Throwable ex ){ + ex.printStackTrace(); + if( file.exists() ){ + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + last_error = String.format( "%s %s", ex.getClass().getSimpleName(), ex.getMessage() ); + }finally{ + try{ + in.close(); + }catch( Throwable ignored ){ + } + } + break; + } + log.e( "http error: %d %s", rcode, url ); + if( rcode >= 400 && rcode < 500 ){ + last_error = String.format( "HTTP error %d", rcode ); + break; + } + }finally{ + conn.disconnect(); + } + // retry ? + }catch( MalformedURLException ex ){ + ex.printStackTrace(); + last_error = String.format( "bad URL:%s", ex.getMessage() ); + break; + }catch( IOException ex ){ + ex.printStackTrace(); + last_error = String.format( "%s %s", ex.getClass().getSimpleName(), ex.getMessage() ); + } + } + return last_error; + } + ///////////////////////////////////////////////////////// + // 複数URLに対応したリクエスト処理 + + public boolean no_cache = false; + + @SuppressWarnings({ "unused", "ConstantConditions" }) + public File getFile( LogCategory log, File cache_dir, String[] url_list, File _file ){ + // + if( url_list == null || url_list.length < 1 ){ + setError( 0, "missing url argument." ); + return null; + } + // make cache_dir + if( cache_dir != null ){ + if( ! cache_dir.mkdirs() && ! cache_dir.isDirectory() ){ + setError( 0, "can not create cache_dir" ); + return null; + } + } + for( int nTry = 0 ; nTry < 10 ; ++ nTry ){ + if( cancel_checker.isCancelled() ){ + setError( 0, "cancelled." ); + return null; + } + // + String url = url_list[ nTry % url_list.length ]; + File file = ( _file != null ? _file : new File( cache_dir, Utils.url2name( url ) ) ); + + // + //noinspection TryWithIdenticalCatches + try{ + HttpURLConnection conn = (HttpURLConnection) new URL( url ).openConnection(); + if( user_agent != null ) conn.setRequestProperty( "User-Agent", user_agent ); + try{ + conn.setConnectTimeout( 1000 * 10 ); + conn.setReadTimeout( 1000 * 10 ); + if( ! no_cache && file.exists() ) + conn.setIfModifiedSince( file.lastModified() ); + conn.connect(); + this.rcode = conn.getResponseCode(); + + if( debug_http ) if( rcode != 200 ) log.d( "getFile %s %s", rcode, url ); + + // 変更なしの場合 + if( rcode == 304 ){ + /// log.d("304: %s",file); + return file; + } + + // 変更があった場合 + if( rcode == 200 ){ + // メッセージボディをファイルに保存する + InputStream in = null; + FileOutputStream out = null; + try{ + byte[] tmp = new byte[ 4096 ]; + in = conn.getInputStream(); + out = new FileOutputStream( file ); + for( ; ; ){ + if( cancel_checker.isCancelled() ){ + setError( 0, "cancelled" ); + if( file.exists() ){ + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + return null; + } + int delta = in.read( tmp, 0, tmp.length ); + if( delta <= 0 ) break; + out.write( tmp, 0, delta ); + } + out.close(); + out = null; + // + long mtime = conn.getLastModified(); + if( mtime >= 1000 ){ + + //noinspection ResultOfMethodCallIgnored + file.setLastModified( mtime ); + } + // + /// log.d("200: %s",file); + return file; + }catch( Throwable ex ){ + setError( ex ); + }finally{ + try{ + if( in != null ) in.close(); + }catch( Throwable ignored ){ + } + try{ + if( out != null ) out.close(); + }catch( Throwable ignored ){ + } + } + // エラーがあったらリトライ + if( file.exists() ){ + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + + continue; + } + + // その他、よく分からないケース + log.e( "http error: %d %s", rcode, url ); + + // URLが複数提供されている場合、404エラーはリトライ対象 + if( rcode == 404 && url_list.length > 1 ){ + last_error = String.format( "(HTTP error %d)", rcode ); + continue; + } + + // それ以外の永続エラーはリトライしない + if( rcode >= 400 && rcode < 500 ){ + last_error = String.format( "(HTTP error %d)", rcode ); + break; + } + }finally{ + conn.disconnect(); + } + // retry ? + }catch( UnknownHostException ex ){ + rcode = 0; + last_error = ex.getClass().getSimpleName(); + // このエラーはリトライしてもムリ + break; + }catch( MalformedURLException ex ){ + setError( ex ); + break; + }catch( SocketTimeoutException ex ){ + setError_silent( log, ex ); + }catch( ConnectException ex ){ + setError_silent( log, ex ); + }catch( IOException ex ){ + setError( ex ); + } + } + return null; + } + + /////////////////////////////////////////////////////////////////// + + public boolean setError( int i, String string ){ + rcode = i; + last_error = string; + return false; + } + + public boolean setError( Throwable ex ){ + ex.printStackTrace(); + rcode = 0; + last_error = String.format( "%s %s", ex.getClass().getSimpleName(), ex.getMessage() ); + return false; + } + + public boolean setError_silent( LogCategory log, Throwable ex ){ + log.d( "ERROR: %s %s", ex.getClass().getName(), ex.getMessage() ); + rcode = 0; + last_error = String.format( "%s %s", ex.getClass().getSimpleName(), ex.getMessage() ); + return false; + } + + //! HTTPレスポンスのヘッダを読む + public String getHeaderString( String key, String defval ){ + List< String > list = response_header.get( key ); + if( list != null && list.size() > 0 ){ + String v = list.get( 0 ); + if( v != null ) return v; + } + return defval; + } + + //! HTTPレスポンスのヘッダを読む + @SuppressWarnings("unused") + public int getHeaderInt( String key, int defval ){ + String v = getHeaderString( key, null ); + try{ + return Integer.parseInt( v, 10 ); + }catch( Throwable ex ){ + return defval; + } + } + + static Pattern reHostName = Pattern.compile( "//([^/]+)/" ); + + static String toHostName( String url ){ + Matcher m = reHostName.matcher( url ); + if( m.find() ) return m.group( 1 ); + return url; + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/HTTPClientReceiver.java b/app/src/main/java/jp/juggler/subwaytooter/util/HTTPClientReceiver.java new file mode 100644 index 00000000..9c9dcb8c --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/util/HTTPClientReceiver.java @@ -0,0 +1,9 @@ +package jp.juggler.subwaytooter.util; + +import java.io.InputStream; + +//! HTTPClientのバッファ管理を独自に行いたい場合に使用する. +//! このインタフェースを実装したものをHTTPClient.getHTTP()の第二引数に指定する +public interface HTTPClientReceiver { + byte[] onHTTPClientStream( LogCategory log,CancelChecker cancel_checker, InputStream in, int content_length); +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/LogCategory.java b/app/src/main/java/jp/juggler/subwaytooter/util/LogCategory.java new file mode 100644 index 00000000..96dbf4c5 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/util/LogCategory.java @@ -0,0 +1,152 @@ +package jp.juggler.subwaytooter.util; + +import android.content.ContentValues; +import android.content.res.Resources; + +import jp.juggler.subwaytooter.table.LogData; + +public class LogCategory { + + final ContentValues cv = new ContentValues(); + final String category; + + public LogCategory( String category ){ + this.category = category; + } + + @SuppressWarnings("unused") + public void addLog( int level, String message ){ + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), level, category, message ); + } + } + + @SuppressWarnings("unused") + public void e( String fmt, Object... args ){ + if( args.length > 0 ) fmt = String.format( fmt, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_ERROR, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void w( String fmt, Object... args ){ + if( args.length > 0 ) fmt = String.format( fmt, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_WARNING, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void i( String fmt, Object... args ){ + if( args.length > 0 ) fmt = String.format( fmt, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_INFO, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void v( String fmt, Object... args ){ + if( args.length > 0 ) fmt = String.format( fmt, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_VERBOSE, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void d( String fmt, Object... args ){ + if( args.length > 0 ) fmt = String.format( fmt, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_DEBUG, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void h( String fmt, Object... args ){ + if( args.length > 0 ) fmt = String.format( fmt, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_HEARTBEAT, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void f( String fmt, Object... args ){ + if( args.length > 0 ) fmt = String.format( fmt, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_FLOOD, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void e( Resources res, int string_id, Object... args ){ + String fmt = res.getString( string_id, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_ERROR, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void w( Resources res, int string_id, Object... args ){ + String fmt = res.getString( string_id, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_WARNING, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void i( Resources res, int string_id, Object... args ){ + String fmt = res.getString( string_id, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_INFO, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void v( Resources res, int string_id, Object... args ){ + String fmt = res.getString( string_id, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_VERBOSE, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void d( Resources res, int string_id, Object... args ){ + String fmt = res.getString( string_id, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_DEBUG, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void h( Resources res, int string_id, Object... args ){ + String fmt = res.getString( string_id, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_HEARTBEAT, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void f( Resources res, int string_id, Object... args ){ + String fmt = res.getString( string_id, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_FLOOD, category, fmt ); + } + } + + @SuppressWarnings("unused") + public void e( Throwable ex, String fmt, Object... args ){ + if( args.length > 0 ) fmt = String.format( fmt, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_ERROR, category, fmt + String.format( ":%s %s", ex.getClass().getSimpleName(), ex.getMessage() ) ); + } + } + + @SuppressWarnings("unused") + public void e( Throwable ex, Resources res, int string_id, Object... args ){ + String fmt = res.getString( string_id, args ); + synchronized( cv ){ + LogData.insert( cv, System.currentTimeMillis(), LogData.LEVEL_ERROR, category, fmt + String.format( ":%s %s", ex.getClass().getSimpleName(), ex.getMessage() ) ); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/Utils.java b/app/src/main/java/jp/juggler/subwaytooter/util/Utils.java new file mode 100644 index 00000000..9409b52a --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/util/Utils.java @@ -0,0 +1,787 @@ +package jp.juggler.subwaytooter.util; + +import android.annotation.SuppressLint; +import android.content.Context; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.reflect.Method; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; + +import android.content.res.Resources; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Looper; +import android.os.storage.StorageManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Base64; +import android.util.SparseBooleanArray; + +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.webkit.MimeTypeMap; +import android.widget.Toast; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import java.text.DecimalFormat; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +public class Utils { + + @SuppressLint("DefaultLocale") + public static String formatTimeDuration( long t ){ + StringBuilder sb = new StringBuilder(); + long n; + // day + n = t / 86400000L; + if( n > 0 ){ + sb.append( String.format( Locale.JAPAN, "%dd", n ) ); + t -= n * 86400000L; + } + // h + n = t / 3600000L; + if( n > 0 || sb.length() > 0 ){ + sb.append( String.format( Locale.JAPAN, "%dh", n ) ); + t -= n * 3600000L; + } + // m + n = t / 60000L; + if( n > 0 || sb.length() > 0 ){ + sb.append( String.format( Locale.JAPAN, "%dm", n ) ); + t -= n * 60000L; + } + // s + float f = t / 1000f; + sb.append( String.format( Locale.JAPAN, "%.03fs", f ) ); + + return sb.toString(); + } + + static DecimalFormat bytes_format = new DecimalFormat( "#,###" ); + + public static String formatBytes( long t ){ + return bytes_format.format( t ); + +// StringBuilder sb = new StringBuilder(); +// long n; +// // giga +// n = t / 1000000000L; +// if( n > 0 ){ +// sb.append( String.format( Locale.JAPAN, "%dg", n ) ); +// t -= n * 1000000000L; +// } +// // Mega +// n = t / 1000000L; +// if( sb.length() > 0 ){ +// sb.append( String.format( Locale.JAPAN, "%03dm", n ) ); +// t -= n * 1000000L; +// }else if( n > 0 ){ +// sb.append( String.format( Locale.JAPAN, "%dm", n ) ); +// t -= n * 1000000L; +// } +// // kilo +// n = t / 1000L; +// if( sb.length() > 0 ){ +// sb.append( String.format( Locale.JAPAN, "%03dk", n ) ); +// t -= n * 1000L; +// }else if( n > 0 ){ +// sb.append( String.format( Locale.JAPAN, "%dk", n ) ); +// t -= n * 1000L; +// } +// // remain +// if( sb.length() > 0 ){ +// sb.append( String.format( Locale.JAPAN, "%03d", t ) ); +// }else if( n > 0 ){ +// sb.append( String.format( Locale.JAPAN, "%d", t ) ); +// } +// +// return sb.toString(); + } + + // public static PendingIntent createAlarmPendingIntent( Context context ){ +// Intent i = new Intent( context.getApplicationContext(), Receiver1.class ); +// i.setAction( Receiver1.ACTION_ALARM ); +// return PendingIntent.getBroadcast( context.getApplicationContext(), 0, i, 0 ); +// } +// + // 文字列とバイト列の変換 + public static byte[] encodeUTF8( String str ){ + try{ + return str.getBytes( "UTF-8" ); + }catch( Throwable ex ){ + return null; // 入力がnullの場合のみ発生 + } + } + + // 文字列とバイト列の変換 + public static String decodeUTF8( byte[] data ){ + try{ + return new String( data, "UTF-8" ); + }catch( Throwable ex ){ + return null; // 入力がnullの場合のみ発生 + } + } + + // 文字列と整数の変換 + public static int parse_int( String v, int defval ){ + try{ + return Integer.parseInt( v, 10 ); + }catch( Throwable ex ){ + return defval; + } + } + + public static String optStringX( JSONObject src, String key){ + return src.isNull( key ) ? null : src.optString( key ); + } + + public static String optStringX( JSONArray src, int key){ + return src.isNull( key ) ? null : src.optString( key ); + } + + public static ArrayList< String > parseStringArray( LogCategory log, JSONArray array ){ + ArrayList< String > dst_list = new ArrayList<>( ); + if( array != null ){ + for(int i=0,ie=array.length();i> 4 ) & 15 ] ); + sb.append( hex[ ( b ) & 15 ] ); + } + + public static int hex2int( int c ){ + switch( c ){ + default: + return 0; + case '0': + return 0; + case '1': + return 1; + case '2': + return 2; + case '3': + return 3; + case '4': + return 4; + case '5': + return 5; + case '6': + return 6; + case '7': + return 7; + case '8': + return 8; + case '9': + return 9; + case 'a': + return 0xa; + case 'b': + return 0xb; + case 'c': + return 0xc; + case 'd': + return 0xd; + case 'e': + return 0xe; + case 'f': + return 0xf; + case 'A': + return 0xa; + case 'B': + return 0xb; + case 'C': + return 0xc; + case 'D': + return 0xd; + case 'E': + return 0xe; + case 'F': + return 0xf; + } + } + + // 16進ダンプ + public static String encodeHex( byte[] data ){ + if( data == null ) return null; + StringBuilder sb = new StringBuilder(); + for( byte b : data ){ + addHex( sb, b ); + } + return sb.toString(); + } + + public static byte[] encodeSHA256( byte[] src ){ + try{ + MessageDigest digest = MessageDigest.getInstance( "SHA-256" ); + digest.reset(); + return digest.digest( src ); + }catch( NoSuchAlgorithmException e1 ){ + return null; + } + } + + public static String encodeBase64Safe( byte[] src ){ + try{ + return Base64.encodeToString( src, Base64.URL_SAFE ); + }catch( Throwable ex ){ + return null; + } + } + + public static String url2name( String url ){ + if( url == null ) return null; + return encodeBase64Safe( encodeSHA256( encodeUTF8( url ) ) ); + } + +// public static String name2url(String entry) { +// if(entry==null) return null; +// byte[] b = new byte[entry.length()/2]; +// for(int i=0,ie=b.length;i taisaku_map = new HashMap<>(); + static SparseBooleanArray taisaku_map2 = new SparseBooleanArray(); + + static void _taisaku_add_string( String z, String h ){ + for( int i = 0, e = z.length() ; i < e ; ++ i ){ + char zc = z.charAt( i ); + taisaku_map.put( zc, "" + Character.toString( h.charAt( i ) ) ); + taisaku_map2.put( (int) zc, true ); + } + } + + static{ + taisaku_map = new HashMap<>(); + taisaku_map2 = new SparseBooleanArray(); + + // tilde,wave dash,horizontal ellipsis,minus sign + _taisaku_add_string( + "\u2073\u301C\u22EF\uFF0D" + , "\u007e\uFF5E\u2026\u2212" + ); + // zenkaku to hankaku + _taisaku_add_string( + " !”#$%&’()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}" + , " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}" + ); + + } + + static boolean isBadChar2( char c ){ + return c == 0xa || taisaku_map2.get( (int) c ); + } + + //! フォントによって全角文字が化けるので、その対策 + public static String font_taisaku( String text, boolean lf2br ){ + if( text == null ) return null; + int l = text.length(); + StringBuilder sb = new StringBuilder( l ); + if( ! lf2br ){ + for( int i = 0 ; i < l ; ++ i ){ + int start = i; + while( i < l && ! taisaku_map2.get( (int) text.charAt( i ) ) ) ++ i; + if( i > start ){ + sb.append( text.substring( start, i ) ); + if( i >= l ) break; + } + sb.append( taisaku_map.get( text.charAt( i ) ) ); + } + }else{ + for( int i = 0 ; i < l ; ++ i ){ + int start = i; + while( i < l && ! isBadChar2( text.charAt( i ) ) ) ++ i; + if( i > start ){ + sb.append( text.substring( start, i ) ); + if( i >= l ) break; + } + char c = text.charAt( i ); + if( c == 0xa ){ + sb.append( "
" ); + }else{ + sb.append( taisaku_map.get( c ) ); + } + } + } + return sb.toString(); + } + + //////////////////////////// + + public static String toLower( String from ){ + if( from == null ) return null; + return from.toLowerCase( Locale.US ); + } + + public static String toUpper( String from ){ + if( from == null ) return null; + return from.toUpperCase( Locale.US ); + } + + public static String getString( Bundle b, String key, String defval ){ + try{ + String v = b.getString( key ); + if( v != null ) return v; + }catch( Throwable ignored ){ + } + return defval; + } + + public static byte[] loadFile( File file ) throws IOException{ + int size = (int) file.length(); + byte[] data = new byte[ size ]; + FileInputStream in = new FileInputStream( file ); + try{ + int nRead = 0; + while( nRead < size ){ + int delta = in.read( data, nRead, size - nRead ); + if( delta <= 0 ) break; + } + return data; + }finally{ + try{ + in.close(); + }catch( Throwable ignored ){ + } + } + } + + public static String ellipsize( String t, int max ){ + return ( t.length() > max ? t.substring( 0, max - 1 ) + "…" : t ); + } + +// public static int getEnumStringId( String residPrefix, String name,Context context ) { +// name = residPrefix + name; +// try{ +// int iv = context.getResources().getIdentifier(name,"string",context.getPackageName() ); +// if( iv != 0 ) return iv; +// }catch(Throwable ex){ +// } +// log.e("missing resid for %s",name); +// return R.string.Dialog_Cancel; +// } + +// public static String getConnectionResultErrorMessage( ConnectionResult connectionResult ){ +// int code = connectionResult.getErrorCode(); +// String msg = connectionResult.getErrorMessage(); +// if( TextUtils.isEmpty( msg ) ){ +// switch( code ){ +// case ConnectionResult.SUCCESS: +// msg = "SUCCESS"; +// break; +// case ConnectionResult.SERVICE_MISSING: +// msg = "SERVICE_MISSING"; +// break; +// case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED: +// msg = "SERVICE_VERSION_UPDATE_REQUIRED"; +// break; +// case ConnectionResult.SERVICE_DISABLED: +// msg = "SERVICE_DISABLED"; +// break; +// case ConnectionResult.SIGN_IN_REQUIRED: +// msg = "SIGN_IN_REQUIRED"; +// break; +// case ConnectionResult.INVALID_ACCOUNT: +// msg = "INVALID_ACCOUNT"; +// break; +// case ConnectionResult.RESOLUTION_REQUIRED: +// msg = "RESOLUTION_REQUIRED"; +// break; +// case ConnectionResult.NETWORK_ERROR: +// msg = "NETWORK_ERROR"; +// break; +// case ConnectionResult.INTERNAL_ERROR: +// msg = "INTERNAL_ERROR"; +// break; +// case ConnectionResult.SERVICE_INVALID: +// msg = "SERVICE_INVALID"; +// break; +// case ConnectionResult.DEVELOPER_ERROR: +// msg = "DEVELOPER_ERROR"; +// break; +// case ConnectionResult.LICENSE_CHECK_FAILED: +// msg = "LICENSE_CHECK_FAILED"; +// break; +// case ConnectionResult.CANCELED: +// msg = "CANCELED"; +// break; +// case ConnectionResult.TIMEOUT: +// msg = "TIMEOUT"; +// break; +// case ConnectionResult.INTERRUPTED: +// msg = "INTERRUPTED"; +// break; +// case ConnectionResult.API_UNAVAILABLE: +// msg = "API_UNAVAILABLE"; +// break; +// case ConnectionResult.SIGN_IN_FAILED: +// msg = "SIGN_IN_FAILED"; +// break; +// case ConnectionResult.SERVICE_UPDATING: +// msg = "SERVICE_UPDATING"; +// break; +// case ConnectionResult.SERVICE_MISSING_PERMISSION: +// msg = "SERVICE_MISSING_PERMISSION"; +// break; +// case ConnectionResult.RESTRICTED_PROFILE: +// msg = "RESTRICTED_PROFILE"; +// break; +// +// } +// } +// return msg; +// } + +// public static String getConnectionSuspendedMessage( int i ){ +// switch( i ){ +// default: +// return "?"; +// case GoogleApiClient.ConnectionCallbacks.CAUSE_NETWORK_LOST: +// return "NETWORK_LOST"; +// case GoogleApiClient.ConnectionCallbacks.CAUSE_SERVICE_DISCONNECTED: +// return "SERVICE_DISCONNECTED"; +// } +// } + + static HashMap< String, String > mime_type_ex = null; + static final Object mime_type_ex_lock = new Object(); + + static String findMimeTypeEx( String ext ){ + synchronized( mime_type_ex_lock ){ + if( mime_type_ex == null ){ + HashMap< String, String > tmp = new HashMap<>(); + tmp.put( "BDM", "application/vnd.syncml.dm+wbxml" ); + tmp.put( "DAT", "" ); + tmp.put( "TID", "" ); + tmp.put( "js", "text/javascript" ); + tmp.put( "sh", "application/x-sh" ); + tmp.put( "lua", "text/x-lua" ); + mime_type_ex = tmp; + } + return mime_type_ex.get( ext ); + } + } + + public static final String MIME_TYPE_APPLICATION_OCTET_STREAM = "application/octet-stream"; + + public static String getMimeType( LogCategory log, String src ){ + String ext = MimeTypeMap.getFileExtensionFromUrl( src ); + if( ! TextUtils.isEmpty( ext ) ){ + ext = ext.toLowerCase( Locale.US ); + + // + String mime_type = MimeTypeMap.getSingleton().getMimeTypeFromExtension( ext ); + if( ! TextUtils.isEmpty( mime_type ) ) return mime_type; + + // + mime_type = findMimeTypeEx( ext ); + if( ! TextUtils.isEmpty( mime_type ) ) return mime_type; + + // 戻り値が空文字列の場合とnullの場合があり、空文字列の場合は既知でありログ出力しない + + if( mime_type == null && log != null ) + log.w( "getMimeType(): unknown file extension '%s'", ext ); + } + return MIME_TYPE_APPLICATION_OCTET_STREAM; + } + + + + static class FileInfo { + + Uri uri; + String mime_type; + + FileInfo( String any_uri ){ + if( any_uri == null ) return; + + if( any_uri.startsWith( "/" ) ){ + uri = Uri.fromFile( new File( any_uri ) ); + }else{ + uri = Uri.parse( any_uri ); + } + + String ext = MimeTypeMap.getFileExtensionFromUrl( any_uri ); + if( ext != null ){ + mime_type = MimeTypeMap.getSingleton().getMimeTypeFromExtension( ext.toLowerCase() ); + } + } + } + + static + @NonNull + Map< String, String > getSecondaryStorageVolumesMap( Context context ){ + Map< String, String > result = new HashMap<>(); + try{ + + StorageManager sm = (StorageManager) context.getApplicationContext().getSystemService( Context.STORAGE_SERVICE ); + + // SDカードスロットのある7.0端末が手元にないから検証できない +// if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ){ +// for(StorageVolume volume : sm.getStorageVolumes() ){ +// // String path = volume.getPath(); +// String state = volume.getState(); +// +// } +// } + + Method getVolumeList = sm.getClass().getMethod( "getVolumeList" ); + Object[] volumes = (Object[]) getVolumeList.invoke( sm ); + // + for( Object volume : volumes ){ + Class< ? > volume_clazz = volume.getClass(); + + String path = (String) volume_clazz.getMethod( "getPath" ).invoke( volume ); + String state = (String) volume_clazz.getMethod( "getState" ).invoke( volume ); + if( ! TextUtils.isEmpty( path ) && "mounted".equals( state ) ){ + // + boolean isPrimary = (Boolean) volume_clazz.getMethod( "isPrimary" ).invoke( volume ); + if( isPrimary ) result.put( "primary", path ); + // + String uuid = (String) volume_clazz.getMethod( "getUuid" ).invoke( volume ); + if( ! TextUtils.isEmpty( uuid ) ) result.put( uuid, path ); + } + } + }catch( Throwable ex ){ + ex.printStackTrace(); + } + return result; + } + + public static String toCamelCase( String src ){ + StringBuilder sb = new StringBuilder(); + for( String s : src.split( "_" ) ){ + if( TextUtils.isEmpty( s ) ) continue; + sb.append( Character.toUpperCase( s.charAt( 0 ) ) ); + sb.append( s.substring( 1, s.length() ).toLowerCase() ); + } + return sb.toString(); + } + + private static DocumentBuilder xml_builder; + + public static Element parseXml( byte[] src ){ + if( xml_builder == null ){ + try{ + xml_builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + }catch( Throwable ex ){ + ex.printStackTrace(); + return null; + } + } + try{ + return xml_builder.parse( new ByteArrayInputStream( src ) ).getDocumentElement(); + }catch( Throwable ex ){ + ex.printStackTrace(); + return null; + } + } + + public static String getAttribute( NamedNodeMap attr_map, String name, String defval ){ + Node node = attr_map.getNamedItem( name ); + if( node != null ) return node.getNodeValue(); + return defval; + } + + @SuppressWarnings("unused") + public static String formatError( Throwable ex, String fmt, Object... args ){ + if( args.length > 0 ) fmt = String.format( fmt, args ); + return fmt + String.format( " :%s %s", ex.getClass().getSimpleName(), ex.getMessage() ); + } + + @SuppressWarnings("unused") + public static String formatError( Throwable ex, Resources resources, int string_id, Object... args ){ + return resources.getString( string_id, args ) + String.format( " :%s %s", ex.getClass().getSimpleName(), ex.getMessage() ); + } + + public static void runOnMainThread( @NonNull Runnable proc ){ + if( Looper.getMainLooper().getThread() == Thread.currentThread() ){ + proc.run(); + }else{ + new Handler( Looper.getMainLooper() ).post( proc ); + } + } + + public static void showToast( final Context context, final boolean bLong, final String fmt, final Object... args ){ + runOnMainThread( new Runnable() { + @Override + public void run(){ + Toast.makeText( + context + , ( args.length == 0 ? fmt : String.format( fmt, args ) ) + , bLong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT + ).show(); + } + } ); + } + + public static void showToast( final Context context, final Throwable ex, final String fmt, final Object... args ){ + runOnMainThread( new Runnable() { + @Override + public void run(){ + Toast.makeText( + context + , formatError( ex, fmt, args ) + , Toast.LENGTH_LONG + ).show(); + } + } ); + } + + public static void showToast( final Context context, final boolean bLong, final int string_id, final Object... args ){ + runOnMainThread( new Runnable() { + @Override + public void run(){ + + Toast.makeText( + context + , context.getString( string_id, args ) + , bLong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT + ).show(); + } + } ); + } + + public static void showToast( final Context context, final Throwable ex, final int string_id, final Object... args ){ + runOnMainThread( new Runnable() { + @Override + public void run(){ + Toast.makeText( + context + , formatError( ex, context.getResources(), string_id, args ) + , Toast.LENGTH_LONG + ).show(); + } + } ); + } + + public static boolean isExternalStorageDocument( Uri uri ){ + return "com.android.externalstorage.documents".equals( uri.getAuthority() ); + } + + private static final String PATH_TREE = "tree"; + private static final String PATH_DOCUMENT = "document"; + + public static String getDocumentId( Uri documentUri ){ + final List< String > paths = documentUri.getPathSegments(); + if( paths.size() >= 2 && PATH_DOCUMENT.equals( paths.get( 0 ) ) ){ + // document + return paths.get( 1 ); + } + if( paths.size() >= 4 && PATH_TREE.equals( paths.get( 0 ) ) + && PATH_DOCUMENT.equals( paths.get( 2 ) ) ){ + // document in tree + return paths.get( 3 ); + } + if( paths.size() >= 2 && PATH_TREE.equals( paths.get( 0 ) ) ){ + // tree + return paths.get( 1 ); + } + throw new IllegalArgumentException( "Invalid URI: " + documentUri ); + } + + public static + @Nullable + File getFile( Context context, @NonNull String path ){ + try{ + if( path.startsWith( "/" ) ) return new File( path ); + Uri uri = Uri.parse( path ); + if( "file".equals( uri.getScheme() ) ) return new File( uri.getPath() ); + + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT ){ + if( isExternalStorageDocument( uri ) ){ + try{ + final String docId = getDocumentId( uri ); + final String[] split = docId.split( ":" ); + if( split.length >= 2 ){ + final String uuid = split[ 0 ]; + if( "primary".equalsIgnoreCase( uuid ) ){ + return new File( Environment.getExternalStorageDirectory() + "/" + split[ 1 ] ); + }else{ + Map< String, String > volume_map = Utils.getSecondaryStorageVolumesMap( context ); + String volume_path = volume_map.get( uuid ); + if( volume_path != null ){ + return new File( volume_path + "/" + split[ 1 ] ); + } + } + } + }catch( Throwable ex2 ){ + ex2.printStackTrace(); + } + } + } + // MediaStore Uri + Cursor cursor = context.getContentResolver().query( uri, null, null, null, null ); + if( cursor != null ){ + try{ + if( cursor.moveToFirst() ){ + int col_count = cursor.getColumnCount(); + for( int i = 0 ; i < col_count ; ++ i ){ + int type = cursor.getType( i ); + if( type != Cursor.FIELD_TYPE_STRING ) continue; + String name = cursor.getColumnName( i ); + String value = cursor.isNull( i ) ? null : cursor.getString( i ); + if( ! TextUtils.isEmpty( value ) ){ + if( "filePath".equals( name ) ){ + return new File( value ); + } + } + } + } + }finally{ + cursor.close(); + } + } + }catch( Throwable ex ){ + ex.printStackTrace(); + } + return null; + } + +} diff --git a/app/src/main/res/drawable-hdpi/black_close.png b/app/src/main/res/drawable-hdpi/black_close.png new file mode 100644 index 0000000000000000000000000000000000000000..0cd254b3bbc28db9bb8d3bb5caa91ab68470a612 GIT binary patch literal 372 zcmV-)0gL{LP)}{7!;VFySyvM22@{!~Zymzi^6RW66idz=<6jOq`No zgMm|WY_{N(4K{0V$_|?qIJs;$zr@6DhM&VJTWps8NnvIuj^XVbLfM*#y=}0v6FWBJ z1B;QJQf&U?Z9@^oNwN9%w!w_z|x;8Hw3(qKL$tIO*|D2s_(E zvQvwGtx{Fh#W(6GQsw(ez@p$1jRYew0SzcpC76KClMT;w0jrfrBoc{4N756~LUJE} S>Ob=U0000O%Asfh+=n{E-WYB0?_a4>)Eq-qtFKV_VGV!uL9s^n=HQS=HBFU0q$9 zO;%Rc>MDxjtn0dK`3!CW`o5n9;?6aI_;rB@ZUNf1-MNE^0Sp9ARaKp`T$bfU4Deyt zPApa8QM_f@1`kaqyTvIDNvE{|fGNpU&j4te=9bp~umKkFnl}ZI!}TP7Q47EW0f4GtYj=J} zX8keAxjKpMaVinNH|||QBr4ON6=@iTE943#1<==#_?g*_vH*expQ4>+0K5mkw7UKP z;^4gj?9;}~YqoV=pR?vdD1cyB-i$hEb0G-8vEWk}0-&!W*8rMAKS0bZYg++3#0qg} zg^+WhLx$d9b|=`q3Z?&vAv^+nNhhxW`*cD#bIEr?Qt8Ist0^n%pXv=IlOQ`J#l0i| O0000gF^;EToLCJ1zG#g z?7(+OX+DtPc}xP_o)ZC({Mu|bF9nW3CX+Foz_;Bn3{5t^;ao0v75cr48Jyvnj@h0A zm}Pvv!f)^3Ew==i*!T>ewPMUMCS!99j@36%27|vvunqVQGb;SI2XI21V_Mn#6R{8z zzUSSZBz4(3t}0ht9k&ZeI2ssFqWbx&5N{@=Ki#%BO)xFoP9tvds-Zw|m&H!WCKj4aI;&%3R#p7|nW zA7G#x@V|Ci#{Q`GDhF7G;6F9Hy~B!E$}KhqOW1&Kz#loS9vgbzOxTckEMo;qU4tG2 z_!8yfAx4DFe7CIFK*=Dm-O(-8P~6=b;40=e6JlFd?8SWrkEZ%EwQ6nRn5eiQpd;ok z-{^Q*NzazeW~Z&CkN7b;Ao5)4mx7^%7TSMk{{W}Y{sMWi6X5^=002ovPDHLkV1g4m BO+x?x literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/btn_federate_tl.png b/app/src/main/res/drawable-hdpi/btn_federate_tl.png new file mode 100644 index 0000000000000000000000000000000000000000..1fcfc4a2ff623d62338e88cc7b7ae90996cd91a8 GIT binary patch literal 1038 zcmV+p1o8WcP)!n1^i=aNGv{up7cxglZx1M8X z;mf!)+1;e+hGt>dc6VpacjmjynMouvzQ)&RUFCB5QZAQ!UMLjWxPM1mNu^R3N5c4G zv3NP3&u_u_KeFLjYa~d((3iULP6N4>Q6TWkM1WpcSXekZCa}*0R!szu%r8P`@XXB2 zF*ZTq3agLH>~FC;g~o7Hlt~!%MwLmn2HI`D%Vx7z=?Ui@{(coI zGkGUWGvBhr`7S&#i%+AWi+5%GH!=PoXxcN@&521fK0sCxJ*2_X|U*P5`>=#dzk2?1vdo;rY*A zeI3&KcEOOvUSVT0f{PjYe>_kEnM`KROW4?C21wk&_n!`gfGV9G1BtIb5;KbL6KD@X z=p8*r)k-f7V~14)=x>7;Hkpt?Mc~j@>r$z7i=xaxVjZD&{vdRFOqH}}XlrOclnid7 zEvs@ElY3oLEVB>P=7bF|nM@v+cnZsiXN^aEShg4=7oZIe?6?~J&Y`_Gc;bU$xAhN? z;pPR8B#og(0G38B@jf-pBS#J(c2BzPx|o!lWgQRCpZLYYz2+uAqIz9daXp7wz5$>( zTcuLDN&VuotExEzad7Cm^6L`LDB0xONpt=o@?=Z$&iKoJ8FM+fetb5TSD&!h6Mx8{n5zz5CSzZ(=ZPG~W z;mO(H<{B&DX|L6aGb7#Wc)>rLYjC%k-;Uhc{7X;c}1oT=RgaC~+5n$3u0yL&? z0&>+2B4Js#g`sP`LP&<}K=61bDN57*e)a{_GQE-*{|^>VD{HNO%9^vdC<1LoVw>gC zP)7U5BbeNr^Q_X#!oqL32ub}fawy7iWs|G4hPUcP`zwi3cg2HSrZp+CYMU8!JR=8F zeM>&VU|Q2kRy@9wlcmm-%KEnMslKmejHj-%FuKzO!aa-wvv#{;X+x*G#re(*L~06guLnP~7vl_gfy?Y8<| zcY|7nh6h=-ZFs6Wb>UHV5RdPIEV6Rj7aQV9!Laeh*WteY0wJ$l2zTIOH2?qr07*qo IM6N<$g58JWjQ{`u literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/btn_follow.png b/app/src/main/res/drawable-hdpi/btn_follow.png new file mode 100644 index 0000000000000000000000000000000000000000..a50def11d9f7d39c984562769574298cc3a3b0cf GIT binary patch literal 583 zcmV-N0=WH&P)w=RyKOrjL0f zO=S)orjKOioXMR#ciKckLqkJDV+N7*I6KJYa$EU)-UeHiWp#;o{ZJ?r>cwJlBeudJ zG6nGt+5yD=ut?v*#A&ToTZol-5`>v<4inBWavduZ5Y?Dee87Yq3-N|w?2HPV5eB}% zO{Fm$B**MKyy63_D-oYgrK#WvH$tUESOy3P!$tIcHl~>C)6Da1l#v4)>*|~*u5Ku1*z)y|y z@r1zpTEIJwV*&nSQFy`c_}$U6-!>6I(i-rYQ>&q&r7PeFU!lTz(!z)8o*iXx{Hl+SZ z;Q(C;fF}K#XRQO!#MGele4b~Go@yOn-o&!T;Q$sWUR-LZ; zZ+8wPBZM1oGH7yxx$tn?ByG;Qd0x(QlSstl{tV5uXpKUlFqzNiKf|sK!??n{hhxto z0Y05hzX0G10Kc_cE|)vQxu?n#;L1C}TZVI6s)K(hT74UVQ>9~~TCF~i5NIhM)Bw8g zMgVcJp?nOKN~H+~tbo9;a=APjiA-dY*d}PqKe_V7VsR{T#jDwD_R*9_oJ=MkW0hwS zq{mkA!uK8JT_&5&PQlgz`qCxq{Lpp%iE^nDvY>z!K7`%4WgHucJtb0SrBWFI;CTk7 z474M4-hf0dG~f(EN&xz`AK*?`f(?b{G7@m-9*L|dLja)L2;ODNX3kz+36>9?05cBK zl_dN_0faG&QAu-|a~f+*eM*l*06_z#1P zPS1k;E&>2CEg`VY8emrGuH@--_(TIG0C!%J5SX>e>?G_&DFXO>AW5-roE;ln<^x?T zXaXI}Sd>fxnxy;cQslW6N672fAhL*S8@Tp6N%3C2>~;T{rTHbR|J8>E((3hkf6%RC zQ9|H4@SN|Hz()@nh?(cDs#_>uJPNrk{QJF9;OVb|#(mdRt-|Vy>@ydLboSA-JF9Dz zzsJ1h7861Ebq&_!E&^^i1ACQ9r3M78q#*$K(#k0Kz8n^ThRBE6tW$s?sk~z!41!Be zh3a^Eno|P>K|u4XEN#{h1o@D?i|aW7++h5FTSmHG=ma>pkAs`h!E5FVik1fzd7H^( zQc4hT(JW&mrR#bP{{P9`F0}jh9|Za%B?;U`3IVN?6v(WKG>@_be8f~tASMtKhzW!^ txO>Ofs_dIhYbT;MtXE8tdDRru_zT&-vGURq z{iQKZ9PZ|)Uq2Fh@YPo8h$~0rmu+$XjH`r~O@F`led@(j?S^|zFa5S$XM6S-4)H^+1&Uyb0ZM}@ eCI*J!J&fTdS9dTb`cDS>fx*+&&t;ucLK6V-fMARO literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/btn_notification.png b/app/src/main/res/drawable-hdpi/btn_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..021bcf451b0f02b86622d1a167c10bd8059e727a GIT binary patch literal 357 zcmV-r0h<1aP)#X8j6HUkQ>kfV8OI%Mbtn z000000002MBmkY=*VV1c^SrGn05bblZ&uk$zyl4n;3nCH z0AQfKsBLW74!{j(ZEMkH0Gj;=eNPVJ0LmTh+G@THfKQn8%&h(+(*#g6KNATc!u&4) z5oOZ^Af{|I8IWaO0mvMQ!2n?x9>vYP5~9r`5Iy4y2umOXcFF4E00000NkvXXu0mjf D|7VyV literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/btn_refresh.png b/app/src/main/res/drawable-hdpi/btn_refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..0afc96f5bf4fd1d7b73fffd41f8ab6b056577b7a GIT binary patch literal 779 zcmV+m1N8ifP)C?_K8P;?(P-`$QlsQSpQ{XW@B z9ruLI+1-!>J1``i%bfXUXJ@{hsi`1Af&>XNsnTk-j&{4N^dO8dC}wp=bh$Nyp`lR0i;vr?(t!}HT#9nWU7^Z3vTKC}l1f0-yjDKHl!%%*T} zi8j3fpkYMy5gge!bzCam{|Lgu3q06E^Z&@_5p==lWS=IhM)%~xzh{{Px zCGz?FqH6%ODVo^8S;2t8RY@lZ%;8|*lsu{78!cziLFHM1!5?BTY>?a7+GdntZ$mi$ z6?(?$62tIg4PSX;2_nU!py5DEUItgX(-T)BPCD^CAtO)4dHIdW+1e(R2AYWVxCu?F z6P?35RW*h9Z#V!uXTA0Xl+~+4CJI|j2|ROy6AadJmd8e;agL)Fo~sEqf;#sQ{YztM zZfYeyxf*OiHxC9|V;$zQ)XC~JqV8i=9(c=n!_9aZx<1N`l#NlP+T6r&CZxa&9IL^p zH8{5k-Bpx;CPGsCp zk~wC^IU^1X2}zi<*4}IOEkeAnH@@=u{JQT1?sB=@2E7fuBv33C?fWB3r=<~m{kLdk!rBa!NmwHHN z$C=($;0e?}+H2Knb#zb|b!K^3lfeH`SpJkgpVG+#&cKI2_7s-iZ#J7BbV`W`vI!_0 zFB%0;Ve$)E`IhPZ_aN}yVlIm1>GxX*$+SNiBS028QC!w3P0vLF9}>oM4vm1Pi#b7u zUNiZunwDrboBbw%hml&y?k2Rq!mNLZ8pkvNL@;=k87gOjz%O>_fyEqCXU{q`LEub2 zG?U4sEJnIz=IzF?X9lFp|MH{%R%;#GNMsi}HIRu! zh928QXq%MVMx=Wx?!H=(&l)pS+ej%P%P8z)w{!ylf!NfIK4^+$0vHq+vrf5Oo`V;n zf(PM2KZG376tR1DV?1GovI#UTy4kHP2Z2XZq-d0OT}@j$mS>5tr)ZY>c+IgmE{8h@Lgqj zP2d<5?77l>hXD1mwHM4quQ4dVv?C4!YIvd6$I9)nqCE@S(-jN~M9dcM!4H*`(4+`Y z{*mdB-~@zc>zSN{hwF))gs1CKPQv3A*zDe3jf8k#{nrD8xb`<0gwLn|0000MSqLHsf@u^)Q55CQf9sngN(dWs z=Ve1y=D{O@%WdXO-tNw9r^8?{7z_sEe?aqnf6H~<2_>UP;Je_fVHlIjWuAifJns|N zH`N66gLfRKr=);<@Tv*;u}z+;2+)d|S4u!x@G1xp+NMZ|d%$z&xZel$wg}RG%X|f2 zz*EySjWkWiT4$RfnOc^$ktE4@t86nYk&IiiSDS4!h=?9|D6Mvq8GK>ox(>r|wsrv+ ze6I-s8Sfxsv33hJDFR>OJ_v$&DIyyB3jN6)$MKZvb~>ZPDQhQs2kjM7menddWdc+3 zGx&u2_dkkwma;Nc*-2E)>AF6HUU?0_g}5HzdINq`iNLeR(l)p<5rbBvB#LZ;ATRNs zZb>q_wrwv+6;VSuO(z*kNXCgMAu_*$&`zWD0|<5%V_XOlGA*@U%8MAap`mgc?!-c-5JQWDw|7Km}Al1ysN} z0S_gx@LJ)N0KmR_iEo+SP(=keN(BgWaD}ykwT88IN&s)NIK;!Z!Z6&xT->AuT7w}z zk6h1dlMR_HkY(ACDgl3Z8rhJF3XhK~(yI(5h)3TPMX~yloiTX?}RAlVFACmJosGY9~>6+b1y#ye)}Y;WZkK#!BALed|)5d9Mcm0000< KMNUMnLSTYJyq|Rd literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/btn_statuses.png b/app/src/main/res/drawable-hdpi/btn_statuses.png new file mode 100644 index 0000000000000000000000000000000000000000..c62cd0c91485b7075740c232461099ff69a66edf GIT binary patch literal 500 zcmVIYvP*o>QcEpNL;UMDT5kS`U6(|0D5W!M2-blL^eg zp9D&G=eIjMv)OGl#KgqJ*jgBd%RvyFLOtlKWcAShv2ELnxt#ZXf5r2>ZUv725&U?` z1%W@FSMB_-XeROiymJAda8d?f;3IK)x6b*pXxl!v7f>Y_Z zZBw1#n1w#DYg`*+^qZ7&xfZg!VGvX&sMxXIO3+MDIMJG*g-yyTa68Y)%VtghY6?KH z{)D){#(KDco+sH#YX`_aKQH0%Ubfpb2AZ2fcA7&L+QM%K(3R?f)&c-WZt+aWIo0-~ zC|V$3wDC|n0HsPi_tP|;p7iV~Ns<{r9F7XH!T`|Go8vg!MIWLe^mc}wgcJ#|tLr3? zX0;3eLl=N)n)BKLbf3oxfK*~G0JT5d1;7Ot02Iy>WdMATtX6b0GnlqtvF%vOl@SPn qW}=F4qCTNBy5^G0J~1&dzV;27px_6_BA3Dd0000w=RyKOrjL0f zO=S)orjKOioXMR#ciKckLqkJDV+N7*I6KJYa$EU)-UeHiWp#;o{ZJ?r>cwJlBeudJ zG6nGt+5yD=ut?v*#A&ToTZol-5`>v<4inBWavduZ5Y?Dee87Yq3-N|w?2HPV5eB}% zO{Fm$B**MKyy63_D-oYgrK#WvH$tUESOy3P!$tIcHl~>C)6Da1l#v4)>*|~*u$W%OEW53KN%(%w0Dg(7z-a-%?CIC{`qUVwKJA%yd*&{~N1@bb)EZJEVqzC+`Wd@VW)VAzB|CXIY7b_iRkD@Zk`YITwfE zEEzF~`Z}h}d9^3_*D(Ot0+Bh(3cdJK)71t%i7mV;)&J-Lx%sJdfOk;u00000NkvXX Hu0mjfz}0g7 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/btn_boost.png b/app/src/main/res/drawable-mdpi/btn_boost.png new file mode 100644 index 0000000000000000000000000000000000000000..f9a1df189908661aadd3e8934f796e6a26f62f0c GIT binary patch literal 278 zcmV+x0qOpUP)$2oNh!WMCOqlkj0 z2)n~A0Yu?e0OD{909dnEH4=Eh3BvDaU^#BL+s_GKA^3*QIUhcLw`+@-?_}0CA~67l zTXaGZ0EN2%@bH%}nt7lLpb>5{2fPn}{o56w1tFOSxNyAdEkpZ6ZJCy2&$;hoB#j-07*qoM6N<$f)A2wy8r+H literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/btn_favourite.png b/app/src/main/res/drawable-mdpi/btn_favourite.png new file mode 100644 index 0000000000000000000000000000000000000000..f690ae44d87d677e11b08932b440164d7baeedd3 GIT binary patch literal 478 zcmV<40U`d0P)@9ACa-n&vmDWm!vw z6Tp|B4hTR`3I&&Cc^aTSfk01Dl&K=!zaxPEDm=1X2)OV2!$`hPkN|#rrL`h5Uk`#0 z)VA#nPOKq{HQZf?;Z>@4fVZHJQ3SB|fTv4)=?vZ%$OhR;($&Th$2Rl{2j8JzQ9x;x zY>~}B+ZTitm0a`K@WKTgLgX1b7wZWsCv3X?2k;x{Wa1%1u3{u`MmAF?AxIoBfbSJjv;@#oE;)zKX9f>^ zu?b)`4dX>ic+h!}g%5EpiZO1TzafM3&ui@B)O zspSd;&N&ElNpbPC62_D|Xao92my}%Kj^oV2h!;6NW2D(^PJs9^&d*eT;VOxfACRVC UKs~V3Z2$lO07*qoM6N<$g7}=&WB>pF literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/btn_federate_tl.png b/app/src/main/res/drawable-mdpi/btn_federate_tl.png new file mode 100644 index 0000000000000000000000000000000000000000..dd487f623a329a38d83d44fec782e0bec4d00d7d GIT binary patch literal 798 zcmV+(1L6FMP)H+xCogC>y8X3uvGuv)Dig4xH|0e)D1^l^Z_ zGhhJ!S}K*U(h=c&6KEHD25$8&lIZF^hk$pVL>M?@-wr>F|4A%Wv2@nW^z9|xK z%jNRXfFpe%avgi_0{l4?aN@{xXS8MzyqruX2V!$Ju)c)!S%>g7^c{e%qrb$wORZR3 z@VKZf@tT=XTLAMO<`*b~vP5PbGA_0>8MJvw?+uD^2wG&OlZcpBYdIsRSS+5TWrE}o z15tDTpUA-EBeRI{30YFBN~LlN<8AZ}0a_K4lkihbfNLgK@C?sV27IWW*5w(aq>yOb z?PZDEJzEpI!P!n@?Xv*hlk0Ld0gMl1ZjHdLWZ{Yn#A9z<2Og%RbUtq5grj1mP2cwq zXabT4wnV#;Heqe7SEt}FQENB$ew4jeGyzn>Yx3?#adu-V3jM_wI#<-sF2=PcAPI=i zFCyfXemT2Cp__c6_eQY5iZQV!V6KSK(oPQjL-wwdz$-E9t*}EtzFDdv`plgbC@cmzjv3<+KbeS)&=I>Kc2 z-!&l$jxZx)@w-a1yw^GOb5U!GSYn=M^(PyXRcHZYII@&7tmxUD{N0FSj#3u$1e4YN cPpoc#0e2jeU!d*?fdBvi07*qoM6N<$g1Z=SYXATM literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/btn_follow.png b/app/src/main/res/drawable-mdpi/btn_follow.png new file mode 100644 index 0000000000000000000000000000000000000000..569de966a61e8c267628713f321f9f8fd50e2dd2 GIT binary patch literal 404 zcmV;F0c-w=P)98J#G^d69*uUg<=Pg7+owkHa0c_TG$W7Gtn?e%#fy*8yOjC0v#|Ni{)Sp zk^^E5TG`JCG`tCynt~zc z0CRJ5K48!*gYr8td+LL-oq>UY0L>f#t^KTlmSzBP51vHW1H_p?2ia1kcC@jv5d<0* z4K#Erv56MRF(5s$AT!7f6+=*=LaN2s3MNoAky_`=0r50SEk_OwP-R9+y&p|2%aLlx y2(l6-6+IkY~Pz5;JoF4^)0ssJgXLMPPPT=he?VriSC#o-<}w3PII z=Z54u4vD0%9_ReP7~ALXosz>a8~}cTKe-X&7)4RPEBGh~f~$l}`@Vm1U3b_j-1EFK zx_uVm(g^m**R%phhf@IFWpE|L3!d19&rvk4f-50nA;ioI99zCd!K5x+36a1nBRC3e zP$X>%S3;!NJ+1}E7Vgk}rVCfDXoe8m#skI_{3A&9z$y-y7DAK{tg-=P+qDN4&VaB5 zMPp3o5dLof6ujYiR*i4>E(0*r^%Q^tP=HF|r)BZ3OM|sAT~8wYjlS|kp2i-AiDLi& N002ovPDHLkV1g%Tg-8GZ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/btn_local_tl.png b/app/src/main/res/drawable-mdpi/btn_local_tl.png new file mode 100644 index 0000000000000000000000000000000000000000..de625fda0addca0d5d219cf846036c1186034a1c GIT binary patch literal 508 zcmV`tIL?lQz`8a9mSqj2_E#-%9A5@OFsQW&%jI&P>$)@O2O|u_UM--=b{^T* z5HC1xU|mfe6(B=sx)rQ-N#|33FBM=k)gF{9&q@Ut8lhvJl5#t@>zVFX^ zo;RwbDfzOP?RSP5(63|uuO)QY5Y-RH~E^x6OJ{Cj9f}p77SgyhB6Gj z>Wg?8SG{Mr^iqN6OQ?EhRqu+24I)b}c*~a_Si$Tj>rl?lP$(G`8E+@Rry$MG&(6?& z-zZpv;niA+v#cW43^(KqBrljSba_`hh+>Bra5~%t-ILw(t&|t?X5l0!o9a%0=>rI M>FVdQ&MBb@0L?gLf&c&j literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/btn_refresh.png b/app/src/main/res/drawable-mdpi/btn_refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..4cda61774a347995c8e7ed38b598d60c915e1876 GIT binary patch literal 507 zcmV3jgk2d*n`Sp! zC}d#RWp}@s*_oZ0t<@^3Xw|6IYVA0Vwsgi-X*NIatOQ?!Y2iJ2!~ut<-#~U0|1P?qZ-a%RAItKI@&{%XHS#gfD?|EGGA{mM5mKk80=H}AN zEjJ*Jxf@~~`UW}L*pLN^sZz*LF{G}?o00HDeSzCU-nucr zDw~o|Qxz>C_rjQCsu9$VX)Y8eQ)eIlDlP9-J|y($R2o>(xEfSaF@ERNd&-lpw5 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/btn_reload.png b/app/src/main/res/drawable-mdpi/btn_reload.png new file mode 100644 index 0000000000000000000000000000000000000000..ab658ad22f529515a8a87da30f2796e93c57f884 GIT binary patch literal 485 zcmV;6(h3{AbdmMviM%b207PJ{04F zWzcoqW0}*Z?DUub)iR=ISRBemEaO;k&1ZEWU~y#}%gF1TIoh^;*nt2)rZXAG1#?ub zKb|bhj<_+MDv1v@&qW6Xy;dp#--Pt}L5GlGfhmn41$kEiN9i!aRU@wuQ(A~1A`p{? zUU}JO1kqDU>$eRY!E1gS@-mKCTK5D47YQKkkZpc-cM-Bya9B|k`#2$hSj{>0oAe4^ zXUG61Kr48j=V#SsYXV-Qd9liSc`qb@yA34f*d5Rg=~2@`!pL2KCt>VH#7G#u7lOpj b->!cFPV4T)z-IR500000NkvXXu0mjfkl@sg literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/btn_reply.png b/app/src/main/res/drawable-mdpi/btn_reply.png new file mode 100644 index 0000000000000000000000000000000000000000..b6f1e62cb9ba05601c5369367bfb2c9101426ed2 GIT binary patch literal 336 zcmV-W0k8gvP)-KY-D8A3C$f`bhjKWWO{+L^?+E879!~?Dk|(`IRL2Hmr@5n zL!-jd(vovP9bsx}S^_M#Sq9V*Ky$*$P7k;o0Tl0pCb%g?IADgEnVA|@9f4dBX=`iq z04-1iiaP++rUB(9U{PNSRLV#-N8l?7fevB=si5hu1*R!YvT=@TNuU2S*bt4x-_;7XSbN07*qo IM6N<$g6$Gxj{pDw literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/btn_statuses.png b/app/src/main/res/drawable-mdpi/btn_statuses.png new file mode 100644 index 0000000000000000000000000000000000000000..bf952aaa59ddea5e907560754276210da775a7fb GIT binary patch literal 362 zcmV-w0hRuVP)LULvTgeWdzGX|thX=>qn!$X!E1?*@01`?0Wj#S3VzGfbU*JUj^WtUl8sc>(^b literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_account_add.png b/app/src/main/res/drawable-mdpi/ic_account_add.png new file mode 100644 index 0000000000000000000000000000000000000000..569de966a61e8c267628713f321f9f8fd50e2dd2 GIT binary patch literal 404 zcmV;F0c-w=P)98J#G^d69*uUg<=Pg7+owkHa0c_TG$W7Gtn?e%#fy*8yOjC0v#|Ni{)Sp zk^^E5TG`JCG`tCynt~zc z0CRJ5K48!*gYr8td+LL-oq>UY0L>f#t^KTlmSzBP51vHW1H_p?2ia1kcC@jv5d<0* z4K#Erv56MRF(5s$AT!7f6+=*=LaN2s3MNoAky_`=0r50SEk_OwP-R9+y&p|2%aLlx y2(l6-6+IkY~Pz5;JoF4^)0ssJgXLMPPPT=LR4r(yED&az8IV z0%DjUHUe^zBQAnl^oXA11WcB2FKHA-v31q2=>7a$1plbsKeC6O1>*>)Va7N@7Wr`m z^dx79kX8OOg1;u9*Yz)HWS1v|5JCtcgb=N~0Qrb}_9~hy7ytkO07*qoM6N<$g4I&F AGynhq literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/btn_boost.png b/app/src/main/res/drawable-xhdpi/btn_boost.png new file mode 100644 index 0000000000000000000000000000000000000000..a6da9683d41cb2bfa47ef73a06cc46bdf5e254aa GIT binary patch literal 396 zcmV;70dxL|P)ln-pd3O)03I2@i+%;-!W3VZ7~y(vwel%|xH=oz2EEDzi~xAG2^awIjphWD0p!XX z1duIn3_!lTApo87768U^yqNnrDhXin_c#Sz%fkVz%R>Qdm4^XHm4^VxmH$oPsrh_B zQGmVj7Dh3E8p@Xj@X!FYAV@5NSp>=F0}{^+OcL9mx3CSWCWQcXAhCr21+awx2e3wf q1Xv=#0?Y`|05SY?lwlaA!h8XX4{@3Vu@$rc00002A9{JKa5#OtvX>=X~FpIWuR@>Fd*?MT-_K-bgx~-W!cZcL$aL!!WL_Wvus8wk?u-)|%U zi{YI>fM?nR;MGO~K*}#@zwZfP+G_+~7Y1X#2G4ZIwAYLP{Otq&E#&9amuv>UPS5$7 zfZ0|Azzd%EHT4bHn0;o0Ke1TskRMauZ94>5UQqNE_CGttSPS0zA|*1dIR#&*5vk?uHq$f@{7&sREeXDB#k%o>MhA|F9HxP>@7%IcS zJf5E%E@nywy1(5I*B}3;P&e`n3nZFU8~WBV9>EGyeT6pO{pFnUiA0n?rwuE6jl_wOFgI&EL}v@FKrXmnEUcA%@U zn-Z}%z``?x+4!z-rT+%rPl?(fWwY7CAmO6|@b94qrMA6JD9(jbU(YhYbWisC@a_WL zzJY`p2?fw{&E4s;blq;*;vRsvOlx7m&6lU(d6~%qzd%=2*?57zrfqJ*JR^Xc61t`r sEos`cuHN|fUZEB(TC`}WezO+OuK)l507*qoM6N<$f&lBbYybcN literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/btn_federate_tl.png b/app/src/main/res/drawable-xhdpi/btn_federate_tl.png new file mode 100644 index 0000000000000000000000000000000000000000..0e0682fd13e6146a66a6fad9b274cee1d42981f5 GIT binary patch literal 1518 zcmV>8ZV4D9#tY3N?VRrkzhdaj7Nfsibg0g`h&M2!J)bELVs`C>dCUlV!a zTQETIJP7`ynfdni_N|d6K9NZ5bs&CW;{IDaS`|qEKwQp=c*{HyRRE?;0Kjv-Q3c?a zApj2|2>`7AXA=PM+~ueOaNU6T-*l5vRe|-GUz`8{@wdyG$ZR&d4P5M@9&_?Hh^kq< z-`Lo=Qw2UnoX6k3-rnAo5aP=ogfNJ=W`A&v8=|iBe16Q~nX%T^ z)I-SZ@|5G9>4Xi6aaV~lz7Xxs=?j+Wd#64_k9?|YYggmSpWb7H54ZC zi-QRg-Z{$>c}W96!VCbIhiKRP`93qmL?Y2ZPk`@yDmefY2>k{XogeTRM}w}_+1a@k zgdZR%bibwI4Xd!3D+ZDMz0DwTf)wIOAJcj3o>1m{j78ph;`bxYGgqRYa@Ctw~tj05!eC8;J zr6eq=g3=qwwKG8?K@I1gdj$aRywOOK@1loNhXNP55GMeg4sqjC(B2#4e3=2#3z!1H z;7gtp2pTpkoA5<6S+m^ClQ6_ggSgDrQlWYeB><4*=W~Oz8-#Xe&Kk(U}|;9YR{{q+I}F zpaMDJw_C#46N4qO=4?!5P)irx0t^r0cHT~m-2jm4&NM((TaAPXPwJug4#$wDQ z?V>L?05nl)e;9jW*esYb9FwJIY~)VRh3H15ZNcKlOaV~2enHJ34ArC>igo*nP+|N| zKO9i}Ps_@Wl>j#Yl+T_O+{y$sfQ)dh-z3lrhCx4GeVsym{ty2a@`#=z^lVxwg zt(^GAxN3p5|pA&K0-^ z$u*VF_n0oC`gt!kiBMkvt9@Uy@n;?+fqBU#O|C;}s+Jb=m`xS;g;Qooj<9H|tNY=- zk3mj%s47xk^WAaU1Se@=?qE}Qm}gk5Bcn zD}t)E`zhkw5Y?9PhFyD|v@t+)GZwA7DvHP2+lWb(*9!AjCd ze(_}IavW-+I?yJV9;erwG4kAiVR<~wm)&{;%8d>{3U@7cqegj$oK3AzGUJWvLU(*z?0=XEukwH|ftAI=dB3mt$DELl>Aka8Jx4nM$*s5Hy&pjLN8t%J?tFU$OC` UfEv_c1^@s607*qoM6N<$g5&|reE|nBK)KLplzZ9i9c@1+eG+PEEb>XnDFKrC2tuj#7H^1gtScz=}2kP}x!~0DMkrBLG|tGE{+nZ3Hm$z_G&hf5=wnEyIIW zogQB8o@)C9I{GSH|6#TnZqy!$g6|5*e`O7EP62R;c@6?U8wmZQP$<-?tpMhc_yvgU zyCA;<>tauG+yW3vrBXX>W%2)u8i-e@jM9P1lPvFr0Jfaag?m1jxq;plT+#bM*l&7Q zh(!IKmQ7QRu&T?Tb`SwX01-e0U=$!&C=_~;=_lw^2fs_m{Ofea$LA&Ru+qKkx2fKhcyRb#_rF>tYS^yd#H99fi4L6;j8U@*;2w@ka z-UbM43UPv(Ie3LkMu=cL6ZQ>qsQ>Qn>(d z{!WA0w`|*fd@52Eit&3S@ zmjo9Ivv9Tvas2#hWWB@MBaFRpBHvw+IZA$y7CAfqkIrv9pd2u3&$E4Zrq5#*H?pnT zcPHGgFZsDz-E&g=s#Sgbi}t+#y*zT- z+S#AZF3H@*b@)TdrKimbGlFk!FZ-FsaVA^DHGQ&_h4s_|=bh47`N#kIr(0?@m(^mdW9M~=grr>FzK1Q*d9qbxoMx$a{{QP+ zfXZJP=gkqGQ`^1$IA>xO_qhwoQ8Tu20;!-GWgyCH#x5>C=9Z7GE7w0Th-v@Wax$bq z!tCHagSXliJOXbxF9$RoE|9&^+UZ&_M?fv_prEeLHWv3K1~5cinPR`?Bn^ps(#&)IsL#O c1EG1}`Q1z`~7{=p`qPAKPJV2?jQB#v_?!+i7YW*U3k)ndulY)xhqzGz*MGDpfY_7iWK3CBADx5FB?7 z032UM7XVhuY3Bgc>-7`50IJpMgp)BJGrOcQ`wPxS0qpIwjQtS+*0@rf&IKScpr;=& z^D9p7eOX8ZgbNUU4|W$TqmfRhca20EJaV}|_cUhv5oWq0a{qOFegn%no%2Ir9b(@q zvc6kR32(Jp2MUG44G90D8}ni3t&i_WT&5soUJ>GC_8s?)S*My_Th#R*5{TYlc+PWc zgy(X(LlE8O%xzY9pH181+;4}zQ2TsLMD^h zmt+-ibs3!>$N=E?_YwZady=0sNw)%?^#jj3Wfg#YKA-Kqj(xq&_ye}i0HJ9u0EV?; zI7V(MeCJ{70O&M6#7uvL-BkGnT=Rl0HAIQM#sg?H8oPBvC7$<;_b`2qu|1Z>LCTDg z07P1>D`uw0Z`5I{G47o+5&%A5jnVv^>d!J+D%%d=scy{Yb{SM$iMH9Il>q42bRD3o z%49x3q0B6e0Kg+F@ds@%065yM5aT*P-I6;9f+LIsc>ZWbY@_dF2_i6 z7HhTIey0FH)Z>^b?-Cs0L1zH4^P3>=UuO8TUObx_344OH&vbHD1AzFGGI6j_6&-k7 zX7zgk0B}hnY+gi&rfP+-oo<4VF@)PmCOt9lo0GJ;uJkxbTvqxObvkTCRTkGt3czyt6AJOB@1WC4ux kOtIJa4;3jt{P?fqFFkxApX=&5x&QzG07*qoM6N<$f>1J?Q2+n{ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/btn_more.png b/app/src/main/res/drawable-xhdpi/btn_more.png new file mode 100644 index 0000000000000000000000000000000000000000..44b7c6142e09785694192f57df95e9e9f44fe96a GIT binary patch literal 307 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=zdT(WLn>~)o#D&X?7-u;*WtgP z;f{%ptutGX3ofbXRrgmr_RT6g{K&jvKo3_DQJ!+Qb9Y%J5))i@>?m$Y+Mf z?pT(4X;s8tpW9M@^6T2r(5o6(XXUrF?D60&{!r=}EVpd7!o&B`55+HEn8ovc?|!}G zb;}tSpVXZ2;pp}c#V-%eC}A$Ko{{Zv*<`zNf5Sg_^VXBa4u`(7t1U3z&ALpp;+;%G z^UdA+vz+$V$b8wp&ZE}f<5_P(mdnDW+LQX384j>NVE(}PgQ14OjzPYGAI4=s7l6ux coX+q=S6*HqOZ?y?yAL3)r>mdKI;Vst0F3{8H2?qr literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/btn_notification.png b/app/src/main/res/drawable-xhdpi/btn_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..1a1b3a3ef05bbf615f84ad6a7bffb438e90dd392 GIT binary patch literal 394 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEU<~whaSW-r_4bx;)}a8AqaU{z zAD6w!S`zW;s>8Jf4_t0%va**l>ORuA;JRY>+sX$DEV-XsoQmH6Kj3;o;n|-W6*U!4 z4#k!j|7ofHmGjPh;<7=B~pNDpe(PGCOJz%WgJ?w{|W z=>ZI{W|!?ZRscF26*MF=-2Gi2S26AXQDH@()B)CpZ~HIj++O^)TCW+bl$r6zhdRkS zObiXW40Ds>Vwqo*{l2N&aO!>N>vO6NyK2HizY2Vvb>{|S$)ENB8{f;WvDYWRn69GC zAf|So{~c$-<{E#7-A}6-Zs`4IZ}|IZKf@1=de#H}Ke-w9ohoPeGc}*_hmQQB|L6QU d6sJvPMkP#;>3v)Cr+Fgk#4tppxJEB)M~Z!xL&35 z?{>R=It;`8S&UICl@3`0n4lVs#kieB6K4w;1d0a_s>Ov&x?R5Ol%RB`lS*4U$^*Kuh(y?z#AukfeOH%#+f*N z_Zrt5v?BB_cD(A;5VTRylDcaMu8WBjKHQ7xY9IqfzPH{CB_KgvX1~h z#6<3ovnTUeS~{s~86iLfyqyTE&t)|h2)G2@wuS&&w*Fg9T^qUm~aGzIM zUI%zpw9kbAMsHb^7CwJ2Z}8d)h_L>m4dAsBz`_PVwZridfY(ZZsOn?`&f5ZB zD*;GBpU8Zl1&}S^b+3Rm2?4Kc0imfoRS?AgXA41qY8!-WT=$A1 z0Nj@luv#PnmL+a5$fFe{&JCaMR?MfaGZ0o4aUjTnfCb|*iE%)7Hpk=$Sl}`A2x>r)XRZkQSdc0DhKD7Ks72jpZb$HuvsXpVA=43Z>E!o|9H?tRp|c}AIgb|~88D>06$ zxFS|*F=z?kPI7h7%Jgq8V|I~o?**>kW3$nfO3LjB(r6TH#9Co_@@) zL)NqGI%_LZ=$o=+WegpN53hW0QUrz5309ABTiObXZ*doh`QYP+iSw@kckc4~Z-6pO>f|A}0)j P00000NkvXXu0mjf?**?j literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/btn_reload.png b/app/src/main/res/drawable-xhdpi/btn_reload.png new file mode 100644 index 0000000000000000000000000000000000000000..b75e28c783e1856bd35735cc7951a016c1546b88 GIT binary patch literal 911 zcmV;A191F_P)=`4}BY`I*1LEm*1fVqLt>-BC{DwPrm+NbX+iMk#o zdTt=l3O^tLyCmQ;j^NZope=q!3cs3Gcw}tMy!d9bxklUkEMego-C5V3sn_c}B>szUwIPpVT|w#Df(WVIv@?=1+D1L2;o-8EEEd&ZLI!V#p-Ws-Ug=@KO9tkcTLCs5=}H<0G!*d z)d~1#E(B4Rn7q3O-0IWT-*k|IUvs`7JDHsHJs11fj*c?XF9yOpliThG1K_KxbWC%9 zVf;BZG8ytLSNPDD+jIzEive&=V>$83D9)d9F^ATcLjXC>r$(dk*hKuVDGr@ghX7U- zcZq_D$tv7rR!h+TeOl&xLJ!g!0GD?XT(K2J*}91u;1xGntJS*e5J1-2xuiM-OQq5i zmpw?u4#*lQJQQiLHEU@QCA+mkh@gCQ*pWJ9{5dt*1>%~Y17pO;9Dr!FGXCsazYMtu z18(~pOM#KRJ7D7OlTGosdgJQL_;HE1NTZWeHrR_0KhnJo(!YPB>4T~py=$C4<^4y) zme{o7bqXo@sKU17@E~g|{(4D>j|)KdBQKn-ez+z`LVO$mQv8YH;5&2Cl$l!mqyW%u zR;@gWGgO4B6h97tJngqf{nh{nVrs>!0AM?#(W-iV3e_J6^yit>ipM}38&w^Nc?Qww zF;4R961++qZGqbmuQL$y@)<1Sop^+V(Y}9z;y02Mj*!5EY${S+s8#hn0r3O8MaF$` zPAvq)H+l~81uAfH&9)8!%~rJB(Q4tPxKU8bETrIC+^8ssA{2ynQxp_@*2ll~T!;`# z(pGJgq4yrRG%a){=j(JHCx#&si9{liNF)-8M55lww(U8#<_m?wZa$wsVrw4t#S*}e zd6ode=Gg&gHqQz`lzBD);>@!E(8_$@188TyuK{#ozHb3^YCZt)k;~<(xIV=7Hfpg} zs|~O)-vY`L>e#ZZ4d3@iSecK2@j|1+ahwHq<|FC?KvuY#508oy)C5cOp?af96St7z zXg)j|&Wpw37)$dZ2)DLvPbUpP=>j0AN7r?iSwo;D6Pv0f?hu|)v)m!vWwY5)?hvjL zKT~K3VK0s4QE3F=9cPFcts&qhA5fLxNL;7)xPI*-gi0#A#ZDo_1R@#Q-BC6G7#@Db z5?)W~nEV$A)J9rcW!23@h$c19^X6e_2j9K^&8p!0{*aas{u7B|WK5xreYA1k4g$8y zn`s`JR4+5Za=AQ+wtSjDT0p3$8nwki=w>KN@fr0KH;Ll&^|Z4Cl|(9x33L+e-bDNj z&U6S+R1&c`O?ohV(40V{#*oTPC>`1mh5FmlB}!9CJs5Fa*F-;!pubC*i zlOfYI*Mc0efg08ZLXvqu`m(SRd}1@TYH$?*lxXA`jFq9SG-m*YR&#(zBoc{4B9TZl ZeFN9OYhM0xk7@t_002ovPDHLkV1oLM1k3;c literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/btn_report.png b/app/src/main/res/drawable-xhdpi/btn_report.png new file mode 100644 index 0000000000000000000000000000000000000000..4f0df60423f493767356fddada1a2296117d4ae0 GIT binary patch literal 415 zcmV;Q0bu@#P)At56a`@ag3uozxV1w}UP3cuadGHYaHzO-DV;mCI2AhTB!c>HdovV5kxcjAcOLH? zxXTM9IiD{>+;YDUgaSXyvsu& z#3~Pg5W9R-1eEeo5YWoIB50MD^L)v?4@cH_&AH9$WEAOHafKmY;|fB*y_01$xuM}O`Sh9m3iYydom00bZa9zY1; zT$bgF%zaT55Assn_G>u-R5w1oJ!SzQML$NqKYk(tJSpGQdgNLgW$cy8mtkdu*OBYI z!IjrWkQ+O8T{pe4{qN-X39`JwPWW!yb~Y$Km-&G%Z}B+D&F*Cmu-o48#h6ZcLoZ@yG2ofis)HmryHFP*GdEPldTu+wt6 zJge0C4G0hJ>gIHSbzq(n=X1H-qG8VeKv2>X@ba$=biO|mQ;8vM7zFCwLk|wVReW$v#OTibgBkBq9+6m zha{!VD%T*jD~D-Lpzcl4fkGtZ3F5v+X)`Z~FaQR?02qL60K~@St><~i$nLgrzlLx( zhwEguTAg-XcLATTi%4)n(LmJ(Y-|AFz&oxNv`Ojv{zRBk7{E$^aU+u#L=J$qqwvRf z!#EvO76a&hm~Fbq0MJ{49mv%GgOtf+#_{~4VTMOk0G{yHSE4$sQK?jDzIfIG0RG4u zogRt$#0ncBHaL{>9 pp!EXSC9N|nBK)KLplzZ9i9c@1+eG+PEEb>XnDFKrC2tuj#7H^1gtScz=}2kP}x!~0DMkrBLG|tGE{+nZ3Hm$z_G&hf5=wnEyIIW zogQB8o@)C9I{GSH|6#TnZqy!$g6|5*e`O7EP62R;c@6?U8wmZQP$<-?tpMhc_yvgU zyCA;<>tauG+yW3vrBXX>W%2)u8i-e@jM9P1lPvFr0Jfaag?m1jxq;plT+#bM*l&7Q zh(!IKmQ7QRu&T?Tb`SwX01-e0U=$!&C=_~;=_lw^2fs_m{Ofea$LA&Ru+qKkx2fKhcyRb#_rF>tYS^yd#H99fi4L6;j8U@*;2w@ka z-UbM43UPv(Ie3LkMu=cL6ZQ>qsQ>Qn>(d z{!WA0w`|V2bl}aSW-r^>)tQEG9=0x7H9_x{0gP-%%ra^hq&1*IjQbBpy?=ybc8&zSrB>^$bnxrOQP-rbb_ zJ0px6XdnVO%zjx?<$AXBMTyn+=7R~>-hY=5$&%x1_cx1M`%L=h(f41gxU&x&Q`;Pr z(WJlc>h5o|a?hmBPH5d;%XGUlXPfU2sb07K!0dNryU&L`-W0L9mw*36k*&u1zA5)x zI{cgN|6~`dzt1vLec_6^$;z|(ylz>VJ$jvCG&3f8VZOSWke9prmg0gR-Ijl@PZYhi zk+XLG?UpJ2hdAF(Ieu&X^a~fZajrI=J;B92Ju5x@aZuXF@*C9;mO2S%T@4g{`=hjO z?~~;jF0!esWm#oY*Uvp+mDN+G^lC-nt(!UZ(zk|qd++)U9t zs*w8b&wib{FA^!!ZaKO$ZfWj&b^TnFD%;GtWo5H8NLw+j|CK+R#wV3%dRz-Ps^VD7Qk z8bvV%)v}sLST|pt802MXE&DT3HtfmnpuFWt?=r;xZQzT(Vm zD}~p*Xbp<_(9-v;Xn9)i@yEZ5gcluW?SFBCKPzi>U6)v?ilDg8D-SR0{iX-w&v-@p q<~Pm%qq_gc(e=t%poE10E9S~G7t}T$_}QQb67+QSb6Mw<&;$To$Rs@g literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/btn_boost.png b/app/src/main/res/drawable-xxhdpi/btn_boost.png new file mode 100644 index 0000000000000000000000000000000000000000..5a2a0c1afae260a5d3d90c00f18279630e9f7270 GIT binary patch literal 675 zcmV;U0$lxxP)-)`-gD+YFPxPv^gQ0j znR{oZPKOXe2qAfl%DIF~Dl=C?TLGBSQCk6+(NWt0#Hs)wYW{FId`3Y! z0F{d$;WWhCjN#t#U;7U800G4USS;DOdGumkz z0CU=D69ALiX&HdZ^s|6c0hrcK=>W`Yrv(5iH;*7o0^mkFB?54zok9UrE>}Ky2e5sl z`q=Rr?*L-DuGjn&0Kj(uV8EdvgxA>Uw?h>n%+tCdI@O><6NI1cx2M-SY=R?%5JCtc zWPcbvo6Sz+oaO9LrPmlnH$0L2Wq-p96Uhd{trP$>?^w$YEgnGp5O?Xv3;_R}^pGR% z9w}g9mOlVH&EEh(kh=MWEKT$Sp#J6;wl>rUfL-kXK&%}Ah_wR%v339;)(!x~+5v!A zJNw&I_`GVfL!eWK&%}A{BhFm5der4l=e@d zATfk=B>+I2Tpd|IFX)K#gchcjrR3_!`guXWmC5}LIXYrpjaWwj5bFp4VjTfMtRn!3 zbp!yhjsPIm5dg$G0)SXY01zjH@L+j)dK6y9yMKJe9+v_2Z^W0le+unX+61T>4*`qr2RVjDsuQ85}Djl`Dv$l6w;pi+$$ z{kQ$TIM>{u?B2)RyEAjomrU>xc4xjbbLN|K&P*;R5C{YUfj}S-2tpz+6toeQN@Y)9 zU*8U4qD`}ZU|`^4e}Dg^tb{fIyhuZa=Cn6m`MR3`rGB?vvEaW zFW4#!B`iHb_SDA=po}mfk~Be3Zi3|JD;WT7zN@QiqcD{aK>o2NV{i!bbhQFs5Jo-)u#`&j?qAF}>1ryZ_)7WEC)0B+ThE z!1ohRNzCMXQ4N6K{iD%n zd?HLnpZ&c-q-PTYyhi&@n2c_KYcbr9XM~HwWVFqlpKC{QMbzpTYm#V$7apCCqLmL3T)m^o)IBpV&A2c}Dg16#I^U>*rWFCK#Tk52 ziEIe)YhTgd3(+gKKQZ&aOmtTs8XDRJ|8_&j{=n~FODzG!N@TdCNg@9IhC7jZDa`oz z_@_MBr~^(`4e$!>csoznJk^e>j%01KeTgWY#$x46w?yn9WH-|q}kI_SX6 zzsEWyBLhK{VCdlQ4wHY&_zQumi1vz0p$j#ZX-08vNhhvmO3{Vj9HX_E^=GZCxTZ9N zuRLtC5!iP|IIT|PU?Aldw^~dkpQ^6MmHM87O@TZH)M~ZbE~oO44CG;O4GYM>n|MI2 zjYjCe0IwTN{uSnjU9Wmri-G=XAo;&gLT9D!r);3s@6JqJN~KbnFwg~^%>?->jD6d{ z6>uY$g9nX{M?m$j{wb*nm%%YJg02^KMFo~e(SF60wsx|NlaZ;&$Ft4pcyvv_mI`)npquz5~tR9HW`mIL6U8*>_F z{T#1KHIcd9>i${W6Y;E5H!3+>W8Sctr@LV!HK4J_;*?4UYqN&^WN5Z%z77sW< zzPh-*5YXLB`_)YX2SK`}OF`TcWiAT{tT%uv%35mWZm3{#DWn9cZq%OCXmyL8FSWa% z|9@;UNUn?{LDWqW(wNDiX6aEF;GhczxVpl_8Pf(lu9}D3{2E5MyuzCm(=G(yo-QN* zzAAb(sRALd-lqO*nE@W!u70-q4>xp2CTE;@{Z{3y7rlNbC<;27R{tFX^>OT1)~69@zXfj}S-2m}IwKp+qZG~j=z$DAMM{UX%> O0000mc(Cb48X@ZhiK&VhH6c8|AX$g-Y6`|6mK(SCB-^3+(i}VceY@Z8a=q>(+iULieKWH=JM){Fy_%Xxks?Kk6e&`qNYzWZLZPs!v$ONZ zZQHg@(&GbqyibqU=|r9{{GjZdh;3?}GmbVsu*;7EoQ5M5CR3i#Y+AE3`I(%2X!=*ATtF%85>_(M^0>1KE> zjXEB+!h7)A1%oJfJmNvg@Io^Q>m&*GFB*T-xP$-k9KF^$Jw5%(@(;kBq^S5fOp8wV z2;8FUsUf(9B&=U~rs{0?B#kdr&;+m{dhG^%#~cRUOV17G^Z8GrgpKZuO$qS`9V za6;y;;ofi$`2c`(RgDutI68RPLIGz~I6hxz-eQwRLjO7->caoD3E*8-l&Zi7N8-b^ z*^#b?%4d3g)}aU0v!WHy+}!*YO2d7a9f^8+dOkpGWCqbKL*Myu)hU206=p-2ofP-dfFdlkEUg*^ zfK2OkmNEZ}kCH(S-e8(WQE5^23h3_ce$QmqmE$)a;p80ree(N1BSG(`YaOHie@C${ zS5Y8P>#T}kj2QaTMhO^bs0)ng=StNSRTUv~$UkGb5`Kt@6$jMU*Y_?uQfv|{oMMca zz5`_RO`6r?Hi;G5Ja?`-B>nKUlan1W5`F7zAx+(}fZA^J@+7wm})Tc#MD zDxt(5QkUiYafkgBihk+w1l|L@DTFzMhp>VD<|&ifdwTy96wo3KNH_bwBrGsN@7rTq z*N=8ox<+qS{(u?L_YB(dhtDFmaK0iDInsp=*+hcy1>3=tX@4($CU0v3 zkq2Q6`KtG~Vw*@G3=+yZnu{X+p@) zN$W+A)d@%i4oSW>rxs^SGFq6;DQ7)Wq9dO9gQs=~*E03b>v`i@3REUCCe2*f)U(Yjua+aNwW(wC0ji+eNNK}9! zf5nZp#2+RJln4H>-+({v)XBtOxfgOo$0(K+W)J6EYttV zy(K9im*P7Xeenl#q}>y+A?dyZ32wSro6W|8&mg<4OOi9*->Zog*6AFABn7~sNLWRv zqUf;U4=62wIvu`+$%~Cy%A3BeJKhC$n0?$#L^}A8d$U9#K+w`X>O2;m0)W8kyej03 zL+}R$srI{^?syma!au;g%eaDr9qA?q9V?(%horgQHJ2rdG|SiPl)%T~~ziw5n0$^Rm^GX?gRC+z3&dibObjmwfY!sm3 zZ!%D>V+|w0YW_$QzX>ny3zc7Bq*b*FPC8J)Xa(?hP~h(b6fmKB5lbgvf<7+@^g>1j zz`edAM5WCY!Jkwl<-_&VM5vpEGv2N*H-n}MU_^IXw7&t14@5^p%KU^Zzd)8u6^G#T z!sUnOnJR!$-8liPUmuD;PG~7}BI44i25P<6?0Aha&v3aFO?vS@+= zkmL#%O%QM%q55n~Krf+HxWS8AHepUVPN+8FkWPk{Kgms_F%Zy87?k<|9JrxpE0w>Z zn@xZAzU0G8a}pHdA`sK?HyLPutJMZ#TnVhQzfbA;M&CCGxI+cV;kj~@(sBhfN>E3L zeL&L@`m}gf9Um2NOJHcU#$(wZfH34rg=^0^LPK*>%|9AGe1eUf;yo{Iz|Vm|9UScw zuBFx2ZURShHNz|`;e`REsj^KvIGSGj%H!tSkqAeZA~h);35I>N@N?_v9bq;m=RMz?V) zt!-HfP(fXXBzq=1eYv}Ijqeg_f^K3@u;C9ID)viVlOM~rU;8C|2D7%_5jg}C7mjW4 zSW*QUI@xRdHe2M9&L_e>!aMTB-$#Fv&K@2 zQo!@ZMXVNN!XKBGJm(Jp9{9um;15Zigc>J1{%{dsC~1pxSs71C9Wwm?l0k9qg^=hZ z_oZDnDX_KWkKcERJv5`6T#J-hfXd@ljaYk-+@P_`M*{=bi1ydK z+y=YS#{9qofA^g=5TZAXn;8}y+!zushdH>bgzm;12TjKu@;8iHROdI!b@3yb2v&Dv zvk0&%`L}!F%>cGYIxj+U(fP9*LrkpF4DE znFHl=xx1&QX9V^bHV69%0Y1Q9!zK%b!rfA-bP2x`V$8o@egs61l4x9ig5+O~jQ@lG zUBK@h$ScimNhA_pLibzMZQObs+j*$+_D@+0q zAm(^GOB39%73iXT*aR>Wa?QO@+Q}q)9LiVSQud|6BZbC%jBGwvL?KtJUgGykIw8_Gu8kd^@9q z&&au>BTm=zCCtlAyU^QP85Nx9F;WYm6#@@_kr9i_J-$gXrZ`W8hIn)6Vl+%Wz6lY& z;hkl~2JiuFu5Jlrw7~7w_iBklQgeaQ%JbpO# r90`IT2!bF8f*=TjAP9mW2%UuAle?O+vaETI00000NkvXXu0mjf=<*FS literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/btn_home.png b/app/src/main/res/drawable-xxhdpi/btn_home.png new file mode 100644 index 0000000000000000000000000000000000000000..1b32b28e65640c70b5cf754ccd2e75b6ac7eb92c GIT binary patch literal 826 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U}p4maSW-r^>)td?8^ZX$Hdnk z-mTOneuV$1QtJ{={m7+Bf`3^LeOvZBZ~5h&rZRlxlRfr5)ZZ4h zwmnef@3i@^nUZ#1%y^?bQzm5f)owER)xth@DT z_tmt`l|4(00wmhfpZe8r4*fAJ{^!2>t|L5N@7{1f^0(%&|H#hT^MuFaT~$1j?0JE@ zPlaDfGc#WraW0##KL2^R%89L!hp*dAyMCo_mssIBiG6o|URV7SU)(a^hSUCORCuF7 z^1(~-#?QY#iQ2B2bK7I8CgPNq{X}ND}S^u+r&D*Hd$>FLVW)=BG4Vw}d zU%hpt+->p2b*r}CcD?zaS%+&{rvJ4aYxwFkB3Ps5T|2&1Kj&46m)Yk{IS;ez;?`g1 zpZ@in{pQ2MI=5r@32$4(J?(VsS?;C#`=H#wqE-hGcwzbzK&s^!ngUb`{dpzM$ zZdzyg`_Qx`mkXxp=0cgLIm0;Z`g;0ZW=Q~*9&=Ob?d7ogq4LCDNb>l{iCXnOo-5`* z-TQySSz|k3s)GZAPq~YK%+8vgx#r)sC)>B4V+?5CoVCj-*1We_!6JRr!QC7Q>v(T( zlY?s@$Os5ycxB)168OdLt<3o?sh5@Sf9DTNwXHl6JloIuxbfSQ`@24BZIOc|OmOO4 b!SR=o>+0{B>vbM_fh_QJ^>bP0l+XkKV5o50 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/btn_local_tl.png b/app/src/main/res/drawable-xxhdpi/btn_local_tl.png new file mode 100644 index 0000000000000000000000000000000000000000..3ade7c4754a619a15c49cbab1249d06b11bcb765 GIT binary patch literal 1543 zcmV+i2Kf1jP)>e+Zr^QC{}Ag8mZW(*v3+<6oG&1 zIpKqBw!1qs_uko+ne!!I_Q5V0&bjw{-=VKhB9TZW5{V>~)a&(I7Z(?A5fWVzf;}=a za%gmP^s8#MdIh?H*XwwF3C~ZAkB>i#kClW}1~M=(a4*1~hOPnd%~lE$R`K}B)YQ}+ zLM)&VPyj#Q4)`V#Ht_g8&6AJ|Wak0u*Ga%{(ql450faUm`fpkS@SW?>CxoDM zvqI5R#>U1z_t*h%3qgMZp6itTc8GwkRVtN7grL8aSYB{g{cj1nKr%QucpG-XjFlPU z?EX6Rq@tg|(%;{IJAkcsGDrB?KeYSIAB0$Tq3L7T2_FFHSpa?k`Uj&Oynd%#F5fSN zx+F9)tdeqP$lPQsSmAqBUhZPlGdw)}B+Sf*biB~ZbiU?*FEJuQX!^J^+zgy7zi`6%Kp&;c)DNh1kw?4xjJbyP- zKEddT`AOahC;_nFeUdGju*P4A_ajC<0SH*yRSw&Q01i>Ho8{99Yc+^vtX6v z{{Q9T10;PrXMcYUkgbx0LqasA*o~L2hHAEmg!K0RnZRvF_L{3gHle_QzLG)j_d3hT zeHv4b7l8Fy!g-<8Q*qc`6Cxo#Uzkro0BifRE+uMCS!h(fjUdUnC(Z%eWZTm$8{qMm zw7bD}f^O2$N-XtXOFCw5wVN3i$D9kmLf1n8&dUkFn`yM;EDK;4d{a0Atd4VjyJ<}a z2i?zA>1tyE&ZkN2e~W8j@oqY)Im!zG2b#@=-71)+<)2wWv^md5lSUpSGY&kPVr%u? zz4-YTg(09^F29r}BLz8U>~wpTrxu&*|J*fkohe2!oy;D~7@Tk`1ObjWNovea|1<~s zUKB;cT-gb}w-9$-B$7mTGDmqKU^ei4=0<|goJewQ?DcjUKiTyJ*eYA?!SSM+2-qr^ z?eD<}$JL~>{JAH9E3pTwf2rLWg2EACHJgXADo1#GFh)2$|C{AeRQ&F5lU`gA2)Kz5 z4y!Wehj|*cDCCU&8J tWm9^s^ny^zR!TN6+ExXTL?V&w$bSjjJg4VN@{Rxi002ovPDHLkV1lnn%(4Ig literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/btn_more.png b/app/src/main/res/drawable-xxhdpi/btn_more.png new file mode 100644 index 0000000000000000000000000000000000000000..fce82fd3ebd62b64b633b327370f1a7316270e1d GIT binary patch literal 542 zcmV+(0^$9MP)cN&e>)nD9^!r<_nWv*;`e^}A7xt6 zYHtev6!+K9uK3$BuH*T9zO7>2kD9Bm3jawXE;XUUu@mFv?6<}L(1c^-r~}jK^e`Im z+@b-?W>26-6!ZMA#ecub#;k&s<0lHasA#~A64q_LdvQIsZ9qAqdQV7fK=YoU0W~8i z3V2uX1l%fNrC7xmZ5vQt+*GjAY&JWJLcX+E{ZHkyg$h<$EEZcWpMp~_VjUR_2D`DM zum7<6uWY_6XhQQOynJ)&c`lzV6n}-|sO$-)5#@ALj;PHswY;pIm(Lc)&U%liRuTu}1(kqgS*eC2}va|7gpIv7E6LCNHTlF0=nlMCtrxuBjjK`y8V<47*3Uq@8A gAOHXW008*e8#*@>s*2jjGynhq07*qoM6N<$g5db_S^xk5 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/btn_notification.png b/app/src/main/res/drawable-xxhdpi/btn_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..67f5ff83069a2f2956c88419ba92f6a47543dc1c GIT binary patch literal 647 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>VDj^HaSW-r^>)t5EG9=0*Ru|4 zlinCj^c8F3KDaZmeBGvA37(%#0-u>qv)!{(vf192RlmL8q(A+%`qH44s@H#eExk1T zwZch0;};#XcgDOk*!JM^%P-4nZZ564=&|npkL{|n{aF=llMh$__;hglrBC}gs&<(w zf9wtCxb=y%-i+gx@SYZVmV<_=$JfeKR;s;Vp6)PL(L|Zw^r`c%mX|yWgflxqMKru>f@t3mT3_PYco!<5zooool=QMjh2>z5ItYtGBi3 zoH+zE0~q243=9J24ZgU>+4pbW^Zk^;l)sFBL>ZCHZ)jzka5ClFv&@*eJ7#X!wxVE~ z=K_#cheLchOb$=vE?iB%&yeu_d!_cJg|!SFSAN--%oSpIQzQHHOaOz$_J7v;?&8b| z&u0A>V>vk0regaZnP{E{`=7BHRsR%E+HjdmS6;CN82byCUglq4!Sna;@yB04 O-tu(yb6Mw<&;$Sv%L6O` literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/btn_refresh.png b/app/src/main/res/drawable-xxhdpi/btn_refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..2b3f8534e1fa94119fd1ebb6ce1540381560d743 GIT binary patch literal 1573 zcmV+=2HN?FP)ia^W1 z_4~+a(lp(jncbaxXKv4zOzx)L-JUsT=9@ET&YUKRLZMJ76bgkxp-?Ck3WY+UP$(3N zdKeoUTRSo`vXQM;tF2LucmR~UZg_b3?cUzro^rYTHJ%sIZua%{-A9}4@9%%ahSxLv z8D8^u7x11Vc->tpmENMEe8@(l<8KXf0OU^b0|NseK#3>O!C%pyL7A_bY543@8U^3= zEne?xr;HpB90X{abD{Lk&dxWWsDtRx?|3dWtMqz}--++Ojpu<@l*W-y<%@y;j@|&G z_+vsTuY5N##GiP5*j0Ip%E#yCqc;FM17!@M1Jh|Jz79p*1KpJ%PQih8=9hdvP6G@M z4!(1GG_rGH z0PttO!-D%4xcy^CnrS+S*Dg;iSOd_{mq3|KNA5uRJJObs3pr-PAGtGt`1(^e3mv1I zT%@xMgFu#_a%BL97i<hM4>DJkU;%27nMJ4Ak?GYeXJsCpZHzb%faq24T{LzpGr&gE$ye4f|5a~*%sFzk zQ&281FoXotBHSqHRnZ;;+%l{IpQx5H+=!u#sFpke3LZ7gfGfHX``!AN3^U+SS6A0o z)s!c2k+rzV*d5{8ww?p0UXvhP_A{HVDANlq4&!4pz=Q<#R%}e24~AMH%xS|qFw1=X zSdNLPFwT25$zv2laYPqI9icNqO!i0utY?uhZ;j_87Pq#|@++#$FrUNqgMM z+RBB2@p%$)IK6-lTy?Ay!MYBk?Tj6f;YcZla9z>%1=8Y`Gc+<3ezeeZ*PDeW#4KnC<)m?W-iOkcD}_DOlRHZqkqRxFRmlp4LZMJ76bgkxp-?Ck3WY+UP&CB< XVOt!J%mVzR00000NkvXXu0mjfNtE{G literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/btn_reload.png b/app/src/main/res/drawable-xxhdpi/btn_reload.png new file mode 100644 index 0000000000000000000000000000000000000000..315a253308489ef940eab3732f9dd9668f4b3459 GIT binary patch literal 1521 zcmVVX2gUlTR*RQXuWd!}UjME0$p$3G z%$}1yXJ_3RSn|@G?0(;#Z)ay`wuOS4nwpxLTA(&GG<0@xadDj@;mNAi>UWT!NO-cr z!NH$I!Vmhqq-b~|ph1Glkw*dy2`WcE2~ar_K!D1T00LBw1Q4KdB!B>wBLM`c9A#uv zQ&a25$HzC@5TJ6D(g@%UwOVa=rBW%C%jHY-d7Ii}0&|JldjfRDmH?F_dn*!12MOp& z0_+L3&q+X5IZA20y}dhVmY*TuUs79h3A)M=13UqK)kwHHNT7$peXt)jd3k3ci*Wu_WWv3xMH28T7$yq~m6CP40zn7!z6oUv`(BNy9 z3*!v6jh+5p9pv%bbr-5^>b=?`ADKKbk$ZQmM2}903RuSj_$#t{1W# zWalsE0bz8Bfwm=P_J1-V1@VT{vR2^#LBfKl4%3eJFz0eYO9R@+T;ax3E|(8TlAmd& z!^=2&421xbiSdznqpwJrEgPvlWAy)KXd6nJ;kOt9=8>Bufq-u6&{yUSD1}DA=;-M2 zc2QicX-NZzSi1%<38NJ(fVg9OEmW3Y7pH z$}j}nk+4~r{NEy>jZg#U41!;#&ICyDOm44ZEhe3JG-GCgrvSDr?zsMF3_#7G0Y%THI|B zZOvJ;?6@ERnvY5y%W|q!Trd(X`0Y}~)3};Aqd-Q3?G0PFV5AVEao{HG2o_bOl+$-lBO$oMOQB}HrkhBYoeVE?!j+cODm#9_|9}fdqV)9zfIp8!+$jBg zl!ST&9w&9RXsCs%zBLK;3`qEj)JfDa zqnNx9>ovtTW)gktX>M0El7e%H=crjj;UE&mP#h9+eL@g&gdHz-tolCzJjs#5uVE16vb2V{X#_rK?E@pA(CX1?1tDNi=VXBDu@;FODkd#sVEjj6bux_7l;~B|69+6 z4J@T;ck{Y)$Mb_>w@`Ms_v}4$XLhEA0wIJDLI@#*5JCtcgb+dqA%y(5%p{Fy&}=qm z8;!<(Mqzm&`6gN%qy@PAVP1^U5<{rsf>3U9syzTYT-g6&WMW!s?u z%CFsDBBJNP_`Wkplmx7K-qREfU@mS0A<^u0Lr#Q0hDbg-RkxFABgY)=TFcU za9*!eDyJdJZbo=Qwv(at5>X!D9&MZ-kK=eJqdq3v8B&nfIL>uCouhp7N3@+$S|a*$ z$qZQ40osmIlfDDT_c&f{wOWU1JBm#zDF|`H+l0$D5}zSbX%`ht6ehDFCh50JQScmh+A;J zN)ijg+j7%a5yCKBbiQ+z2b4_kO@LItaNR_Onc%S<{gV9&CdyJ@?FfI@ z541BT%2QwMNM=>r?e;Mfnc%A($tO`0lX$+`k-StglWJzP?4un?e_&?b$X7eU-}BVW z_k=OoQ4clw3a;}Q$7lHYCC=ZXt;-}@GOaw-@c~-H)a>afc9daZ)(kT32-OiQxv zA%Y}AeVtGitee`qJc=C+yE0DhSt^wdi7+yvm3e;=WE_^5eDI$$_YGkfI~wQGL=gnn zyMO>Ljv+g~VQjF^+?TYxb~LHAp}6iYTtC2ZRjQjIt!Uolr_4NdH0i~;@;p*nilv}^ z&MCl0$tsf~WVNGdu8x+@9QVAPr?SHDUNO6(B8weqEKiqt9|U$;O5h9vyc0V!DL@uG z(%K&&&lW||S=|5Cc2gA9YPD=O3%0W(?F|~mVsQ_ip2n)W@w&G2S2OU^gk2A*#H0l literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/btn_report.png b/app/src/main/res/drawable-xxhdpi/btn_report.png new file mode 100644 index 0000000000000000000000000000000000000000..992a50fb766cc14f303df843366d11797e40046a GIT binary patch literal 725 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V4CCU;uunK>+Njc?BGCww%bZy zk`BUGn-g#oJD4=>lDeQ|l=PciF-{|P5od0W$pK9p^UG{x;=1gUrgof{fd2^o6 zTmF1mv6&f%VvB$igfKi&V|V{m+3xneaqr6vB<@5`iwl0WmnV7it&{S6?e2f;{vWIQ z^nYdcPDZ;BPRI9u4o&_q?zCCxg5SzjZti|tg4g%SWG>f}t-RuAYE`su?jI|z1#{}x5HeKn9b5{62WxoEjse8&YP5H#F zHeKtBNjH@1kN2{T4SQSmd#cZwN>i}t>yWJX3qNYGRQ}c3+voMROonO0^}v&N``j6x zS!wOOUC495&s|l1(;Vg<*Du~;oN}kRntcH)FwpP<0oemdCQ`B*EvZX&wcBzvzcOY> zZ5BK06%p2bsdnW`Mi&j3_AG`4x-ZqEd{0|3@I*7%pIl;pJ?_l1z#pxD-gUP#E8S#_ zDY2Ss{{Llk?ecaLh7J4FANFgvZP(O(sLgP#{JVX$>Aq*HyH;;GyK~c-{-@0Uv_3HI zKby8$ef6>HSzEU~-TRqo^=G%}?`!>zPic4{*5J2KF6!dWjW&x)a`x;w|Mm5=(B#>E z%YRR+?+CT7dOq8?&QsPlt@HG6yXBYr#ARRFM=y!9H<7D+oBO$8n!j~l$r53mdcHL4 z`eOz&eadDxH&1`L^>IddiSG5^dXqmM{;{Sg@~y=k*{65ky4;Wr-FTbx%Z9M&Wmy*{ zR=-WT7O3$y<#M3K+muUz5pPp21aiG~xf&?-*5yi|)mxWKfmv@Wt_N1Vt+*Cwdh6qg h#adgP5vi!_nfV!^`VTqh=huS5&C}J-Wt~$(698+HKcN5s literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/btn_statuses.png b/app/src/main/res/drawable-xxhdpi/btn_statuses.png new file mode 100644 index 0000000000000000000000000000000000000000..a30bacd979246acbc6bd65799a83019eb4810b9e GIT binary patch literal 1059 zcmV+;1l;?HP)uVE16vY$Y)+bm*DpIl4D8XdYX159SL;e88Qi--wTBw4eD2N|aT1%`}3u1lM*MI9d zFp6l?-6lJ;$?iFD*|s#@+54NF$GtPtTuzcCNs=TAk(<+b?W@l z(b4_5@q?^7ehUzHTtdj>X1C<{|C*<1C*H5sYG}=N@Iq4H{lNmX@ErcoS0C1C4 zIlHYbSqFfdY|7DX`+*G?0Oag#<_Q9junB+w2taB9U#>7{yo<6otD${yyN)nLjmYAzS3+q zcXwLWMx(LE^~^mL08CEdZa2>V8-mL>AuVsf8avpy%50jWbpeQ2`_XLFEtI<<0MV?4 zX5)Z`AmV_FA!-`lfEyOeJ}&?vR;$%6*($r4tO-EKxPQ=A**RktfN%iTlG~in2|$7X zN~O{v0SK`m2u@hcXAcQL_#1H1R@oUv6@YL6O^c(TIROXvhHYQ@sCkQ}D}Odu9R9@xJe0wYdr+!ZMYU z+&^HNyUYOKM(>$8m!EelE??)mQM?h~QVoD@Jg$Z9I?|-X4jg3*IMPZb01gvb({ep7 zG>fND*5yocLu=Q=qTspkN2l+lZAegyGku!S@L z7|!*nFhsu4_W)c>u5783{RBM+AlKi@3qZmW009sHC1jKl6954a00HQUV=I$Pc|F1vUGbaZ`^N7w_vh|Z7Rvi5&+6?X3^Oq+q)f74mmo!fx8XiAR%Q~U4p zf9g1$q7#C76E-vKcxKFIojd=Z=4gZ+Ge<`m8KQ1-eJYzAlWgk8Y@Z}ak|arzBuSDa dNs^R+egW89Q2}6-<1YXJ002ovPDHLkV1ixE?4bYv literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_account_add.png b/app/src/main/res/drawable-xxhdpi/ic_account_add.png new file mode 100644 index 0000000000000000000000000000000000000000..ad140127f237f9633df46f4d69f1c049137d1b8f GIT binary patch literal 1125 zcmV-r1e*JaP)DE znFHl=xx1&QX9V^bHV69%0Y1Q9!zK%b!rfA-bP2x`V$8o@egs61l4x9ig5+O~jQ@lG zUBK@h$ScimNhA_pLibzMZQObs+j*$+_D@+0q zAm(^GOB39%73iXT*aR>Wa?QO@+Q}q)9LiVSQud|6BZbC%jBGwvL?KtJUgGykIw8_Gu8kd^@9q z&&au>BTm=zCCtlAyU^QP85Nx9F;WYm6#@@_kr9i_J-$gXrZ`W8hIn)6Vl+%Wz6lY& z;hkl~2JiuFu5Jlrw7~7w_iBklQgeaQ%JbpO# r90`IT2!bF8f*=TjAP9mW2%UuAle?O+vaETI00000NkvXXu0mjf=<*FS literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_menu_camera.xml b/app/src/main/res/drawable/ic_menu_camera.xml new file mode 100644 index 00000000..7d1c5833 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_camera.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_menu_gallery.xml b/app/src/main/res/drawable/ic_menu_gallery.xml new file mode 100644 index 00000000..2f2ca2aa --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_gallery.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_manage.xml b/app/src/main/res/drawable/ic_menu_manage.xml new file mode 100644 index 00000000..065d9fa8 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_manage.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_menu_send.xml b/app/src/main/res/drawable/ic_menu_send.xml new file mode 100644 index 00000000..a5546577 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_send.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_share.xml b/app/src/main/res/drawable/ic_menu_share.xml new file mode 100644 index 00000000..8151b38e --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_share.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_slideshow.xml b/app/src/main/res/drawable/ic_menu_slideshow.xml new file mode 100644 index 00000000..e7509918 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_slideshow.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/side_nav_bar.xml b/app/src/main/res/drawable/side_nav_bar.xml new file mode 100644 index 00000000..c1dff26b --- /dev/null +++ b/app/src/main/res/drawable/side_nav_bar.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/act_main.xml b/app/src/main/res/layout/act_main.xml new file mode 100644 index 00000000..7266a595 --- /dev/null +++ b/app/src/main/res/layout/act_main.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dlg_account_add.xml b/app/src/main/res/layout/dlg_account_add.xml new file mode 100644 index 00000000..cf53804b --- /dev/null +++ b/app/src/main/res/layout/dlg_account_add.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + +